Ownership: o conceito central
Esta aula apresenta o conceito central de ownership do Rust, explicando suas regras, a diferença entre stack e heap, os mecanismos de move e drop, e por que esse sistema é fundamental para a segurança de memória sem garbage collector.
Ownership é o conceito mais distintivo e fundamental do Rust. Ele permite que o Rust garanta segurança de memória sem a necessidade de um garbage collector, através de um conjunto de regras que o compilador verifica em tempo de compilação. Nesta aula, vamos explorar as regras de ownership, como a memória é gerenciada entre stack e heap, os mecanismos de move e drop, e entender por que tudo isso é importante para escrever código eficiente e seguro.
Dominar ownership é essencial para programar em Rust de forma eficaz, pois influencia diretamente como você estrutura seus dados e funções. Vamos começar com as regras básicas.
Regras de ownership
Em Rust, cada valor tem um único dono (owner) a cada momento. Quando o dono sai de escopo, o valor é descartado. Existem três regras principais:
- Cada valor em Rust tem uma variável que é seu dono.
- Só pode haver um dono por vez.
- Quando o dono sai de escopo, o valor é liberado.
Essas regras são verificadas em tempo de compilação, prevenindo erros como uso após liberação, dupla liberação e ponteiros soltos. Vejamos um exemplo:
fn main() {
let s1 = String::from("hello"); // s1 é dono da String
let s2 = s1; // ownership é movido para s2
// println!("{}", s1); // erro! s1 não é mais válido
println!("{}", s2); // ok
} // s2 sai de escopo, a String é liberadaNote que quando atribuímos s1 a s2, o ownership é movido. Se tentarmos usar s1 depois, o compilador impede, evitando acesso a memória inválida.
Stack vs heap
Em Rust, os dados podem ser armazenados na stack ou no heap. A stack é rápida e organizada (LIFO), usada para dados de tamanho fixo conhecido em tempo de compilação, como inteiros e booleanos. O heap é mais flexível para dados de tamanho dinâmico, como String e Vec, mas requer alocação e liberação explícitas, que o Rust gerencia via ownership.
Quando você cria uma String, o conteúdo (os caracteres) fica no heap, enquanto um ponteiro, tamanho e capacidade ficam na stack. O ownership garante que quando a variável dona sai de escopo, o Rust chama drop automaticamente para liberar a memória do heap. Exemplo:
fn main() {
let s = String::from("hello"); // s é dono da String
// s está na stack, apontando para o heap
println!("{}", s);
} // s sai de escopo, drop é chamado, memória do heap liberadaSem ownership, seria fácil esquecer de liberar o heap (vazamento) ou liberar duas vezes. O Rust evita isso com as regras de ownership.
Move e drop
O movimento (move) ocorre quando um valor é atribuído a outra variável ou passado para uma função, transferindo o ownership. O drop é a liberação automática da memória quando o dono sai de escopo. Vamos ver exemplos:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 é movido para s2
// s1 não é mais válido
let s3 = s2.clone(); // clone copia dados do heap, s2 continua válido
println!("s2 = {}, s3 = {}", s2, s3);
} // s2 e s3 saem de escopo, ambos liberadosPassar um valor para uma função também move:
fn take_ownership(s: String) {
println!("{}", s);
} // s é dropado aqui
fn main() {
let s = String::from("hello");
take_ownership(s); // s é movido para a função
// println!("{}", s); // erro! s não é mais válido
}Para evitar o move, podemos usar referências (empréstimo), que serão abordadas em aulas futuras. O drop é implementado via trait Drop, que você pode customizar.
Por que isso importa
O sistema de ownership é o que torna Rust único: ele elimina toda uma classe de bugs de memória (uso após liberação, dupla liberação, vazamentos) sem sacrificar performance. Em linguagens como C++, você precisa gerenciar manualmente a memória; em Java, há garbage collector com pausas. Rust oferece o melhor dos dois mundos: segurança e desempenho, com custo zero de abstração.
Além disso, ownership permite que o Rust tenha concorrência sem data races, como veremos em aulas futuras. Entender ownership é o primeiro passo para dominar Rust e escrever código confiável e eficiente.
Boas práticas
- Prefira usar referências (&T) quando não precisar de ownership, para evitar moves desnecessários.
- Use
clone()apenas quando realmente precisar de uma cópia independente, pois é custoso. - Lembre-se que tipos como
i32implementamCopy, então eles são copiados em vez de movidos.
Referências
- The Rust Programming Language - Ownership
- Rust by Example - Ownership and Moves
- Rust std::ops::Drop
- References and Borrowing
- The Rustonomicon - Ownership
Exercícios
O que acontece se você tentar usar uma variável após mover seu valor para outra variável? Dê um exemplo.
✓ Resposta: O compilador gera um erro de compilação, pois a variável original não é mais válida. Exemplo:let s1 = String::from("hello"); let s2 = s1; println!("{}", s1); // erro: use of moved value: `s1`Explique a diferença entre stack e heap em Rust. Dê exemplos de tipos que vão para cada um.
✓ Resposta: A stack armazena dados de tamanho fixo conhecido em tempo de compilação, como i32, bool, tuplas de tipos fixos. O heap armazena dados de tamanho dinâmico, como String e Vec. A stack é rápida e organizada, enquanto o heap requer alocação dinâmica e é gerenciado pelo ownership.O que é o trait
Drope quando ele é chamado?✓ Resposta: O traitDroppermite customizar o que acontece quando um valor sai de escopo. É chamado automaticamente pelo Rust no final do escopo do dono. Exemplo:struct Custom; impl Drop for Custom { fn drop(&mut self) { println!("Custom dropped"); } } fn main() { let c = Custom; } // imprime "Custom dropped"Considere o código:
let x = 5; let y = x; println!("{}", x);. Por que isso funciona, ao contrário de String?✓ Resposta: Porquei32implementa o traitCopy, que faz com que a atribuição copie o valor em vez de movê-lo. Portanto,xpermanece válido. Tipos comoStringnão implementamCopypor serem alocados no heap.Escreva uma função que receba uma
Stringe retorne o seu comprimento, sem consumir a String (ou seja, permitindo que a String continue sendo usada após a chamada). Dica: use referência.✓ Resposta: Use uma referência:fn len(s: &String) -> usize { s.len() } fn main() { let s = String::from("hello"); let l = len(&s); println!("{} length {}", s, l); // s continua válido }