Rust Admin

RefCell<T> e Interior Mutability em Rust: Do Básico ao Avançado Já leu

O que é Interior Mutability? Interior mutability é um padrão de design em Rust que permite modificar dados através de uma referência imutável. Normalmente, Rust impõe que você tenha ou uma referência mutável exclusiva ( ) ou várias referências imutáveis ( ), nunca ambas simultaneamente. Interior mutability quebra essa regra em tempo de execução, permitindo mutações controladas sem violar a segurança de memória. Isso é essencial quando você precisa compartilhar propriedade sobre dados e ainda assim modificá-los — um cenário comum em estruturas como gráficos, caches ou callbacks. O custo dessa flexibilidade é a verificação em tempo de execução. Enquanto o borrow checker valida referências em compile-time, interior mutability valida em runtime, podendo causar panic se as regras forem violadas. é a ferramenta mais prática para isso: fornece mutabilidade interior com verificação dinâmica de borrowing. RefCell : Estrutura e Funcionamento é um wrapper que armazena um valor e mantém controle sobre quantas referências imutáveis ( ) e mutáveis ( )

O que é Interior Mutability?

Interior mutability é um padrão de design em Rust que permite modificar dados através de uma referência imutável. Normalmente, Rust impõe que você tenha ou uma referência mutável exclusiva (&mut T) ou várias referências imutáveis (&T), nunca ambas simultaneamente. Interior mutability quebra essa regra em tempo de execução, permitindo mutações controladas sem violar a segurança de memória. Isso é essencial quando você precisa compartilhar propriedade sobre dados e ainda assim modificá-los — um cenário comum em estruturas como gráficos, caches ou callbacks.

O custo dessa flexibilidade é a verificação em tempo de execução. Enquanto o borrow checker valida referências em compile-time, interior mutability valida em runtime, podendo causar panic se as regras forem violadas. RefCell<T> é a ferramenta mais prática para isso: fornece mutabilidade interior com verificação dinâmica de borrowing.

RefCell: Estrutura e Funcionamento

RefCell<T> é um wrapper que armazena um valor T e mantém controle sobre quantas referências imutáveis (Ref<T>) e mutáveis (RefMut<T>) existem em tempo de execução. Diferentemente de Box<T> que apenas aloca memória, RefCell<T> adiciona metadados para rastrear empréstimos.

use std::cell::RefCell;

#[derive(Debug)]
struct Usuario {
    nome: String,
    saldo: RefCell<i32>,
}

impl Usuario {
    fn novo(nome: String, saldo: i32) -> Self {
        Usuario {
            nome,
            saldo: RefCell::new(saldo),
        }
    }

    fn depositar(&self, valor: i32) {
        // &self é imutável, mas podemos mutar saldo através de RefCell
        let mut saldo = self.saldo.borrow_mut();
        *saldo += valor;
    }

    fn consultar_saldo(&self) -> i32 {
        *self.saldo.borrow()
    }
}

fn main() {
    let usuario = Usuario::novo("Alice".to_string(), 100);
    usuario.depositar(50);
    println!("Saldo: {}", usuario.consultar_saldo()); // Saldo: 150
}

Dois métodos são centrais: borrow() retorna Ref<T> (referência imutável temporária) e borrow_mut() retorna RefMut<T> (referência mutável temporária). Estas são liberadas quando saem de escopo. Se você tentar fazer borrow_mut() enquanto um borrow() ativo existe, o programa faz panic — justamente a validação em tempo de execução.

Casos de Uso Práticos

Cache e Lazy Evaluation

Um uso comum é implementar valores calculados sob demanda:

use std::cell::RefCell;

struct Computacao {
    entrada: i32,
    resultado: RefCell<Option<i32>>,
}

impl Computacao {
    fn novo(entrada: i32) -> Self {
        Computacao {
            entrada,
            resultado: RefCell::new(None),
        }
    }

    fn calcular(&self) -> i32 {
        let mut res = self.resultado.borrow_mut();
        if res.is_none() {
            println!("Calculando...");
            *res = Some(self.entrada * 2);
        }
        res.unwrap()
    }
}

fn main() {
    let comp = Computacao::novo(10);
    println!("{}", comp.calcular()); // Calcula: 20
    println!("{}", comp.calcular()); // Usa cache: 20
}

Observadores e Callbacks

Estruturas que precisam notificar múltiplos observadores muitas vezes usam RefCell:

use std::cell::RefCell;

struct Modelo {
    dados: RefCell<String>,
    observadores: RefCell<Vec<Box<dyn Fn(&str)>>>,
}

impl Modelo {
    fn novo() -> Self {
        Modelo {
            dados: RefCell::new(String::new()),
            observadores: RefCell::new(Vec::new()),
        }
    }

    fn registrar_observador(&self, callback: Box<dyn Fn(&str)>) {
        self.observadores.borrow_mut().push(callback);
    }

    fn atualizar(&self, novo_valor: String) {
        *self.dados.borrow_mut() = novo_valor.clone();
        for observador in self.observadores.borrow().iter() {
            observador(&novo_valor);
        }
    }
}

fn main() {
    let modelo = Modelo::novo();
    modelo.registrar_observador(Box::new(|v| println!("Notificado: {}", v)));
    modelo.atualizar("Nova dados".to_string());
}

Alternativas e Quando Usar

RefCell<T> não é a única solução. Cell<T> é semelhante mas não oferece referências — apenas cópia/substituição completa de valores, útil para tipos pequenos como inteiros. Mutex<T> é thread-safe e apropriado para ambientes concorrentes. Arc<T> combina contagem de referências com Mutex<T> para compartilhamento seguro entre threads.

Escolha RefCell<T> quando: (1) você está em single-thread, (2) precisa mutar campos dentro de um método que recebe &self, e (3) está confiante que seus empréstimos em tempo de execução serão válidos. Evite quando há risco de panics frequentes — eles indicam design problemático. Se frequentemente você tenta fazer múltiplos borrow_mut() simultâneos, reconsidere a arquitetura.

// ❌ Evite
let cell = RefCell::new(5);
let _r1 = cell.borrow_mut();
let _r2 = cell.borrow_mut(); // PANIC!

// ✅ Melhor
let cell = RefCell::new(5);
{
    let _r1 = cell.borrow_mut();
} // r1 liberado aqui
let _r2 = cell.borrow_mut(); // OK

Conclusão

RefCell<T> e interior mutability são ferramentas poderosas que desbloqueiam padrões de design impossíveis com as regras normais de borrowing. Eles permitem mutações através de referências imutáveis, pagando o preço com verificações em runtime. Três pontos-chave: primeiro, use RefCell<T> para contornar limitações do borrow checker em código single-thread, mantendo segurança de memória; segundo, sempre estruture seu código para evitar múltiplos empréstimos mutáveis simultâneos — panics indicam bugs; terceiro, considere se sua arquitetura realmente exige interior mutability ou se uma refatoração resolveria melhor.

Referências


Artigos relacionados