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
- The Rust Programming Language, Cap. 4.2 — References and Borrowing — https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
- The Rust Programming Language, Cap. 4.3 — The Slice Type — https://doc.rust-lang.org/book/ch04-03-slices.html
- Rust by Example — Borrowing — https://doc.rust-lang.org/rust-by-example/scope/borrow.html
- Rust RFC 2094 — Non-Lexical Lifetimes — o documento que introduziu NLL — https://rust-lang.github.io/rfcs/2094-nll.html
- Jon Gjengset — Crust of Rust: Lifetime Annotations — https://www.youtube.com/watch?v=rAl-9HwD858
- Rustlings, seção
references— https://github.com/rust-lang/rustlings