Se existe um conceito que define o Rust e o diferencia de todas as outras linguagens, é o sistema de ownership (propriedade). É graças a esse sistema que o Rust consegue garantir segurança de memória sem precisar de um garbage collector. Neste tutorial, vamos desmistificar esse conceito fundamental com muitos exemplos e diagramas.
Por que Ownership Existe?
Em linguagens como C/C++, o programador gerencia a memória manualmente, o que leva a bugs como use-after-free, double-free e memory leaks. Em linguagens como Java/Python/Go, um garbage collector faz esse trabalho, mas com custo de performance.
Rust encontrou uma terceira via: o sistema de ownership verifica regras de memória em tempo de compilação. Se o código viola essas regras, ele simplesmente não compila. Isso significa:
- Zero custo em runtime (sem garbage collector)
- Impossível ter use-after-free, double-free ou data races
- Erros de memória são pegos pelo compilador, não em produção
As Três Regras do Ownership
Memorize estas três regras — todo o sistema se baseia nelas:
- Cada valor em Rust tem uma variável que é sua “dona” (owner)
- Só pode haver um owner por vez
- Quando o owner sai do escopo, o valor é descartado (dropped)
Vamos ver cada regra na prática.
Regra 1: Todo valor tem um owner
fn main() {
let nome = String::from("Rust Brasil"); // 'nome' é o owner de "Rust Brasil"
println!("{}", nome);
} // 'nome' sai do escopo aqui, a String é descartada
Diagrama da memória:
Stack Heap
+--------+ +---+---+---+---+---+---+---+---+---+---+---+
| nome | -------> | R | u | s | t | | B | r | a | s | i | l |
| ptr | +---+---+---+---+---+---+---+---+---+---+---+
| len: 11|
| cap: 11|
+--------+
A variável nome na stack contém um ponteiro para os dados na heap, o tamanho e a capacidade. Quando nome sai do escopo, tanto a stack quanto a heap são liberadas.
Regra 2: Apenas um owner por vez (Move Semantics)
Quando você atribui uma String a outra variável, o ownership é movido (moved):
fn main() {
let s1 = String::from("Olá");
let s2 = s1; // s1 é MOVIDO para s2
// println!("{}", s1); // ERRO! s1 não é mais válido!
println!("{}", s2); // OK! s2 é o novo owner
}
Diagrama do que acontece:
ANTES do move: DEPOIS do move:
Stack Heap Stack Heap
+------+ +---+---+---+ +------+ +---+---+---+
| s1 |---->| O | l | á | | s1 | (inválido) |
+------+ +---+---+---+ +------+ +---+---+---+
| s2 |---->| O | l | á |
+------+ +---+---+---+
Isso é radicalmente diferente de outras linguagens! Em Python, s2 = s1 criaria duas referências para o mesmo objeto. Em C++, faria uma cópia (deep copy). Em Rust, transfere a propriedade.
Por que? Porque se s1 e s2 apontassem para o mesmo dado na heap, quando ambos saíssem do escopo, o Rust tentaria liberar a mesma memória duas vezes (double-free). O move previne isso.
Regra 3: Drop automático ao sair do escopo
fn main() {
{
let texto = String::from("Temporário");
println!("{}", texto);
} // 'texto' é descartado aqui automaticamente
// println!("{}", texto); // ERRO! 'texto' não existe mais
}
O Rust chama automaticamente a função drop quando uma variável sai do escopo. Isso é similar ao RAII do C++, mas obrigatório e verificado pelo compilador.
Move em Funções
Passar um valor para uma função também transfere o ownership:
fn imprimir(texto: String) {
println!("{}", texto);
} // 'texto' é descartado aqui
fn main() {
let mensagem = String::from("Olá, Rust!");
imprimir(mensagem); // ownership movido para a função
// println!("{}", mensagem); // ERRO! 'mensagem' foi movido
}
Diagrama do fluxo:
main() imprimir()
+----------+ +----------+
| mensagem |--- move ---->| texto |
| (movido) | | (owner) |
+----------+ +----------+
|
drop ao sair
do escopo
O mesmo acontece com retorno de funções — o ownership é transferido de volta:
fn criar_saudacao(nome: &str) -> String {
format!("Olá, {}!", nome) // retorna um novo String
}
fn main() {
let saudacao = criar_saudacao("Maria");
// 'saudacao' é o owner da String retornada
println!("{}", saudacao);
}
Tipos que Implementam Copy
Tipos simples que vivem inteiramente na stack implementam o trait Copy. Para esses tipos, atribuição cria uma cópia em vez de um move:
fn main() {
let x = 42; // i32 implementa Copy
let y = x; // x é COPIADO, não movido
println!("x = {}, y = {}", x, y); // Ambos são válidos!
let a = true; // bool implementa Copy
let b = a; // cópia
println!("a = {}, b = {}", a, b); // OK!
let c = 3.14; // f64 implementa Copy
let d = c; // cópia
println!("c = {}, d = {}", c, d); // OK!
}
Tipos que implementam Copy: todos os inteiros, floats, bool, char, tuplas (se todos os elementos forem Copy), arrays (se o tipo do elemento for Copy), e referências &T.
Tipos que NÃO implementam Copy: String, Vec<T>, Box<T>, e qualquer tipo que aloca na heap.
Clone: Cópia Explícita
Quando você precisa de uma cópia profunda de um tipo que não implementa Copy, use clone():
fn main() {
let s1 = String::from("Olá");
let s2 = s1.clone(); // cópia profunda explícita
println!("s1 = {}", s1); // OK! s1 ainda é válido
println!("s2 = {}", s2); // OK! s2 é uma cópia independente
}
Diagrama com clone:
Stack Heap
+------+ +---+---+---+
| s1 |----->| O | l | á | (dados originais)
+------+ +---+---+---+
+------+ +---+---+---+
| s2 |----->| O | l | á | (cópia independente)
+------+ +---+---+---+
Use clone() com consciência — ele pode ser custoso para dados grandes. Na maioria dos casos, o borrowing é a solução melhor.
Borrowing (Empréstimo): Referências
E se você quiser usar um valor sem tomar o ownership? É aí que entra o borrowing (empréstimo). Você cria uma referência ao valor:
fn calcular_tamanho(texto: &String) -> usize {
texto.len()
} // 'texto' sai do escopo, mas como é uma referência, não faz drop
fn main() {
let mensagem = String::from("Olá, Rust Brasil!");
let tamanho = calcular_tamanho(&mensagem);
// 'mensagem' ainda é válida!
println!("'{}' tem {} bytes", mensagem, tamanho);
}
Diagrama:
main() calcular_tamanho()
+-----------+ +--------+
| mensagem |<---ref------| texto | (&String)
| (owner) | | (ref) |
+-----------+ +--------+
| ptr -----|---> Heap |
| len: 18 | lê, mas não
| cap: 18 | modifica nem
+-----------+ toma ownership
O & cria uma referência. Ao chamar a função com &mensagem, estamos emprestando o valor, não transferindo ownership.
Referências Mutáveis
Para modificar um valor emprestado, use &mut:
fn adicionar_exclamacao(texto: &mut String) {
texto.push('!');
}
fn main() {
let mut mensagem = String::from("Olá, Rust");
println!("Antes: {}", mensagem);
adicionar_exclamacao(&mut mensagem);
println!("Depois: {}", mensagem);
}
Saída:
Antes: Olá, Rust
Depois: Olá, Rust!
As Regras do Borrowing
O borrow checker do Rust impõe duas regras fundamentais:
Regra 1: Pode haver quantas referências imutáveis (&T) forem necessárias ao mesmo tempo:
fn main() {
let texto = String::from("Olá");
let r1 = &texto; // OK
let r2 = &texto; // OK
let r3 = &texto; // OK
println!("{}, {}, {}", r1, r2, r3); // Todas válidas
}
Regra 2: Só pode haver UMA referência mutável (&mut T) por vez, e nenhuma referência imutável pode coexistir com ela:
fn main() {
let mut texto = String::from("Olá");
let r1 = &mut texto;
// let r2 = &mut texto; // ERRO! Duas refs mutáveis
// let r3 = &texto; // ERRO! Ref imutável com ref mutável
r1.push_str(", mundo!");
println!("{}", r1);
}
Diagrama das regras:
+-----------------------------------------------------+
| PERMITIDO: |
| |
| &T + &T + &T (muitas refs imutáveis) |
| &mut T (uma ref mutável, sozinha) |
| |
+-----------------------------------------------------+
| PROIBIDO: |
| |
| &mut T + &mut T (duas refs mutáveis) |
| &mut T + &T (mutável com imutável) |
| |
+-----------------------------------------------------+
Por que essa restrição? Ela previne data races em tempo de compilação. Um data race ocorre quando dois acessos ao mesmo dado acontecem simultaneamente e pelo menos um é de escrita.
Non-Lexical Lifetimes (NLL)
O compilador Rust é inteligente o suficiente para saber quando uma referência não está mais sendo usada:
fn main() {
let mut texto = String::from("Olá");
let r1 = &texto;
let r2 = &texto;
println!("{} e {}", r1, r2);
// r1 e r2 não são mais usados depois daqui
let r3 = &mut texto; // OK! r1 e r2 já não estão em uso
r3.push_str(", mundo!");
println!("{}", r3);
}
Isso funciona porque o compilador detecta que r1 e r2 não são usados depois do primeiro println!, então seus “lifetimes” terminam ali.
Lifetimes: Uma Introdução
Lifetimes são a forma do Rust garantir que referências nunca apontem para dados inválidos. Na maioria dos casos, o compilador infere os lifetimes automaticamente. Mas às vezes você precisa anotá-los:
// O compilador não sabe qual referência retornar
// Precisamos dizer que o retorno vive tanto quanto ambos os parâmetros
fn maior<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
fn main() {
let string1 = String::from("Olá, mundo!");
let resultado;
{
let string2 = String::from("Olá");
resultado = maior(&string1, &string2);
println!("A maior string é: {}", resultado);
}
// Se tentássemos usar 'resultado' aqui, e ele fosse &string2,
// teríamos uma referência inválida. O lifetime 'a garante segurança.
}
O 'a é uma anotação de lifetime. Ela diz ao compilador: “o retorno desta função vive pelo menos tanto quanto os parâmetros”. Não se preocupe em dominar lifetimes agora — eles serão abordados com mais detalhes em tutoriais avançados.
Dangling References: O Que Rust Impede
Uma “dangling reference” é uma referência para memória que já foi liberada. Rust impede isso em tempo de compilação:
// ERRO! Não compila!
// fn criar_referencia() -> &String {
// let s = String::from("Olá");
// &s // s será descartada, a referência seria inválida!
// }
// CORRETO: retorne o valor por ownership
fn criar_string() -> String {
let s = String::from("Olá");
s // ownership é transferido para quem chamou
}
fn main() {
let texto = criar_string();
println!("{}", texto);
}
Exemplo Prático: Sistema de Biblioteca
Vamos aplicar tudo que aprendemos em um exemplo real:
fn contar_palavras(texto: &str) -> usize {
texto.split_whitespace().count()
}
fn primeira_palavra(texto: &str) -> &str {
let bytes = texto.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &texto[0..i];
}
}
texto
}
fn formatar_titulo(titulo: &mut String) {
let primeiro_char = titulo.remove(0).to_uppercase().to_string();
titulo.insert_str(0, &primeiro_char);
titulo.push('.');
}
fn gerar_resumo(titulo: &str, autor: &str, paginas: u32) -> String {
format!(
"\"{}\" por {} ({} páginas, ~{} palavras estimadas)",
titulo,
autor,
paginas,
paginas * 250
)
}
fn main() {
// Ownership: livro_titulo é o owner
let mut livro_titulo = String::from("a linguagem de programação Rust");
// Borrowing imutável: contar palavras sem tomar ownership
let palavras = contar_palavras(&livro_titulo);
println!("Palavras no título: {}", palavras);
// Borrowing imutável: pegar primeira palavra
let primeira = primeira_palavra(&livro_titulo);
println!("Primeira palavra: {}", primeira);
// Borrowing mutável: formatar o título
formatar_titulo(&mut livro_titulo);
println!("Título formatado: {}", livro_titulo);
// Ownership: gerar_resumo recebe referências e retorna um novo String
let resumo = gerar_resumo(&livro_titulo, "Steve Klabnik", 560);
println!("\n{}", resumo);
// Clone: quando realmente precisa de uma cópia
let titulo_backup = livro_titulo.clone();
println!("\nOriginal: {}", livro_titulo);
println!("Backup: {}", titulo_backup);
}
Saída:
Palavras no título: 5
Primeira palavra: a
Título formatado: A linguagem de programação Rust.
"A linguagem de programação Rust." por Steve Klabnik (560 páginas, ~140000 palavras estimadas)
Original: A linguagem de programação Rust.
Backup: A linguagem de programação Rust.
Resumo Visual das Regras
+================================================================+
| OWNERSHIP EM RUST |
+================================================================+
| |
| let s1 = String::from("Olá"); |
| let s2 = s1; // MOVE: s1 invalidado |
| let s3 = s2.clone(); // CLONE: cópia independente |
| |
| let x = 42; |
| let y = x; // COPY: tipos simples copiam |
| |
+----------------------------------------------------------------+
| BORROWING |
+----------------------------------------------------------------+
| |
| let r1 = &s2; // ref imutável (pode ter várias) |
| let r2 = &s2; // outra ref imutável (OK) |
| |
| let r3 = &mut s2; // ref mutável (apenas uma) |
| // NÃO pode coexistir com &s2 |
| |
+================================================================+
Dicas Para Lidar com o Borrow Checker
- Comece com referências (
&e&mut) antes de recorrer a.clone() - Use
.clone()sem culpa enquanto aprende — otimize depois - Preste atenção nas mensagens de erro — o compilador do Rust é excepcionalmente útil
- Pense em quem é o “dono” de cada dado no seu programa
- Funções que só leem devem receber
&T; funções que modificam,&mut T
Próximos Passos
Agora que você entende ownership e borrowing, está pronto para aprender a modelar dados de forma expressiva com structs, enums e pattern matching.
Acesse o tutorial Structs, Enums e Pattern Matching para continuar.