Borrowing e Referências — Usando sem Possuir

Borrowing e Referências — Usando sem Possuir

Ownership resolve a memória e cria um incômodo: passar um valor adiante significa perdê-lo. O borrowing é a saída — usar sem possuir. O texto trata da regra que permite muitas leituras ou uma escrita, nunca as duas, das referências pendentes que o compilador recusa, e das slices.
Rust

13 min de leitura

 

No artigo Ownership — A Ideia que Muda Tudo, aprendemos que ownership resolve o problema do gerenciamento de memória — mas cria um inconveniente: passar um valor para uma função o move, e você perde o acesso a ele. A solução para isso é borrowing: a capacidade de usar um valor sem tomar posse dele.

Borrowing é implementado através de referências. É um dos sistemas mais elegantes de Rust, e também o que mais exige atenção nos primeiros dias. Vamos com calma.

O que é uma referência

Uma referência é um ponteiro que aponta para um valor sem possuí-lo. Você cria uma referência com o símbolo &:

fn calcular_tamanho(s: &String) -> usize {
    s.len()
}

fn main() {
    let s = String::from("hello");
    let tam = calcular_tamanho(&s);
    println!("'{s}' tem {tam} caracteres.");
}

&s cria uma referência para s — um empréstimo. A função calcular_tamanho recebe &String, ou seja, uma referência a uma String, não a String em si. Quando a função termina, a referência expira — mas s continua válida no main, porque nunca transferimos a posse.

Visualmente:

s  --> [ ptr | len=5 | cap=5 ] --> heap: "hello"
         ^
         |
referência &s aponta para s, não para o heap diretamente

O ato de criar uma referência chama-se borrowing — empréstimo. Você empresta o valor para outra parte do código, que o usa e o devolve implicitamente ao final.

Referências são imutáveis por padrão

Assim como variáveis, referências são imutáveis por padrão. Tentar modificar algo através de uma referência comum gera erro:

fn tentar_modificar(s: &String) {
    s.push_str(" mundo"); // ERRO: não pode modificar referência imutável
}

fn main() {
    let s = String::from("hello");
    tentar_modificar(&s);
}

O compilador recusa. Para modificar um valor através de uma referência, você precisa de uma referência mutável.

Referências mutáveis com &mut

Para criar uma referência mutável, tanto a variável quanto a referência precisam ser declaradas como mutáveis:

fn acrescentar(s: &mut String) {
    s.push_str(", mundo");
}

fn main() {
    let mut s = String::from("hello");
    acrescentar(&mut s);
    println!("{s}"); // hello, mundo
}

Dois requisitos obrigatórios: a variável s deve ser let mut, e a referência passada deve ser &mut s. Ambos precisam concordar — o compilador não deixa passar se um dos dois estiver faltando.

A regra de ouro das referências mutáveis

Aqui está a restrição mais importante de todo o sistema de borrowing:

Em qualquer ponto do código, você pode ter OU uma referência mutável OU qualquer número de referências imutáveis — mas nunca ambas ao mesmo tempo.

Isso parece restritivo. É proposital. Veja por quê:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;     // referência imutável — OK
    let r2 = &s;     // outra referência imutável — OK
    let r3 = &mut s; // ERRO: já existem referências imutáveis!

    println!("{r1} {r2} {r3}");
}

O compilador rejeita r3. Por quê? Porque se r3 pudesse modificar s, as leituras através de r1 e r2 veriam dados inconsistentes — o que em sistemas concorrentes seria uma condição de corrida catastrófica. Em sistemas de thread única, seria simplesmente um comportamento imprevisível.

Rust elimina toda uma categoria de bugs — data races — tornando-os impossíveis de compilar.

Da mesma forma, duas referências mutáveis simultâneas também são proibidas:

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s; // ERRO: segunda referência mutável!

    println!("{r1} {r2}");
}

O compilador é mais inteligente do que parece

O compilador analisa o tempo de vida real de cada referência, não apenas o escopo do bloco. Isso é chamado de Non-Lexical Lifetimes (NLL), introduzido no Rust 2018. Na prática, significa que referências expiram quando são usadas pela última vez, não quando o bloco fecha:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{r1} e {r2}"); // último uso de r1 e r2

    // r1 e r2 expiraram aqui — não são mais usadas
    let r3 = &mut s; // OK! r1 e r2 já não existem
    println!("{r3}");
}

