Ownership: o conceito central
Esta aula explica o conceito central de ownership em Rust, suas regras fundamentais, a diferença entre stack e heap, e os mecanismos de move e drop. Você entenderá por que ownership é essencial para segurança de memória sem garbage collector.
Ownership é o sistema de gerenciamento de memória do Rust que elimina a necessidade de um garbage collector. Ele garante segurança de memória em tempo de compilação através de um conjunto de regras que controlam como os valores são criados, movidos e destruídos. Nesta aula, vamos explorar as regras de ownership, a diferença entre stack e heap, os mecanismos de move e drop, e por que isso é tão importante para programação segura e eficiente.
Entender ownership é fundamental para dominar Rust, pois afeta como você escreve código, gerencia recursos e evita erros comuns como uso após liberação e vazamentos de memória. Vamos começar com as regras básicas.
Regras de ownership
O sistema de ownership em Rust segue três regras simples, mas poderosas:
- Cada valor em Rust tem uma variável que é sua dona (owner).
- Só pode haver um dono por vez.
- Quando o dono sai de escopo, o valor é descartado (drop).
Vamos ver um exemplo prático. Quando você declara uma variável, ela se torna a dona do valor. Se você atribuir essa variável a outra, a ownership é transferida (move), e a variável original não pode mais ser usada. Isso evita duplicação acidental de dados e garante que a memória seja liberada corretamente.
let s1 = String::from("hello");
let s2 = s1; // move: s1 não é mais válida
// println!("{}", s1); // erro: s1 foi movida
println!("{}", s2); // ok
Outro aspecto importante é que tipos simples como inteiros implementam a trait Copy, então eles são copiados em vez de movidos. Isso ocorre porque esses tipos são armazenados na stack e são baratos de copiar.
let x = 5;
let y = x; // cópia, x ainda é válido
println!("x = {}, y = {}", x, y);
Stack vs heap
Para entender ownership, é crucial saber a diferença entre stack e heap. A stack é uma estrutura de dados LIFO (last in, first out) que armazena valores de tamanho fixo conhecido em tempo de compilação. O heap, por outro lado, armazena dados de tamanho dinâmico, alocados em tempo de execução.
Em Rust, tipos como String e Vec são alocados no heap. A variável na stack contém um ponteiro para o heap, além do comprimento e capacidade. Quando você move um valor heap, o Rust copia o ponteiro (e metadados) para a nova variável, mas não copia os dados do heap. Isso é eficiente, mas significa que apenas um dono existe para evitar dupla liberação.
let s1 = String::from("hello");
let s2 = s1; // move: s1 é inválido
// s1 não pode mais ser usado
Move e drop
O move é a transferência de ownership de um valor. Quando você atribui um valor a outra variável, passa como argumento para uma função ou retorna de uma função, a ownership pode ser movida. Após o move, a variável original não pode mais ser usada, evitando referências pendentes.
fn take_ownership(s: String) {
println!("{}", s);
} // s é dropado aqui
let s = String::from("world");
take_ownership(s); // s é movido para a função
// println!("{}", s); // erro: s foi movido
O drop é a liberação da memória quando o dono sai de escopo. Rust chama automaticamente a função drop da trait Drop para limpar recursos. Você pode implementar Drop para tipos personalizados, mas geralmente o compilador cuida disso para tipos padrão.
{
let s = String::from("temporary");
// s é usado aqui
} // s sai de escopo e é dropado
Por que isso importa
Ownership resolve problemas clássicos de gerenciamento de memória sem a necessidade de um garbage collector. Ele previne:
- Uso após liberação: como o dono único garante que o valor não seja acessado após ser dropado.
- Dupla liberação: porque apenas um dono pode liberar a memória.
- Vazamentos de memória: o drop automático no fim do escopo garante que a memória seja liberada.
Além disso, ownership permite que Rust seja tão rápido quanto C/C++ em muitas tarefas, pois não há overhead de garbage collector. Para o programador, isso significa mais controle sobre recursos e previsibilidade de desempenho. Combinado com o sistema de empréstimos (borrowing) e lifetimes, ownership torna o código seguro por construção.
Boas práticas
- Prefira tipos que implementam
Copypara valores pequenos e imutáveis. - Use referências (
&T) para emprestar dados sem transferir ownership. - Evite clonar desnecessariamente; clone apenas quando precisar de uma cópia independente.
Referências
- The Rust Book: What is Ownership?
- The Rust Book: References and Borrowing
- Rust std::mem::drop
- Rust by Example: Ownership and Moves
- Rust Design Patterns: Ownership
Exercícios
Explique o que acontece com a variável
s1apóslet s2 = s1;ses1é umaString. O que ocorreria ses1fosse umi32?✓ Resposta: Ses1éString, a ownership é movida paras2es1se torna inválida, não podendo mais ser usada. Ses1éi32, como implementaCopy, o valor é copiado e ambas as variáveis permanecem válidas.O que o código a seguir imprime? Justifique.
let x = 10; let y = x; println!("{}", x);✓ Resposta: Imprime10.i32implementaCopy, entãoy = xfaz uma cópia, expermanece válido.Considere a função
fn foo(s: String) { }. O que acontece com aStringpassada? Como evitar a perda de ownership?✓ Resposta: AStringé movida para a função, e após a execução, é dropada. Para evitar a perda, pode-se retornar aStringda função ou passar uma referência&String.Escreva um código que demonstre a transferência de ownership ao retornar um valor de uma função.
✓ Resposta:fn give_ownership() -> String { let s = String::from("hello"); s // ownership é movida para quem chamou } let my_string = give_ownership(); println!("{}", my_string);Explique a diferença entre mover e clonar um valor heap. Dê um exemplo onde clonar é necessário.
✓ Resposta: Mover transfere ownership sem copiar os dados do heap; clonar cria uma cópia profunda, duplicando os dados. Clonar é necessário quando se quer manter a variável original e ter uma cópia independente. Exemplo:let s1 = String::from("hello"); let s2 = s1.clone();— ambas válidas.