Nesta aula, exploraremos duas traits essenciais do Rust: Deref e Drop. A trait Deref permite que tipos implementem o comportamento do operador de desreferência (*), possibilitando que smart pointers e outros wrappers se comportem como referências. Já a trait Drop é executada automaticamente quando um valor sai de escopo, permitindo liberar recursos como memória, arquivos ou conexões de rede. Compreender essas traits é crucial para escrever código seguro e idiomático em Rust.

Além disso, veremos como a coerção de Deref simplifica o uso de referências e smart pointers, e como a ordem de execução de Drop pode afetar o design de estruturas aninhadas. Por fim, apresentaremos a função std::mem::drop, que força a liberação antecipada de um valor.

trait Deref e coerção

A trait Deref é definida na biblioteca padrão e permite que um tipo seja tratado como uma referência ao seu alvo. Sua assinatura é:

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

Quando você implementa Deref para um tipo, o operador * pode ser aplicado a ele, retornando uma referência ao alvo. Por exemplo, Box<T> implementa Deref para T, então *box equivale a *(box.deref()).

A coerção de Deref (deref coercion) é um mecanismo que converte automaticamente referências a um tipo que implementa Deref em referências ao seu Target. Isso ocorre em contextos como passagem de argumentos para funções, atribuições e chamadas de métodos. Por exemplo, uma função que espera &str pode receber &String porque String implementa Deref<Target=str>. Isso torna o código mais flexível e evita conversões explícitas.

Exemplo prático:

use std::ops::Deref;

struct MeuBox<T>(T);

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

fn main() {
    let x = MeuBox(42);
    println!("{}", *x); // 42, graças a Deref
    // coerção: &MeuBox<i32> vira &i32
    fn pegar_ref(y: &i32) { println!("{}", y); }
    pegar_ref(&x); // funciona
}

trait Drop

A trait Drop permite personalizar o que acontece quando um valor sai de escopo. Ela possui apenas um método:

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

O método drop é chamado automaticamente pelo compilador quando a variável que possui o valor deixa de ser válida (por exemplo, ao final de um bloco). É usado para liberar recursos como memória alocada no heap, descritores de arquivo ou conexões de rede. Importante: você não deve chamar drop diretamente (a menos que use std::mem::drop), pois isso causaria um double free.

Exemplo:

struct Custom {
    data: String,
}

impl Drop for Custom {
    fn drop(&mut self) {
        println!("Liberando recurso: {}", self.data);
    }
}

fn main() {
    let c = Custom { data: String::from("arquivo.txt") };
    println!("Dentro do escopo");
} // ao sair, drop é chamado

Ordem de drop

Em Rust, a ordem em que os drops ocorrem é determinada pela ordem de declaração das variáveis (primeiro a ser declarado, último a ser droppado) e, para structs, pela ordem dos campos (primeiro campo é droppado primeiro). Isso é importante quando há dependências entre recursos: por exemplo, se um campo guarda uma referência a outro campo, o campo referenciado deve ser droppado depois.

Considere:

struct Par {
    a: String,
    b: String,
}

impl Drop for Par {
    fn drop(&mut self) {
        println!("Droppando Par: a={}, b={}", self.a, self.b);
    }
}

fn main() {
    let p = Par { a: "primeiro".into(), b: "segundo".into() };
    // ordem de drop: p.a, p.b, depois p (implícito)
    // mas o drop de p é chamado após os campos
    // Na verdade, o drop da struct é chamado antes dos campos?
    // Regra: o drop da struct é executado, depois os campos são droppados na ordem de declaração.
    // Então: drop de p (imprime), depois drop de p.a, depois drop de p.b
}

Para evitar confusão, a documentação oficial recomenda que a implementação de Drop não dependa da ordem dos campos. Se precisar de ordem específica, considere usar std::mem::drop manualmente ou reestruturar o código.

std::mem::drop

A função std::mem::drop é uma forma de forçar a liberação de um valor antes do final do escopo. Ela simplesmente move o valor para dentro da função, que o descarta imediatamente, chamando seu drop. É útil para liberar recursos cedo, como fechar um arquivo antes do final da função.

Exemplo:

use std::mem::drop;

struct Recurso {
    id: u32,
}

impl Drop for Recurso {
    fn drop(&mut self) {
        println!("Recurso {} liberado", self.id);
    }
}

fn main() {
    let r = Recurso { id: 1 };
    drop(r); // libera aqui
    println!("Após drop manual");
    // r não pode mais ser usado
}

Note que após drop(r), a variável r é movida e não pode mais ser acessada. Isso evita uso após liberação.

Referências

Exercícios

  1. Implemente a trait Deref para uma struct Wrapper que contém um i32. Faça com que *wrapper retorne o valor interno.

    ✓ Resposta:
    use std::ops::Deref;
    
    struct Wrapper(i32);
    
    impl Deref for Wrapper {
        type Target = i32;
        fn deref(&self) -> &i32 {
            &self.0
        }
    }
    
    fn main() {
        let w = Wrapper(10);
        assert_eq!(*w, 10);
    }
  2. Crie uma struct Arquivo que implemente Drop e imprima "Arquivo fechado" ao ser droppada. Instancie a struct e verifique a mensagem no final do escopo.

    ✓ Resposta:
    struct Arquivo {
        nome: String,
    }
    
    impl Drop for Arquivo {
        fn drop(&mut self) {
            println!("Arquivo fechado: {}", self.nome);
        }
    }
    
    fn main() {
        let _f = Arquivo { nome: String::from("dados.txt") };
        println!("Dentro do escopo");
    } // Ao sair, "Arquivo fechado: dados.txt" é impresso
  3. Explique por que não se deve chamar value.drop() diretamente. O que aconteceria?

    ✓ Resposta:Chamar drop diretamente não é permitido pelo compilador, pois o método drop da trait Drop é chamado automaticamente. Se você tentar chamar value.drop(), o compilador emitirá um erro. Para liberar manualmente, use std::mem::drop(value), que move o valor e o descarta. Chamar o método diretamente causaria um conflito com o drop automático, resultando em double free (se fosse possível).
  4. Escreva um programa que demonstre a ordem de drop de uma struct com dois campos: a: String e b: String. Implemente Drop para a struct e para cada campo (crie tipos novos para os campos).

    ✓ Resposta:
    struct CampoA(String);
    struct CampoB(String);
    
    impl Drop for CampoA {
        fn drop(&mut self) {
            println!("Droppando CampoA: {}", self.0);
        }
    }
    
    impl Drop for CampoB {
        fn drop(&mut self) {
            println!("Droppando CampoB: {}", self.0);
        }
    }
    
    struct MinhaStruct {
        a: CampoA,
        b: CampoB,
    }
    
    impl Drop for MinhaStruct {
        fn drop(&mut self) {
            println!("Droppando MinhaStruct");
        }
    }
    
    fn main() {
        let s = MinhaStruct {
            a: CampoA("primeiro".into()),
            b: CampoB("segundo".into()),
        };
        println!("Fim do escopo");
    } // Ordem: drop de MinhaStruct, depois CampoA, depois CampoB
  5. Use std::mem::drop para liberar um recurso antes do final de um bloco e mostre que a variável não pode mais ser usada.

    ✓ Resposta:
    use std::mem::drop;
    
    struct Recurso {
        nome: String,
    }
    
    impl Drop for Recurso {
        fn drop(&mut self) {
            println!("Liberando {}", self.nome);
        }
    }
    
    fn main() {
        let r = Recurso { nome: String::from("conexão") };
        drop(r);
        // println!("{}", r.nome); // Erro! r foi movido
        println!("Recurso já liberado");
    }