Deref e Drop
Esta aula aborda os traits Deref e Drop em Rust, explicando como o primeiro possibilita a coerção de desreferência e o segundo permite a execução de código na destruição de um valor. Também são discutidas a ordem de execução dos drops e a função std::mem::drop, com exemplos práticos e exercícios.
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
Dropapenas 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
Dropem tipos que implementamCopy; 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::droppara liberar recursos mais cedo, mas não abuse: a gestão automática de escopo geralmente é suficiente.
Referências
- Documentação oficial do trait Deref
- Documentação oficial do trait DerefMut
- Documentação oficial do trait Drop
- Documentação oficial de std::mem::drop
- Capítulo 15.2 do Livro de Rust: Tratando Smart Pointers como Referências com Deref
- Capítulo 15.3 do Livro de Rust: Executando Código na Limpeza com o Trait Drop
- Referência de Rust: Destrutores (ordem de drop)
Exercícios
-
Crie um struct
Noisyque implementaDrope imprime "Drop!" quando é dropado. Em seguida, crie uma função que recebe umNoisypor 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 } -
Implemente um smart pointer
MyBox<T>que implementaDerefparaTeDropque imprime "MyBox dropped". Teste com um tipoi32e chame um método dei32através doMyBox.✓ 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 } -
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.
-
Use
std::mem::droppara liberar um recurso antes do final do escopo. Crie um structResourceque 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 -
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
Dropnão pode implementarCopy. Para compilar, remova o derive deCopy(eClonese não for necessário).