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:

  1. Cada valor em Rust tem exatamente um dono (owner)
  2. Quando o dono sai de escopo, o valor é automaticamente liberado
  3. 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