Regras de ownership e move semantics
1. Introdução ao Ownership em Rust
O sistema de ownership é o recurso mais distintivo do Rust, responsável por garantir segurança de memória sem a necessidade de um garbage collector. Em vez de confiar em coleta automática como Java/Go ou gerenciamento manual como C/C++, Rust utiliza um conjunto de regras verificadas em tempo de compilação que determinam como a memória é gerenciada.
As três regras fundamentais de ownership são:
- Cada valor em Rust tem exatamente um dono (owner)
- Quando o dono sai de escopo, o valor é automaticamente liberado
- Pode haver múltiplas referências imutáveis ou uma única referência mutável
Enquanto em C++ você precisa manualmente chamar delete e em Java o garbage collector decide quando liberar memória, Rust aplica essas regras estaticamente, eliminando categorias inteiras de bugs como use-after-free e double-free.
2. Regras de Ownership
A regra mais básica do ownership é que cada valor tem um único dono, e quando esse dono sai de escopo, o Rust automaticamente chama drop para liberar a memória.
{
let s = String::from("hello"); // s é o dono da String
// usa s aqui
} // s sai de escopo, drop é chamado, memória liberada
Escopos aninhados demonstram claramente esse comportamento:
let x = 10; // x é dono do inteiro
{
let y = 20; // y é dono do inteiro
println!("x: {}, y: {}", x, y);
} // y sai de escopo, liberado
println!("x: {}", x); // x ainda existe
// println!("y: {}", y); // erro! y não existe mais
O Rust garante que cada recurso seja liberado exatamente uma vez, no momento correto, sem custo de runtime.
3. Move Semantics: Transferência de Ownership
Move é a transferência da propriedade de um valor de uma variável para outra. Após um move, a variável original não pode mais ser usada.
let s1 = String::from("hello");
let s2 = s1; // s1 é movido para s2
// println!("{}", s1); // ERRO! s1 não é mais válido
println!("{}", s2); // OK: s2 é o novo dono
Isso acontece porque String é um tipo alocado no heap. Quando s1 é movido para s2, o Rust copia o ponteiro, o tamanho e a capacidade, mas invalida s1 para evitar double-free.
Com tipos primitivos armazenados na stack, o comportamento é diferente:
let a = 5;
let b = a; // cópia, não move (inteiros implementam Copy)
println!("a: {}, b: {}", a, b); // ambos válidos
O erro "borrow after move" é um dos mais comuns para iniciantes em Rust:
fn main() {
let v = vec![1, 2, 3];
let v2 = v;
println!("{:?}", v); // ERRO: valor movido para v2
}
4. Copy Semantics: Tipos com Cópia Implícita
Tipos que implementam o trait Copy são copiados implicitamente em vez de movidos. Isso é possível porque esses tipos são armazenados inteiramente na stack e sua cópia é barata.
let inteiro: i32 = 42;
let copia = inteiro; // cópia implícita
println!("{} {}", inteiro, copia); // ambos funcionam
let booleano: bool = true;
let copia_bool = booleano; // cópia
println!("{} {}", booleano, copia_bool);
let caractere: char = 'R';
let copia_char = caractere; // cópia
println!("{} {}", caractere, copia_char);
Tuplas com elementos Copy também implementam Copy:
let tupla = (1, 2.5, true);
let copia_tupla = tupla; // cópia, porque i32, f64 e bool são Copy
println!("{:?} {:?}", tupla, copia_tupla);
String e Vec NÃO implementam Copy porque contêm ponteiros para heap. Copiá-los implicitamente seria ineficiente e arriscado.
5. Clone: Cópia Explícita e Profunda
O trait Clone permite criar cópias profundas de tipos heap, mas de forma explícita, deixando claro que há custo computacional.
let s1 = String::from("Rust é seguro");
let s2 = s1.clone(); // cópia profunda explícita
println!("s1: {}", s1); // s1 ainda é válido
println!("s2: {}", s2); // s2 é uma cópia independente
let v1 = vec![1, 2, 3];
let v2 = v1.clone(); // clonando um Vec
println!("v1: {:?}", v1);
println!("v2: {:?}", v2);
A diferença entre clone() e move é crucial: clone preserva o original, mas tem custo O(n) para copiar dados do heap. Evite clone em loops ou dados grandes — prefira referências.
// Evite clone desnecessário
let grande = String::from("dados muito grandes...");
let copia = grande.clone(); // caro!
// Prefira referência
fn processar(s: &String) {
println!("{}", s);
}
6. Funções e Transferência de Ownership
Passar um valor para uma função transfere ownership para o parâmetro da função.
fn tomar_posse(s: String) {
println!("Recebi: {}", s);
} // s sai de escopo, drop é chamado
fn main() {
let nome = String::from("Alice");
tomar_posse(nome); // nome é movido para a função
// println!("{}", nome); // ERRO: nome não é mais válido
}
Para recuperar o valor, podemos retorná-lo:
fn modificar_e_retornar(mut s: String) -> String {
s.push_str(" mundo");
s // retorna ownership para o chamador
}
fn main() {
let s1 = String::from("Olá");
let s2 = modificar_e_retornar(s1); // s1 movido, s2 recebe ownership
println!("{}", s2); // "Olá mundo"
}
Esse padrão de take and give back funciona, mas é verboso. Felizmente, Rust oferece referências como alternativa.
7. Referências como Alternativa ao Move
Borrowing permite acessar um valor sem transferir ownership, usando referências (&T).
fn calcular_tamanho(s: &String) -> usize {
s.len() // acessa s, mas não toma posse
} // s não é dropado aqui, apenas a referência sai de escopo
fn main() {
let texto = String::from("Rust");
let tam = calcular_tamanho(&texto); // empresta texto
println!("Tamanho de '{}': {}", texto, tam); // texto ainda é válido
}
Referências resolvem o problema de take and give back:
fn processar_dados(dados: &Vec<i32>) {
for item in dados {
println!("{}", item);
}
}
fn main() {
let meu_vec = vec![10, 20, 30];
processar_dados(&meu_vec); // só empresta
println!("Vec ainda disponível: {:?}", meu_vec);
}
O trade-off é claro: move transfere ownership permanentemente, enquanto borrowing oferece acesso temporário. Use move quando a função precisa ser dona do valor (ex: inserir em uma struct) e referências quando só precisa ler ou modificar temporariamente.
// Move: função toma posse
fn consumir(v: Vec<i32>) {
// faz algo e descarta
}
// Borrow: função só lê
fn inspecionar(v: &Vec<i32>) {
println!("{:?}", v);
}
O sistema de ownership do Rust, combinado com move semantics, Copy, Clone e borrowing, forma um ecossistema coeso que garante segurança de memória sem sacrificar performance. Dominar esses conceitos é essencial para escrever código Rust idiomático e eficiente.
Referências
- The Rust Programming Language - Ownership — Capítulo oficial do livro sobre ownership, com exemplos detalhados das regras fundamentais.
- Rust By Example - Ownership and Move — Tutoriais práticos com exemplos interativos de move semantics e escopos.
- Rust Reference - Ownership — Especificação técnica detalhada do sistema de ownership na referência da linguagem.
- Understanding Rust: Ownership, Move, Borrowing — Artigo técnico explicando os conceitos com exemplos do mundo real e diagramas.
- Rust Ownership Explained — Guia introdutório com foco em exemplos práticos e comparações com outras linguagens.
- The Rustonomicon - Ownership — Aprofundamento avançado sobre ownership, incluindo casos extremos e unsafe Rust.