Padrões de mutabilidade interior
Nesta aula, exploramos os padrões de mutabilidade interior em Rust, que permitem modificar dados mesmo quando há referências imutáveis, respeitando as regras de segurança da linguagem. Abordamos as principais ferramentas: Cell, RefCell, Mutex, RwLock, OnceCell e Lazy, com exemplos práticos e critérios para escolher a mais adequada.
A mutabilidade interior (interior mutability) é um padrão de design em Rust que permite modificar dados mesmo quando existe uma referência imutável para eles. Isso é possível graças a um conjunto de tipos que realizam verificações de empréstimo (borrow) em tempo de execução, em vez de tempo de compilação. Essa técnica é fundamental para implementar estruturas que precisam ser mutáveis em contextos onde a API pública só oferece referências imutáveis, como em callbacks, caches e contadores.
Nesta aula, vamos explorar as principais ferramentas que Rust oferece para alcançar a mutabilidade interior: Cell, RefCell, Mutex, RwLock, OnceCell e Lazy. Cada uma tem características específicas que as tornam adequadas para diferentes cenários, e entender essas diferenças é crucial para escrever código seguro e eficiente.
Cell vs RefCell
O tipo Cell<T> é a forma mais simples de mutabilidade interior. Ele permite que você obtenha e defina o valor contido usando get e set, mas não permite obter referências ao valor. Isso significa que Cell só funciona com tipos que implementam Copy, pois o valor é copiado para dentro e para fora. Cell é ideal para tipos simples como inteiros, booleanos e enums que são Copy.
Por outro lado, RefCell<T> permite obter referências mutáveis e imutáveis ao valor contido, mas com uma verificação em tempo de execução. Ele usa um contador de empréstimos que garante que você não tenha duas referências mutáveis ao mesmo tempo, nem uma referência mutável enquanto existem referências imutáveis. Se a regra for violada, o programa entra em pânico. RefCell é útil quando você precisa de referências ao valor, mas não pode usar Cell porque o tipo não é Copy.
Exemplo prático com Cell:
use std::cell::Cell;
let contador = Cell::new(0);
contador.set(contador.get() + 1);
println!("Contador: {}", contador.get()); // Contador: 1Exemplo com RefCell:
use std::cell::RefCell;
let nome = RefCell::new(String::from("Alice"));
{
let mut nome_mut = nome.borrow_mut();
nome_mut.push_str(" Silva");
}
println!("Nome: {}", nome.borrow()); // Nome: Alice SilvaMutex/RwLock
Enquanto Cell e RefCell são limitados a uma única thread, Mutex e RwLock fornecem mutabilidade interior segura para múltiplas threads. Mutex<T> garante exclusão mútua: apenas uma thread pode acessar o valor por vez, bloqueando as outras até que o lock seja liberado. O método lock() retorna um guard que concede acesso ao valor e libera o lock quando é destruído. Mutex é simples e eficaz, mas pode causar contenção se muitas threads tentarem acessar o valor simultaneamente.
RwLock<T> (Reader-Writer Lock) permite múltiplas leituras simultâneas, mas apenas uma escrita por vez. Isso é mais eficiente que Mutex em cenários com muitas leituras e poucas escritas. O método read() retorna um guard de leitura, e write() retorna um guard de escrita. Ambos os guards liberam o lock ao sair do escopo.
Exemplo com Mutex:
use std::sync::Mutex;
use std::thread;
let contador = Mutex::new(0);
let mut handles = vec![];
for _ in 0..10 {
let contador = &contador;
handles.push(thread::spawn(move || {
let mut num = contador.lock().unwrap();
*num += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Resultado: {}", *contador.lock().unwrap()); // Resultado: 10Exemplo com RwLock:
use std::sync::RwLock;
let dados = RwLock::new(vec![1, 2, 3]);
{
let leitura = dados.read().unwrap();
println!("Elementos: {:?}", *leitura);
}
{
let mut escrita = dados.write().unwrap();
escrita.push(4);
}
println!("Agora: {:?}", *dados.read().unwrap());OnceCell e Lazy
O crate padrão std não inclui OnceCell, mas ele é fornecido pelo crate once_cell, que é amplamente utilizado. OnceCell<T> permite que um valor seja inicializado apenas uma vez, e depois é imutável. É útil para inicialização preguiçosa (lazy initialization) de valores estáticos ou globais. O método set() define o valor se ainda não estiver definido, e get() retorna uma referência imutável. Ele é thread-safe e pode ser usado em contextos estáticos.
O tipo Lazy<T> (também do crate once_cell) é uma variação que aceita uma função de inicialização e a executa na primeira vez que o valor é acessado. Ele é conveniente para valores que são caros de calcular e só precisam ser calculados uma vez.
Exemplo com OnceCell:
use once_cell::sync::OnceCell;
static VALOR: OnceCell<u32> = OnceCell::new();
fn obter_valor() -> &'static u32 {
VALOR.get_or_init(|| {
println!("Inicializando...");
42
})
}
fn main() {
println!("Valor: {}", obter_valor());
println!("Valor: {}", obter_valor()); // Não imprime "Inicializando" novamente
}Exemplo com Lazy:
use once_cell::sync::Lazy;
static NUMERO: Lazy<u32> = Lazy::new(|| {
println!("Calculando...");
42
});
fn main() {
println!("Número: {}", *NUMERO);
println!("Número: {}", *NUMERO); // Não recalcula
}Escolhendo
A escolha entre essas ferramentas depende principalmente de dois fatores: se o ambiente é single-thread ou multi-thread, e se você precisa de referências ou apenas cópias. Para single-thread, Cell é a opção mais leve quando o tipo é Copy; RefCell é necessário quando você precisa de referências ou o tipo não é Copy. Para multi-thread, Mutex é a escolha padrão, mas RwLock pode ser mais eficiente se houver muitas leituras. OnceCell e Lazy são ideais para inicialização única e preguiçosa de valores globais.
Lembre-se de que a mutabilidade interior adia a verificação de empréstimos para o tempo de execução, o que pode causar panics se as regras forem violadas. Portanto, use-a com cuidado e prefira a segurança em tempo de compilação sempre que possível. Em código multi-thread, certifique-se de que os locks sejam liberados corretamente (o guard faz isso automaticamente) e evite segurar locks por longos períodos para reduzir contenção.
Boas práticas
- Prefira
Cellpara tiposCopyem ambientes single-thread, pois é mais leve e sem overhead de verificação. - Use
RefCellapenas quando necessário e sempre documente as regras de empréstimo para evitar panics. - Em multi-thread,
Mutexé mais seguro e simples;RwLocké uma otimização para cenários com muitas leituras. - Para valores que são inicializados uma única vez,
OnceCelleLazysão excelentes, mas lembre-se de adicionar o crateonce_cellao seu projeto. - Evite usar
RefCellouMutexdesnecessariamente; muitas vezes a mutabilidade normal com&muté suficiente.
Exercícios
- Crie uma struct
Contadorque useCell<u32>para armazenar um contador e implemente um métodoincrementarque aumenta o valor em 1 e retorna o novo valor. Teste emmain. - Escreva um programa que use
RefCell<Vec<String>>para armazenar uma lista de nomes e adicione um nome usandoborrow_mut. Depois, imprima a lista usandoborrow. - Implemente um contador compartilhado entre 5 threads usando
Mutex. Cada thread deve incrementar o contador 1000 vezes. Ao final, imprima o valor total (deve ser 5000). - Crie um
OnceCell<String>que armazena uma saudação personalizada. Useget_or_initpara definir a saudação e imprima-a duas vezes, garantindo que a inicialização ocorra apenas uma vez. - Usando
Lazy, crie um valor global que seja um vetor de números primos até 100. UseLazy::newcom uma função que calcula os primos e imprima o vetor.
use std::cell::Cell;
struct Contador {
valor: Cell<u32>,
}
impl Contador {
fn new() -> Self {
Contador { valor: Cell::new(0) }
}
fn incrementar(&self) -> u32 {
self.valor.set(self.valor.get() + 1);
self.valor.get()
}
}
fn main() {
let c = Contador::new();
println!("{}", c.incrementar()); // 1
println!("{}", c.incrementar()); // 2
}use std::cell::RefCell;
fn main() {
let nomes = RefCell::new(vec![String::from("Alice"), String::from("Bob")]);
nomes.borrow_mut().push(String::from("Carol"));
for nome in nomes.borrow().iter() {
println!("{}", nome);
}
}use std::sync::Mutex;
use std::thread;
fn main() {
let contador = Mutex::new(0);
let mut handles = vec![];
for _ in 0..5 {
let contador = &contador;
handles.push(thread::spawn(move || {
for _ in 0..1000 {
*contador.lock().unwrap() += 1;
}
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Total: {}", *contador.lock().unwrap()); // Total: 5000
}use once_cell::sync::OnceCell;
static SAUDACAO: OnceCell<String> = OnceCell::new();
fn main() {
let saudacao = SAUDACAO.get_or_init(|| {
println!("Inicializando saudação");
String::from("Olá, mundo!")
});
println!("{}", saudacao);
let saudacao2 = SAUDACAO.get_or_init(|| String::from("Outra"));
println!("{}", saudacao2); // Não imprime "Inicializando"
}use once_cell::sync::Lazy;
static PRIMOS: Lazy<Vec<u32>> = Lazy::new(|| {
let mut primos = vec![];
for n in 2..=100 {
if (2..n).all(|d| n % d != 0) {
primos.push(n);
}
}
primos
});
fn main() {
println!("Primos: {:?}", *PRIMOS);
}