RAII e Drop Pattern em Rust: Gerenciamento Automático de Recursos

Guia completo de RAII em Rust: trait Drop para cleanup automático, guard patterns, gerenciamento de escopo, MutexGuard e exemplo prático de transação de banco de dados com auto-rollback.

RAII (Resource Acquisition Is Initialization) é um dos padrões mais fundamentais em Rust — e diferente de C++, onde nasceu, em Rust ele é garantido pelo compilador. A ideia é simples: vincular o tempo de vida de um recurso (arquivo, conexão, lock, memória) ao tempo de vida de um objeto. Quando o objeto sai de escopo, o recurso é liberado automaticamente através da trait Drop.

Em Rust, RAII não é opcional ou uma boa prática — é a forma como a linguagem funciona. Variáveis são destruídas ao sair de escopo, e Drop::drop() é chamado automaticamente. Combinado com o sistema de ownership, isso elimina vazamentos de recursos e garante cleanup determinístico sem garbage collector.

Problema

Gerenciamento manual de recursos é uma fonte interminável de bugs:

// PERIGOSO: gerenciamento manual de recursos
fn processar_dados() -> Result<(), String> {
    let arquivo = abrir_arquivo("dados.txt")?;
    let conexao = conectar_banco("postgres://...")?;
    let lock = adquirir_lock("recurso_compartilhado")?;

    // Se ocorrer um erro aqui, recursos não são liberados!
    let dados = ler_dados(&arquivo)?;  // Se falhar aqui...
    salvar_no_banco(&conexao, &dados)?; // ...conexão vaza!

    // Cleanup manual — fácil de esquecer
    liberar_lock(lock);
    fechar_conexao(conexao);
    fechar_arquivo(arquivo);

    Ok(())
}

Problemas: se um erro ocorre no meio, os recursos anteriores vazam. Se um return antecipado é adicionado, o cleanup é pulado. E com muitos recursos, a ordem de liberação se torna um pesadelo.

Solução em Rust

Trait Drop Basica

/// Recurso que precisa de cleanup ao ser destruído
struct Conexao {
    endereco: String,
    ativa: bool,
}

impl Conexao {
    fn nova(endereco: &str) -> Self {
        println!("[CONEXÃO] Abrindo conexão com {}", endereco);
        Conexao {
            endereco: endereco.to_string(),
            ativa: true,
        }
    }

    fn executar_query(&self, sql: &str) -> Result<Vec<String>, String> {
        if !self.ativa {
            return Err("Conexão fechada".to_string());
        }
        println!("[QUERY] Executando: {}", sql);
        Ok(vec!["resultado1".to_string(), "resultado2".to_string()])
    }
}

/// Drop é chamado automaticamente quando Conexao sai de escopo
impl Drop for Conexao {
    fn drop(&mut self) {
        if self.ativa {
            println!("[CONEXÃO] Fechando conexão com {}", self.endereco);
            self.ativa = false;
        }
    }
}

fn main() {
    {
        let conn = Conexao::nova("localhost:5432");
        let resultados = conn.executar_query("SELECT * FROM usuarios");
        println!("Resultados: {:?}", resultados);
        // conn sai de escopo aqui — Drop é chamado automaticamente!
    }
    println!("Após o escopo — conexão já foi fechada");

    // Mesmo com retorno antecipado, Drop é chamado
    fn funcao_com_erro() -> Result<(), String> {
        let _conn = Conexao::nova("remoto:5432");

        // Erro aqui — mas a conexão será fechada pelo Drop!
        return Err("Algo deu errado".to_string());

        // Este código nunca executa, mas não importa:
        // Drop cuida da limpeza
    }

    match funcao_com_erro() {
        Ok(_) => println!("Sucesso"),
        Err(e) => println!("Erro: {} (mas a conexão foi fechada!)", e),
    }
}

Guard Pattern

O Guard Pattern usa RAII para garantir que uma ação de cleanup ocorra, similar a defer em Go ou finally em Java:

use std::sync::{Mutex, Arc};

/// Guard para medir tempo de execução automaticamente
struct TimerGuard {
    nome: String,
    inicio: std::time::Instant,
}

impl TimerGuard {
    fn novo(nome: &str) -> Self {
        println!("[TIMER] Iniciando '{}'", nome);
        TimerGuard {
            nome: nome.to_string(),
            inicio: std::time::Instant::now(),
        }
    }
}

impl Drop for TimerGuard {
    fn drop(&mut self) {
        let duracao = self.inicio.elapsed();
        println!("[TIMER] '{}' levou {:?}", self.nome, duracao);
    }
}

/// Guard para logging de entrada/saída de funções
struct LogGuard {
    funcao: String,
}

impl LogGuard {
    fn entrar(funcao: &str) -> Self {
        println!("[LOG] >>> Entrando em {}", funcao);
        LogGuard {
            funcao: funcao.to_string(),
        }
    }
}

