O conceito de ownership: a base de tudo em Rust

1. Introdução ao Ownership

Toda linguagem de programação precisa gerenciar memória. Algumas usam coleta de lixo (garbage collector) como Java e Go, outras exigem gerenciamento manual como C. Rust introduz uma terceira via: o sistema de ownership. Essa abordagem inovadora permite que o compilador garanta segurança de memória sem a necessidade de um garbage collector, simplesmente aplicando três regras fundamentais:

  1. Cada valor em Rust tem exatamente um dono (owner)
  2. Só pode haver um dono por vez
  3. Quando o dono sai de escopo, o valor é descartado automaticamente

Essas regras podem parecer restritivas, mas são a base que permite ao Rust oferecer segurança de memória em tempo de compilação, eliminando categorias inteiras de bugs como ponteiros soltos, double-free e vazamentos de memória.

2. Regras do Escopo e Drop

Em Rust, o escopo de uma variável determina seu ciclo de vida. Quando uma variável sai de escopo, o compilador insere automaticamente uma chamada a drop(), liberando a memória associada.

fn main() {
    let x = 42; // x entra em escopo
    {
        let y = String::from("Olá"); // y entra em escopo
        println!("y = {}", y);
    } // y sai de escopo -> drop() é chamado aqui

    println!("x = {}", x);
} // x sai de escopo -> drop() é chamado aqui

Observe que y é descartado assim que o bloco interno termina, enquanto x permanece acessível até o final da função main. Esse comportamento previsível é fundamental para o gerenciamento seguro de recursos.

3. Move Semantics: Transferência de Ownership

A característica mais marcante do ownership é a movimentação (move). Quando você atribui um valor a outra variável, o ownership é transferido — a variável original não pode mais ser usada.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 move seu valor 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. Para tipos que vivem exclusivamente na stack, como inteiros, o comportamento é diferente:

fn main() {
    let a = 5;
    let b = a; // a é copiado (não movido) para b
    println!("a = {}, b = {}", a, b); // Ambos funcionam!
}

Tipos que implementam a trait Copy (como inteiros, booleanos, floats) são copiados implicitamente. Tipos que não implementam Copy (como String, Vec<T>) são movidos. Essa distinção evita cópias profundas desnecessárias e permite que o compilador rastreie o fluxo de dados.

4. Clone: Cópia Profunda Explícita

Quando você realmente precisa de uma cópia independente de um tipo que não implementa Copy, use o método .clone(). Isso realiza uma cópia profunda (deep copy) do valor.

fn main() {
    let s1 = String::from("Rust é incrível");
    let s2 = s1.clone(); // Cópia profunda explícita

    println!("s1 = {}", s1); // Agora funciona!
    println!("s2 = {}", s2);
}

A diferença fundamental entre Clone e Copy:
- Copy é implícito e barato (apenas cópia de bits)
- Clone é explícito e pode ser caro (alocações de heap, cópias profundas)

Para tipos complexos como structs, você precisa derivar Clone:

#[derive(Clone)]
struct Pessoa {
    nome: String,
    idade: u32,
}

fn main() {
    let p1 = Pessoa {
        nome: String::from("Ana"),
        idade: 30,
    };
    let p2 = p1.clone(); // Cópia profunda da struct inteira
}

5. Ownership em Funções: Passagem de Parâmetros

Quando você passa um valor para uma função, o ownership é transferido para o parâmetro da função. Isso significa que a variável original não pode mais ser usada após a chamada.

fn processa_string(s: String) {
    println!("Processando: {}", s);
} // s é descartado aqui

fn main() {
    let minha_string = String::from("dados importantes");
    processa_string(minha_string);
    // println!("{}", minha_string); // ERRO! ownership foi transferido
}

Se a função precisa usar o valor e depois devolvê-lo, você pode retorná-lo:

fn processa_e_devolve(s: String) -> String {
    println!("Processando: {}", s);
    s // Retorna ownership para o chamador
}

fn main() {
    let s1 = String::from("dados");
    let s2 = processa_e_devolve(s1);
    println!("De volta: {}", s2);
}

6. Ownership e Retorno de Funções

Quando uma função retorna um valor, o ownership é transferido para a variável que recebe o retorno. Esse padrão de "tomar e devolver" pode se tornar verboso quando múltiplos valores estão envolvidos:

fn processa_multi(s: String, v: Vec<i32>) -> (String, Vec<i32>) {
    println!("Processando string e vetor");
    (s, v) // Retorna ownership de ambos
}

fn main() {
    let texto = String::from("exemplo");
    let numeros = vec![1, 2, 3];
    let (texto, numeros) = processa_multi(texto, numeros);
    // Agora podemos usar texto e numeros novamente
}

Esse padrão funciona, mas rapidamente se torna tedioso. É aqui que o borrowing (empréstimo) e as referências entram em cena, permitindo que funções acessem dados sem tomar ownership. Este será o tema do próximo artigo.

7. Resumo e Conexão com Temas Vizinhos

Recapitulando as três regras de ownership:

  1. Cada valor tem exatamente um dono
  2. Apenas um dono por vez
  3. O valor é descartado quando o dono sai de escopo

Essas regras, combinadas com move semantics e clone explícito, formam a base do sistema de gerenciamento de memória do Rust. Elas eliminam:
- Ponteiros soltos: porque o compilador sabe exatamente quem é o dono
- Double-free: porque só há um dono que chama drop()
- Data races: porque o ownership é exclusivo para escrita

No próximo artigo, exploraremos como o borrowing e as referências permitem que múltiplas partes do código acessem dados sem violar as regras de ownership, abrindo caminho para padrões mais flexíveis e poderosos.

O sistema de ownership pode parecer desafiador no início, mas é precisamente essa característica que torna Rust uma das linguagens mais seguras para programação de sistemas, detectando em tempo de compilação bugs que em outras linguagens só seriam descobertos em produção.

Referências