Rust

Borrowing e Referências — Usando sem Possuir Já leu

9 min de leitura

Borrowing e Referências — Usando sem Possuir
  No artigo anterior, aprendemos que ownership resolve o problema do gerenciamento de memória — mas cria um inconveniente: passar um valor pa

 

No artigo anterior, 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.


Fontes e leituras recomendadas

Comentários

Mais em Rust

Traits — Definindo Comportamento Compartilhado
Traits — Definindo Comportamento Compartilhado

  Nos artigos anteriores criamos structs e enums para modelar dados. Ma...

Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores
Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores

Se você vem de Python, JavaScript ou Java, já sabe o que &eacute...

Smart Pointers — Box, Rc e RefCell
Smart Pointers — Box, Rc e RefCell

  Até agora trabalhamos com dados na stack e referências s...