impl Drop for LogGuard {
    fn drop(&mut self) {
        println!("[LOG] <<< Saindo de {}", self.funcao);
    }
}

/// Guard para backup temporário que restaura em caso de falha
struct BackupGuard<T: Clone> {
    original: T,
    referencia: *mut T,
    comprometido: bool,
}

impl<T: Clone> BackupGuard<T> {
    /// Cria um backup do valor atual
    fn novo(valor: &mut T) -> Self {
        BackupGuard {
            original: valor.clone(),
            referencia: valor as *mut T,
            comprometido: false,
        }
    }

    /// Confirma a mudança — o backup não será restaurado
    fn comprometer(mut self) {
        self.comprometido = true;
    }
}

impl<T: Clone> Drop for BackupGuard<T> {
    fn drop(&mut self) {
        if !self.comprometido {
            println!("[BACKUP] Restaurando valor original");
            // Seguro porque mantemos a referência válida
            unsafe {
                *self.referencia = self.original.clone();
            }
        }
    }
}

fn processar_com_guards() {
    let _timer = TimerGuard::novo("processar_com_guards");
    let _log = LogGuard::entrar("processar_com_guards");

    println!("  Processando dados...");
    std::thread::sleep(std::time::Duration::from_millis(100));
    println!("  Processamento concluído");

    // Ao sair da função, ambos os guards são dropados
    // na ordem inversa de criação (LIFO)
}

fn main() {
    processar_com_guards();
    println!("---");

    // Demonstrando MutexGuard (da biblioteca padrão)
    let dados = Arc::new(Mutex::new(vec![1, 2, 3]));

    {
        // lock() retorna um MutexGuard — RAII para o lock
        let mut guard = dados.lock().unwrap();
        guard.push(4);
        println!("Dados com lock: {:?}", *guard);
        // MutexGuard é dropado aqui — lock é liberado automaticamente
    }

    // Lock já foi liberado — outra thread poderia adquirir agora
    println!("Lock liberado. Dados: {:?}", dados.lock().unwrap());
}

Gerenciamento de Arquivos com RAII

use std::io::{self, Write, BufWriter};
use std::fs::File;

/// Arquivo temporário que se auto-deleta ao sair de escopo
struct ArquivoTemporario {
    caminho: String,
    arquivo: File,
}

impl ArquivoTemporario {
    fn novo(prefixo: &str) -> io::Result<Self> {
        let caminho = format!("/tmp/{}_{}.tmp", prefixo, std::process::id());
        let arquivo = File::create(&caminho)?;
        println!("[TEMP] Arquivo criado: {}", caminho);
        Ok(ArquivoTemporario { caminho, arquivo })
    }

    fn escrever(&mut self, dados: &[u8]) -> io::Result<()> {
        self.arquivo.write_all(dados)
    }

    /// Promove o arquivo temporário para permanente
    /// Consome self sem chamar Drop — o arquivo NÃO é deletado
    fn promover(self, novo_caminho: &str) -> io::Result<()> {
        let caminho_antigo = self.caminho.clone();
        // Importante: std::mem::forget previne Drop
        std::mem::forget(self);
        std::fs::rename(&caminho_antigo, novo_caminho)?;
        println!("[TEMP] Promovido: {} -> {}", caminho_antigo, novo_caminho);
        Ok(())
    }
}

impl Drop for ArquivoTemporario {
    fn drop(&mut self) {
        println!("[TEMP] Deletando arquivo temporário: {}", self.caminho);
        let _ = std::fs::remove_file(&self.caminho);
    }
}

Diagrama

    RAII: Ciclo de Vida do Recurso vinculado ao Escopo

    fn exemplo() {
    ┌─── ESCOPO DA FUNÇÃO ──────────────────────┐
    │                                            │
    │  let conn = Conexao::nova("db");           │ ← recurso adquirido
    │  │                                         │
    │  │  {                                      │
    │  │  ┌─── ESCOPO INTERNO ────────────┐      │
    │  │  │ let guard = mutex.lock();     │      │ ← lock adquirido
    │  │  │ │                             │      │
    │  │  │ │ // usar guard...            │      │
    │  │  │ │                             │      │
    │  │  │ } ◄── Drop(guard) ───────────┘      │ ← lock liberado
    │  │                                         │
    │  │  // guard não existe mais aqui          │
    │  │  // mas conn ainda existe               │
    │  │                                         │
    │  } ◄── Drop(conn) ────────────────────────┘ ← conexão fechada
    │
    │  // Nenhum recurso vazou!

    Ordem de destruição — LIFO (pilha):

    Criação:               Destruição:
    1. recurso_a           3. Drop(recurso_c) ← primeiro
    2. recurso_b           2. Drop(recurso_b)
    3. recurso_c           1. Drop(recurso_a) ← último

    Guard Pattern — Cleanup automático:

    ┌───────────────────────┐
    │     Guard              │
    │  ┌─────────────────┐  │
    │  │ recurso: &mut R  │  │   new() → adquire recurso
    │  │ ativo: bool      │  │
    │  └─────────────────┘  │   drop() → libera recurso
    │                       │            (automaticamente!)
    └───────────────────────┘

