Borrowing: referências imutáveis e mutáveis

1. Introdução ao Borrowing

Em Rust, o sistema de ownership resolve problemas de gerenciamento de memória sem um garbage collector, mas copiar valores grandes repetidamente seria ineficiente. O borrowing (empréstimo) surge como solução: em vez de transferir a posse de um valor, você pode "emprestá-lo" temporariamente.

A diferença fundamental entre ownership e borrowing é clara: no ownership, você é o dono do valor e responsável por liberá-lo; no borrowing, você apenas pega emprestado, sem assumir a responsabilidade final pela memória.

A sintaxe básica utiliza &T para referências imutáveis (apenas leitura) e &mut T para referências mutáveis (leitura e escrita).

let x = 42;
let ref_imutavel = &x;      // &i32
let mut y = 10;
let ref_mutavel = &mut y;   // &mut i32

2. Regras Fundamentais do Borrowing

O borrow checker impõe duas regras de ouro que todo desenvolvedor Rust precisa memorizar:

  1. Você pode ter uma ou mais referências imutáveis OU exatamente uma referência mutável.
  2. As referências devem sempre ser válidas (nunca apontar para memória liberada).

Essas regras derivam diretamente do sistema de ownership: quando você empresta um valor, o dono original mantém a posse, mas cede acesso temporário. O empréstimo termina quando a referência sai de escopo.

fn main() {
    let mut numero = 5;
    {
        let r = &mut numero; // empréstimo começa
        *r += 1;
    } // empréstimo termina aqui
    println!("{}", numero); // 6 - dono original intacto
}

3. Referências Imutáveis (&T)

Referências imutáveis permitem ler dados sem modificá-los. Você pode criar quantas quiser simultaneamente:

fn main() {
    let dados = vec![1, 2, 3, 4, 5];
    let ref1 = &dados;
    let ref2 = &dados;
    let ref3 = &dados;

    println!("ref1: {:?}, ref2: {:?}, ref3: {:?}", ref1, ref2, ref3);
    // Todas as três referências coexistem pacificamente
}

A limitação principal: você não pode modificar o valor através de uma referência imutável. Tentar fazer isso gera erro de compilação:

fn main() {
    let valor = 10;
    let ref_imut = &valor;
    // *ref_imut = 20; // ERRO: cannot assign to `*ref_imut`, which is behind a `&` reference
}

4. Referências Mutáveis (&mut T)

Referências mutáveis permitem alterar o valor emprestado. A regra de exclusividade é rigorosa: apenas uma referência mutável pode existir em um determinado escopo.

fn main() {
    let mut texto = String::from("Olá");
    let ref_mut = &mut texto;
    ref_mut.push_str(", mundo!");
    println!("{}", ref_mut); // "Olá, mundo!"
}

Tentar criar duas referências mutáveis no mesmo escopo causa erro:

fn main() {
    let mut valor = 42;
    let a = &mut valor;
    let b = &mut valor; // ERRO: cannot borrow `valor` as mutable more than once at a time
    println!("{} {}", a, b);
}

5. Interação entre Referências Imutáveis e Mutáveis

O conflito mais comum ocorre quando tentamos misturar referências imutáveis e mutáveis no mesmo escopo:

fn main() {
    let mut dados = vec![1, 2, 3];
    let imutavel = &dados[0]; // referência imutável
    let mutavel = &mut dados; // ERRO: cannot borrow `dados` as mutable because it is also borrowed as immutable
    println!("{} {}", imutavel, mutavel[1]);
}

A mensagem do compilador é clara: você não pode emprestar como mutável algo que já está emprestado como imutável. A solução é reorganizar o escopo:

fn main() {
    let mut dados = vec![1, 2, 3];
    {
        let imutavel = &dados[0];
        println!("{}", imutavel);
    } // empréstimo imutável termina aqui
    let mutavel = &mut dados;
    mutavel.push(4);
    println!("{:?}", mutavel);
}

6. Escopo e Lifetime das Referências

O escopo determina quando um empréstimo termina. Tradicionalmente, o escopo de uma referência seguia o bloco onde foi criada, mas o Rust moderno implementa NLL (Non-Lexical Lifetimes), que permite que o borrow checker seja mais inteligente:

fn main() {
    let mut x = 5;
    let y = &x;        // empréstimo imutável começa
    println!("{}", y); // último uso de y
    // Com NLL, o empréstimo termina aqui, mesmo dentro do mesmo bloco
    let z = &mut x;    // agora permitido!
    *z += 1;
    println!("{}", z);
}

Sem NLL, o código acima não compilaria. O NLL analisa onde a referência é realmente usada pela última vez, permitindo que empréstimos terminem antes do final do bloco léxico.

7. Boas Práticas e Padrões Comuns

Quando usar referência imutável vs mutável:

  • Use &T quando você só precisa ler dados (funções de consulta, exibição)
  • Use &mut T quando você precisa modificar o valor (funções de atualização)

Evitando referências penduradas (dangling references):

fn cria_referencia_pendurada() -> &i32 {
    let x = 5;
    &x // ERRO: `x` é liberado quando a função termina
}

O compilador impede isso com o lifetime checker. A solução é retornar o valor por ownership ou usar um lifetime explícito com dados que vivem mais tempo.

Exemplo prático: função que recebe &str vs &mut String

fn exibir_texto(texto: &str) {
    println!("Texto: {}", texto);
}

fn adicionar_saudacao(texto: &mut String) {
    texto.push_str(", bem-vindo!");
}

fn main() {
    let mut mensagem = String::from("Olá");
    exibir_texto(&mensagem);       // referência imutável
    adicionar_saudacao(&mut mensagem); // referência mutável
    exibir_texto(&mensagem);       // "Olá, bem-vindo!"
}

Padrão comum: split borrows

Quando você precisa emprestar diferentes partes de uma estrutura simultaneamente:

struct Pessoa {
    nome: String,
    idade: u32,
}

fn main() {
    let mut p = Pessoa {
        nome: String::from("Alice"),
        idade: 30,
    };

    let nome_ref = &mut p.nome;
    let idade_ref = &mut p.idade; // Permitido! São campos diferentes

    nome_ref.push_str(" Silva");
    *idade_ref += 1;

    println!("{} tem {} anos", p.nome, p.idade);
}

O borrow checker entende que campos diferentes de uma struct são independentes, permitindo múltiplas referências mutáveis para partes diferentes do mesmo dado.

Dominar o borrowing é essencial para escrever Rust seguro e eficiente. As regras podem parecer restritivas no início, mas elas previnem toda uma classe de bugs comuns em outras linguagens, como data races e uso após liberação.

Referências