Isso compila sem problemas. O compilador percebe que r1 e r2 não são mais usadas após o primeiro println!, então permite a criação de r3. Esse comportamento inteligente torna o sistema muito menos restritivo na prática do que parece na teoria.

Referências pendentes — dangling references

Em C, é possível retornar um ponteiro para uma variável local — que foi destruída quando a função terminou. O resultado é acesso a memória inválida, um bug devastador. Em Rust, isso é impossível:

fn criar_referencia() -> &String { // ERRO de compilação
    let s = String::from("hello");
    &s // s será destruído ao final da função!
}

O compilador detecta que s morrerá quando a função retornar, e que a referência apontaria para memória liberada. Ele recusa o código antes mesmo de você executar.

A solução correta é retornar a String diretamente — transferindo a ownership:

fn criar_string() -> String {
    let s = String::from("hello");
    s // ownership transferido, sem referência pendente
}

fn main() {
    let s = criar_string();
    println!("{s}");
}

Slices — referências para partes de coleções

Um tipo especial de referência merece atenção aqui: o slice. Um slice é uma referência para uma sequência contígua de elementos em uma coleção — sem copiar os dados:

fn main() {
    let s = String::from("hello mundo");

    let hello = &s[0..5];   // slice dos primeiros 5 bytes
    let mundo = &s[6..11];  // slice dos últimos 5 bytes

    println!("{hello}"); // hello
    println!("{mundo}"); // mundo
}

A sintaxe [inicio..fim] cria um range. O início é inclusivo, o fim é exclusivo. Existem atalhos:

fn main() {
    let s = String::from("hello");

    let do_inicio = &s[..3];  // equivale a &s[0..3]
    let ate_fim   = &s[2..];  // equivale a &s[2..s.len()]
    let tudo      = &s[..];   // equivale a &s[0..s.len()]

    println!("{do_inicio} | {ate_fim} | {tudo}");
}

Slices de string têm tipo &str — e é por isso que literais de string como "hello" têm tipo &str: eles são slices que apontam para o binário do programa.

Um exemplo prático: primeira palavra

Vamos escrever uma função que retorna a primeira palavra de uma frase, sem copiar nada:

fn primeira_palavra(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[..i]; // retorna slice até o espaço
        }
    }

    &s[..] // sem espaço: a string inteira é a primeira palavra
}

fn main() {
    let frase = String::from("hello mundo rust");
    let palavra = primeira_palavra(&frase);
    println!("Primeira palavra: {palavra}");
}

Saída:

Primeira palavra: hello

A função recebe &str em vez de &String — isso é idiomático em Rust. &str é mais genérico: aceita tanto slices de String quanto literais de string. Prefira &str em parâmetros de função sempre que não precisar modificar o conteúdo.

E o slice retornado é automaticamente inválido se você tentar modificar frase depois — o compilador garante isso:

fn main() {
    let mut frase = String::from("hello mundo");
    let palavra = primeira_palavra(&frase);
    frase.clear(); // ERRO: frase é mutavelmente emprestada aqui,
                   // mas palavra ainda está em uso!
    println!("{palavra}");
}

Sem Rust, esse bug seria silencioso: palavra apontaria para memória que acabou de ser limpa. Com Rust, é um erro de compilação.

Resumo das regras de borrowing

// Referência imutável — leitura apenas
let r = &valor;

// Referência mutável — leitura e escrita
let r = &mut valor; // valor também deve ser mut

// Regra central:
// - Múltiplas referências imutáveis: OK
// - Uma referência mutável: OK
// - Referência mutável + imutável ao mesmo tempo: ERRO
// - Duas referências mutáveis ao mesmo tempo: ERRO

// Referências não podem sobreviver ao valor que referenciam
// O compilador garante isso em tempo de compilação

O que borrowing representa filosoficamente

Ownership e borrowing juntos implementam em código uma ideia simples da vida real: você pode emprestar algo a alguém, mas enquanto está emprestado, você não pode jogá-lo fora. E se emprestou para leitura, não pode dar para outra pessoa modificar ao mesmo tempo.

Rust pega essa intuição humana sobre propriedade e responsabilidade e a codifica nas regras do compilador. O resultado é um sistema onde corrida de dados é impossível por construção — não por disciplina, não por testes, mas por definição da linguagem.

