Nesta aula, exploraremos dois traits fundamentais do Rust: Deref e Drop. O trait Deref permite que um tipo seja tratado como uma referência a outro tipo, habilitando a coerção de desreferência (deref coercion). Já o trait Drop define o que acontece quando um valor sai de escopo, sendo essencial para gerenciamento de recursos como memória heap, arquivos ou conexões de rede. Compreender esses traits é crucial para escrever código idiomático e seguro em Rust.

Veremos como implementar ambos, a ordem em que os drops ocorrem (especialmente em structs e enums) e a função std::mem::drop, que força a destruição antecipada de um valor. Ao final, você será capaz de usar Deref para criar smart pointers como Box e Rc, e implementar Drop para liberar recursos de forma segura.

trait Deref e coerção

O trait Deref é definido na biblioteca padrão como:

pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

Ele permite que um tipo seja desreferenciado usando o operador *. Quando você implementa Deref para um tipo T com Target = U, então *t (onde t: &T) se torna equivalente a *(t.deref()), que retorna &U. Isso é a base para a coerção de desreferência: o Rust automaticamente converte referências a tipos que implementam Deref em referências ao seu Target em muitos contextos, como ao passar argumentos para funções ou ao chamar métodos.

Por exemplo, Box<T> implementa Deref para T, então você pode chamar métodos de T diretamente em um Box<T> sem precisar desreferenciar explicitamente. Da mesma forma, String implementa Deref<Target = str>, permitindo que &String seja convertido em &str automaticamente.

A coerção de desreferência é especialmente útil em funções. Considere:

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = Box::new(String::from("Rust"));
    hello(&m); // &Box<String> é coercido a &str via Deref
}

Sem a coerção, precisaríamos escrever hello(&(*m)[..]). O compilador insere chamadas a deref quantas vezes forem necessárias para que o tipo corresponda ao esperado.

Para implementar Deref em seu próprio smart pointer, você deve definir o tipo alvo e a implementação de deref. Por exemplo:

use std::ops::Deref;

struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.0
    }
}

fn main() {
    let x = MyBox(5);
    assert_eq!(*x, 5); // *x desreferencia via Deref
}

Além de Deref, existe o trait DerefMut para desreferência mutável, que permite obter uma referência mutável ao alvo. A coerção de desreferência também se aplica a referências mutáveis, mas com algumas restrições de segurança.

trait Drop

O trait Drop permite que você execute código quando um valor está prestes a sair de escopo. Sua definição é simples:

pub trait Drop {
    fn drop(&mut self);
}

Você não pode chamar drop explicitamente (exceto via std::mem::drop, que veremos adiante); o compilador o invoca automaticamente no final do escopo do valor. A implementação de Drop é útil para liberar recursos como memória heap (embora o Rust já faça isso via Box), fechar arquivos, liberar locks, etc.

Exemplo clássico: um smart pointer que imprime quando é dropado.

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer { data: String::from("my stuff") };
    let d = CustomSmartPointer { data: String::from("other stuff") };
    println!("CustomSmartPointers created.");
} // c e d são dropados aqui, em ordem reversa de criação

A saída será:

CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

Note que os drops ocorrem na ordem inversa da declaração: d antes de c. Isso é importante para gerenciamento de dependências.

Ao implementar Drop, você não pode implementar Copy para o mesmo tipo, pois isso criaria duplicatas e o drop seria chamado múltiplas vezes. O Rust impede essa combinação.

Ordem de drop

A ordem em que os drops acontecem é especificada pela linguagem e segue regras claras:

  • Variáveis locais são dropadas na ordem inversa de sua declaração (último a ser criado, primeiro a ser dropado).
  • Campos de uma struct são dropados na ordem em que foram declarados (primeiro campo declarado, primeiro a ser dropado).
  • Em tuplas e enums, os campos são dropados na ordem de definição.
  • Arrays e slices são dropados elemento por elemento, do índice 0 ao último.
  • O drop de um valor composto (struct, enum) ocorre após o drop de todos os seus campos/elementos.

Essa ordem é garantida e você pode confiar nela para gerenciar recursos que dependem uns dos outros. Por exemplo, se um campo de uma struct possui uma referência a outro campo, o campo referenciado deve ser dropado depois do campo que o referencia. Como os campos são dropados na ordem de declaração, você deve declarar o campo referenciado primeiro.

struct SelfReferential {
    data: String,
    // Se houvesse um campo que referenciasse data, data deveria ser declarado primeiro
}

impl Drop for SelfReferential {
    fn drop(&mut self) {
        println!("Dropping SelfReferential");
    }
}

fn main() {
    let a = SelfReferential { data: String::from("a") };
    let b = SelfReferential { data: String::from("b") };
    // a é dropado depois de b (ordem inversa de declaração)
}

Entender a ordem de drop é crucial para evitar use-after-free e outros bugs sutis.