Exemplo do Mundo Real

Um guard de transação de banco de dados que faz rollback automaticamente se a transação não for comitada:

/// Simula uma conexão com banco de dados
struct BancoDeDados {
    nome: String,
    dados: std::collections::HashMap<String, String>,
}

impl BancoDeDados {
    fn novo(nome: &str) -> Self {
        println!("[BD] Banco '{}' inicializado", nome);
        BancoDeDados {
            nome: nome.to_string(),
            dados: std::collections::HashMap::new(),
        }
    }

    /// Inicia uma transação — retorna um guard RAII
    fn iniciar_transacao(&mut self) -> Transacao<'_> {
        println!("[BD] Iniciando transação em '{}'", self.nome);
        Transacao {
            banco: self,
            operacoes: Vec::new(),
            comitada: false,
        }
    }
}

/// Operação pendente na transação
#[derive(Debug, Clone)]
enum Operacao {
    Inserir { chave: String, valor: String },
    Atualizar { chave: String, valor_antigo: Option<String>, valor_novo: String },
    Deletar { chave: String, valor_antigo: Option<String> },
}

/// Guard de transação — RAII garante rollback se não comitada
struct Transacao<'a> {
    banco: &'a mut BancoDeDados,
    operacoes: Vec<Operacao>,
    comitada: bool,
}

impl<'a> Transacao<'a> {
    /// Insere um registro na transação
    fn inserir(&mut self, chave: &str, valor: &str) {
        println!("[TX] INSERT {} = '{}'", chave, valor);
        self.operacoes.push(Operacao::Inserir {
            chave: chave.to_string(),
            valor: valor.to_string(),
        });
    }

    /// Atualiza um registro na transação
    fn atualizar(&mut self, chave: &str, novo_valor: &str) {
        let valor_antigo = self.banco.dados.get(chave).cloned();
        println!("[TX] UPDATE {} = '{}' (antigo: {:?})", chave, novo_valor, valor_antigo);
        self.operacoes.push(Operacao::Atualizar {
            chave: chave.to_string(),
            valor_antigo,
            valor_novo: novo_valor.to_string(),
        });
    }

    /// Deleta um registro na transação
    fn deletar(&mut self, chave: &str) {
        let valor_antigo = self.banco.dados.get(chave).cloned();
        println!("[TX] DELETE {} (antigo: {:?})", chave, valor_antigo);
        self.operacoes.push(Operacao::Deletar {
            chave: chave.to_string(),
            valor_antigo,
        });
    }

    /// Confirma a transação — aplica todas as operações
    fn comitar(mut self) -> Result<usize, String> {
        let qtd = self.operacoes.len();
        println!("[TX] COMMIT — aplicando {} operações", qtd);

        for op in &self.operacoes {
            match op {
                Operacao::Inserir { chave, valor } => {
                    self.banco.dados.insert(chave.clone(), valor.clone());
                }
                Operacao::Atualizar { chave, valor_novo, .. } => {
                    self.banco.dados.insert(chave.clone(), valor_novo.clone());
                }
                Operacao::Deletar { chave, .. } => {
                    self.banco.dados.remove(chave);
                }
            }
        }

        self.comitada = true;
        Ok(qtd)
    }
}

/// Drop implementa ROLLBACK automático!
impl<'a> Drop for Transacao<'a> {
    fn drop(&mut self) {
        if !self.comitada && !self.operacoes.is_empty() {
            println!(
                "[TX] ROLLBACK automático! {} operações descartadas",
                self.operacoes.len()
            );
            // As operações nunca foram aplicadas ao banco,
            // então "rollback" é simplesmente não fazer nada.
            // Em um sistema real, desfaria operações aplicadas.
        }
    }
}