Isso é o que faz Rust ser usado em kernels de sistemas operacionais, navegadores, sistemas embarcados e infraestrutura crítica. Não é modismo — é garantia.

Exercícios

Exercício 1

Enuncie a regra de ouro do borrowing e explique que problema concreto ela evita.

Ver resposta

✓ Resposta: Num dado ponto do programa, para um mesmo dado, é permitido ter ou várias referências imutáveis ou exatamente uma mutável — nunca as duas coisas ao mesmo tempo. Isso elimina por construção a corrida de dados e a invalidação silenciosa: ninguém pode ler um valor enquanto outro caminho o modifica, nem manter uma referência para dentro de um vetor que acabou de realocar. Em C++ o equivalente compila e produz leitura de memória inválida.

Exercício 2

O código não compila. Explique e corrija sem usar clone.

fn main() {
    let mut s = String::from("olá");
    let r1 = &s;
    let r2 = &mut s;
    println!("{r1} {r2}");
}
Ver resposta

✓ Resposta: r1 é um empréstimo imutável ainda em uso no println!, e r2 tenta pegar um empréstimo mutável ao mesmo tempo — o erro é cannot borrow s as mutable because it is also borrowed as immutable. A correção é separar os usos no tempo:

fn main() {
    let mut s = String::from("olá");
    let r1 = &s;
    println!("{r1}");      // último uso de r1
    let r2 = &mut s;
    r2.push_str(" mundo");
    println!("{r2}");
}

Funciona por causa do non-lexical lifetime: o empréstimo acaba no último uso, não no fim do bloco.

Exercício 3

Explique por que a função abaixo não compila e o que a assinatura tenta fazer de impossível.

fn cria() -> &String {
    let s = String::from("olá");
    &s
}
Ver resposta

✓ Resposta: A função cria uma String local e tenta devolver uma referência a ela. Quando a função retorna, s sai de escopo e é descartada — a referência apontaria para memória liberada. O compilador recusa com missing lifetime specifier, porque não existe nenhum tempo de vida válido que a assinatura possa declarar: não há parâmetro de entrada de onde o empréstimo pudesse vir. A correção é devolver a posse:

fn cria() -> String {
    String::from("olá")
}

É o mesmo erro que em C produz o clássico ponteiro pendurado — com a diferença de que ali compila e falha depois.

Exercício 4

Escreva uma função que receba uma frase e devolva a primeira palavra, sem copiar o texto. Explique por que o tipo de retorno é o que é.

Ver resposta

✓ Resposta:

fn primeira_palavra(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

O retorno é &str — uma slice, isto é, uma referência a um trecho do texto original, composta de ponteiro e comprimento. Não há alocação nem cópia. Por ser uma referência, ela mantém o original emprestado: enquanto o resultado estiver em uso, a string de origem não pode ser modificada nem descartada, e é justamente isso que impede a slice de apontar para lixo.

Exercício 5

Explique por que o trecho abaixo é recusado, ligando a resposta ao que uma slice realmente é.

let mut frase = String::from("olá mundo");
let palavra = primeira_palavra(&frase);
frase.clear();
println!("{palavra}");
Ver resposta

✓ Resposta: palavra é uma slice que aponta para dentro do buffer de frase. O clear() precisa de um empréstimo mutável, mas a slice ainda está viva e será usada no println! — o compilador recusa com cannot borrow frase as mutable because it is also borrowed as immutable. A regra está evitando um bug real: se o clear() passasse, a slice apontaria para conteúdo esvaziado. Em linguagens sem verificação de empréstimo, esse é o caso clássico de iterador ou ponteiro invalidado que falha em tempo de execução.

Fontes e leituras recomendadas

Comentários

Mais em Rust

Traits — Definindo Comportamento Compartilhado
Traits — Definindo Comportamento Compartilhado

Trait descreve comportamento, não estrutura, e é o que Rust oferece no lugar…

Autenticação JWT — Protegendo sua API com Tokens
Autenticação JWT — Protegendo sua API com Tokens

Token assinado não é token secreto: o payload de um JWT é legível por qualquer…

Async/Await — Concorrência sem Threads com Tokio
Async/Await — Concorrência sem Threads com Tokio

Async não cria threads: cria tarefas que cedem a vez enquanto esperam. O…