Ownership e Borrowing em Rust: Tutorial | Rust Brasil

Entenda ownership, borrowing e lifetimes em Rust. Tutorial completo com exemplos práticos do sistema de propriedade.

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:

  1. Cada valor em Rust tem uma variável que é sua “dona” (owner)
  2. Só pode haver um owner por vez
  3. 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

  1. Comece com referências (& e &mut) antes de recorrer a .clone()
  2. Use .clone() sem culpa enquanto aprende — otimize depois
  3. Preste atenção nas mensagens de erro — o compilador do Rust é excepcionalmente útil
  4. Pense em quem é o “dono” de cada dado no seu programa
  5. 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.