std::mem::drop

A função std::mem::drop é uma forma de forçar a destruição de um valor antes do final de seu escopo. Ela simplesmente move o valor para dentro da função, que então o dropa ao sair. Sua assinatura:

pub fn drop<T>(_x: T) { }

Ela é útil para liberar recursos mais cedo, especialmente quando você precisa que um recurso seja liberado antes de outro que depende dele. Por exemplo, ao trabalhar com locks:

use std::sync::Mutex;

struct Resource {
    data: Mutex<i32>,
}

impl Drop for Resource {
    fn drop(&mut self) {
        println!("Resource dropped");
    }
}

fn main() {
    let resource = Resource { data: Mutex::new(42) };
    // ... usa o recurso
    std::mem::drop(resource); // libera resource antes do final do escopo
    println!("Resource has been dropped, but we can still do other things");
}

Sem o drop explícito, o recurso só seria liberado ao final do escopo. Note que você não pode usar o valor após chamar drop, pois ele foi movido. O compilador impede o uso.

É importante não confundir std::mem::drop com a implementação do trait Drop. O trait Drop define o que acontece quando um valor é dropado; std::mem::drop é apenas uma função que invoca o drop de um valor. Você pode chamar drop em qualquer valor, mesmo que ele não implemente Drop explicitamente (o drop padrão apenas desaloca a memória).

Boas práticas

  • Ao implementar Deref, lembre-se de que a coerção de desreferência pode tornar seu código mais limpo, mas use com moderação para não esconder transformações de tipo.
  • Implemente Drop apenas quando seu tipo gerencia um recurso que precisa ser liberado explicitamente (como um ponteiro para memória alocada fora do heap do Rust).
  • Não implemente Drop em tipos que implementam Copy; isso é proibido pelo compilador.
  • Confie na ordem de drop garantida pelo Rust, mas evite criar dependências complexas entre campos que possam levar a use-after-free.
  • Use std::mem::drop para liberar recursos mais cedo, mas não abuse: a gestão automática de escopo geralmente é suficiente.

Referências

Exercícios

  1. Crie um struct Noisy que implementa Drop e imprime "Drop!" quando é dropado. Em seguida, crie uma função que recebe um Noisy por valor e chame essa função. Verifique a saída.

    ✓ Resposta:
    struct Noisy;
    
    impl Drop for Noisy {
        fn drop(&mut self) {
            println!("Drop!");
        }
    }
    
    fn consume(_n: Noisy) {}
    
    fn main() {
        let n = Noisy;
        consume(n); // imprime "Drop!" quando n é dropado dentro de consume
        // ou se não mover, imprime ao final do main
    }
  2. Implemente um smart pointer MyBox<T> que implementa Deref para T e Drop que imprime "MyBox dropped". Teste com um tipo i32 e chame um método de i32 através do MyBox.

    ✓ Resposta:
    use std::ops::Deref;
    
    struct MyBox<T>(T);
    
    impl<T> Deref for MyBox<T> {
        type Target = T;
        fn deref(&self) -> &T {
            &self.0
        }
    }
    
    impl<T> Drop for MyBox<T> {
        fn drop(&mut self) {
            println!("MyBox dropped");
        }
    }
    
    fn main() {
        let x = MyBox(10);
        // i32 tem método abs()
        println!("{}", x.abs()); // coerção: &MyBox<i32> -> &i32
    }
  3. O que será impresso pelo seguinte código? Explique a ordem de drop.

    struct A;
    struct B;
    impl Drop for A { fn drop(&mut self) { println!("A"); } }
    impl Drop for B { fn drop(&mut self) { println!("B"); } }
    struct C(A, B);
    impl Drop for C { fn drop(&mut self) { println!("C"); } }
    fn main() {
        let _c = C(A, B);
    }

    ✓ Resposta:

    Imprime: C, A, B. Primeiro o drop de C (struct), depois os campos na ordem de declaração: A primeiro, depois B.

  4. Use std::mem::drop para liberar um recurso antes do final do escopo. Crie um struct Resource que imprime "Resource dropped" ao ser dropado, e force seu drop antes de imprimir "Fim".

    ✓ Resposta:
    struct Resource;
    impl Drop for Resource {
        fn drop(&mut self) {
            println!("Resource dropped");
        }
    }
    fn main() {
        let r = Resource;
        std::mem::drop(r);
        println!("Fim");
    }
    // Saída:
    // Resource dropped
    // Fim
  5. Explique por que o código abaixo não compila. Modifique-o para que compile sem remover o trait Drop.

    #[derive(Clone, Copy)]
    struct NoCopy;
    impl Drop for NoCopy {
        fn drop(&mut self) {}
    }
    fn main() {}

    ✓ Resposta:

    O erro é que um tipo com Drop não pode implementar Copy. Para compilar, remova o derive de Copy (e Clone se não for necessário).