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: 1

Exemplo 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 Silva

Mutex/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: 10

Exemplo 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 Cell para tipos Copy em ambientes single-thread, pois é mais leve e sem overhead de verificação.
  • Use RefCell apenas 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, OnceCell e Lazy são excelentes, mas lembre-se de adicionar o crate once_cell ao seu projeto.
  • Evite usar RefCell ou Mutex desnecessariamente; muitas vezes a mutabilidade normal com &mut é suficiente.

Exercícios

  1. Crie uma struct Contador que use Cell<u32> para armazenar um contador e implemente um método incrementar que aumenta o valor em 1 e retorna o novo valor. Teste em main.
  2. ✓ Resposta:
    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
    }
  3. Escreva um programa que use RefCell<Vec<String>> para armazenar uma lista de nomes e adicione um nome usando borrow_mut. Depois, imprima a lista usando borrow.
  4. ✓ Resposta:
    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);
        }
    }
  5. 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).
  6. ✓ Resposta:
    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
    }
  7. Crie um OnceCell<String> que armazena uma saudação personalizada. Use get_or_init para definir a saudação e imprima-a duas vezes, garantindo que a inicialização ocorra apenas uma vez.
  8. ✓ Resposta:
    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"
    }
  9. Usando Lazy, crie um valor global que seja um vetor de números primos até 100. Use Lazy::new com uma função que calcula os primos e imprima o vetor.
  10. ✓ Resposta:
    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);
    }

Referências