fn main() {
    let mut bd = BancoDeDados::novo("loja");

    // Transação bem-sucedida
    {
        let mut tx = bd.iniciar_transacao();
        tx.inserir("produto_1", "Teclado");
        tx.inserir("produto_2", "Mouse");
        tx.comitar().unwrap();
        // comitada = true, Drop não faz rollback
    }
    println!("Dados após commit: {:?}\n", bd.dados);

    // Transação que falha — ROLLBACK automático via Drop
    {
        let mut tx = bd.iniciar_transacao();
        tx.inserir("produto_3", "Monitor");
        tx.atualizar("produto_1", "Teclado Mecânico");
        tx.deletar("produto_2");

        // Simulando um erro: não chamamos comitar()
        // Quando tx sai de escopo, Drop faz ROLLBACK!
        println!("[SIMULAÇÃO] Erro detectado, saindo sem comitar...");
    }
    println!("Dados após rollback: {:?}\n", bd.dados);
    // produto_3 NÃO foi inserido, produto_1 NÃO foi atualizado

    // Transação com erro no meio
    fn transacao_com_erro(bd: &mut BancoDeDados) -> Result<(), String> {
        let mut tx = bd.iniciar_transacao();
        tx.inserir("produto_4", "Webcam");

        // Simulando erro
        if true {
            return Err("Erro de validação".to_string());
            // tx é dropada pelo unwinding — ROLLBACK automático!
        }

        tx.comitar().map(|_| ())
    }

    match transacao_com_erro(&mut bd) {
        Ok(_) => println!("Sucesso"),
        Err(e) => println!("Erro: {} (rollback feito automaticamente)", e),
    }
    println!("Dados finais: {:?}", bd.dados);
}

Quando Usar

  • Recursos que precisam de cleanup: Arquivos, conexões de rede, locks, memória mapeada
  • Transações: Auto-rollback quando a transação não é confirmada
  • Medição de tempo: Timer guards para profiling
  • Logging estruturado: Log de entrada/saída de escopos
  • Estado temporário: Configurações que devem ser restauradas ao sair do escopo
  • Qualquer aquisição/liberação pareada: Sempre que adquirir() precisa de liberar()

Quando NÃO Usar

  • Recursos com vida longa: Se o recurso precisa sobreviver ao escopo, use Arc ou Rc
  • Cleanup assíncrono: Drop é síncrono; para cleanup async, use AsyncDrop (experimental) ou channels
  • Ordem de destruição crítica: Se a ordem de destruição entre campos de uma struct importa, cuidado — Rust destroi campos na ordem de declaração
  • Efeitos colaterais indesejados: Se Drop tem efeitos colaterais (IO, rede), considere o impacto de drops em panic

Variações em Rust

Scoped Guard com closure

/// Guard genérico que executa uma closure no Drop
struct ScopeGuard<F: FnOnce()> {
    callback: Option<F>,
}

impl<F: FnOnce()> ScopeGuard<F> {
    fn novo(callback: F) -> Self {
        ScopeGuard {
            callback: Some(callback),
        }
    }

    /// Desativa o guard — a closure NÃO será executada
    fn desativar(mut self) {
        self.callback = None;
    }
}

impl<F: FnOnce()> Drop for ScopeGuard<F> {
    fn drop(&mut self) {
        if let Some(callback) = self.callback.take() {
            callback();
        }
    }
}

/// Função auxiliar para criar scope guards facilmente
fn ao_sair<F: FnOnce()>(callback: F) -> ScopeGuard<F> {
    ScopeGuard::novo(callback)
}

fn main() {
    // Similar a defer em Go
    let _guard = ao_sair(|| println!("Cleanup ao sair do escopo!"));

    println!("Fazendo trabalho...");
    println!("Mais trabalho...");
    // "Cleanup ao sair do escopo!" será impresso aqui
}

ManuallyDrop para controle fino

use std::mem::ManuallyDrop;

/// Recurso que pode ser transferido sem Drop
struct RecursoTransferivel {
    id: u64,
    dados: String,
}

impl Drop for RecursoTransferivel {
    fn drop(&mut self) {
        println!("[DROP] Recurso {} destruído", self.id);
    }
}

fn main() {
    // ManuallyDrop previne Drop automático
    let recurso = ManuallyDrop::new(RecursoTransferivel {
        id: 1,
        dados: "importante".to_string(),
    });

    println!("Recurso criado: id={}", recurso.id);
    // Drop NÃO é chamado — o recurso deve ser liberado manualmente
    // ou transferido para outro dono
}

Padrões Relacionados

  • Builder: Builders podem usar RAII para garantir cleanup de estado parcial
  • Type-State: Type-state com Drop para cleanup em transições de estado
  • Newtype: Newtype com Drop para wrappers de recursos
  • Command: Comandos de undo podem usar guards para rollback automático

Conclusão

RAII em Rust não é apenas um padrão — é o mecanismo fundamental de gerenciamento de recursos da linguagem. A trait Drop garante cleanup determinístico e automático, eliminando vazamentos de recursos, erros de double-free e use-after-free. O guard pattern estende RAII para cenários complexos como transações, medição de tempo e logging. Diferente de linguagens com garbage collector (onde finalizers são não-determinísticos) ou de C (onde cleanup manual é propenso a erros), Rust oferece o melhor dos dois mundos: controle total sobre o ciclo de vida dos recursos com segurança garantida pelo compilador.