teoria

Referências e Borrowing: Empréstimo de Dados

Aprenda sobre referências e borrowing: empréstimo de dados

25 min
Aula 3 de 5

Referências e Borrowing: Empréstimo de Dados 🤝

Olá, estudante! Seja bem-vindo(a) à aula sobre Referências e Borrowing no nosso curso de Rust. Nesta aula, vamos explorar um conceito fundamental que permite que você utilize dados sem precisar se tornar o "dono" deles, garantindo segurança e eficiência. Prepare-se para entender como o Rust lida com o "empréstimo" de dados de forma única!

1. Introdução Clara: Por que Emprestar Dados? 🤔

No Rust, o sistema de ownership (posse) é a base para a segurança de memória. Cada valor tem um único dono, e quando o dono sai de escopo, o valor é descartado. Mas e se você precisar usar um valor em uma função ou em outra parte do seu código sem transferir a posse e sem fazer uma cópia desnecessária?

Imagine que você tem um livro 📚 (um dado) e quer que um amigo o leia. Você não quer dar o livro para sempre (transferir a posse), nem quer comprar um livro novo para ele (fazer uma cópia). Você simplesmente empresta o seu livro. Seu amigo pode lê-lo, talvez até fazer anotações (se você permitir), mas ele sabe que o livro ainda é seu e deve ser devolvido.

É exatamente isso que referências e borrowing (empréstimo) fazem no Rust!

  • Referências são como ponteiros, mas com garantias adicionais de segurança. Elas permitem que você se refira a um valor sem tomar posse dele.
  • Borrowing é o ato de criar uma referência a um valor. Quando você "empresta" um valor, o owner original ainda mantém a posse, mas o "borrower" (quem pegou emprestado) pode acessar o valor pelo tempo que a referência for válida.

Este mecanismo é crucial para evitar data races (condições de corrida de dados) e garantir a segurança de memória sem a necessidade de um garbage collector (coletor de lixo).

2. Explicação Detalhada com Exemplos 🧑‍🏫

Vamos mergulhar nos detalhes de como as referências e o borrowing funcionam no Rust.

O que é uma Referência?

Uma referência é um tipo que "aponta" para outro valor, mas não possui esse valor. Em Rust, as referências são criadas usando o operador &.

fn main() {
    let s1 = String::from("hello"); // s1 é o dono da String
 
    // s2 é uma referência imutável a s1.
    // s1 ainda é o dono.
    let s2 = &s1;
 
    println!("s1: {}", s1); // Podemos usar s1
    println!("s2: {}", s2); // Podemos usar s2 para acessar o conteúdo de s1
} // s1 sai de escopo e é descartado, s2 deixa de ser válido.

No exemplo acima, s2 é uma referência a s1. Isso significa que s2 pode ler o valor que s1 possui, mas não pode modificá-lo (a menos que seja uma referência mutável, como veremos). O mais importante é que s1 continua sendo o dono. Quando s2 sai de escopo, nada acontece com o valor, pois s1 ainda o possui.

Empréstimo (Borrowing)

Quando você passa uma referência para uma função, estamos "emprestando" o valor para essa função. A função pode usar o valor, mas não se torna a dona dele.

fn calcula_tamanho(s: &String) -> usize { // s é uma referência a uma String
    s.len() // .len() não precisa da posse, apenas lê o valor
} // s sai de escopo, mas o valor que ele referencia não é descartado.
 
fn main() {
    let s1 = String::from("olá, mundo"); // s1 é o dono
 
    let len = calcula_tamanho(&s1); // Emprestamos s1 para a função
 
    println!("A string '{}' tem o tamanho {}.", s1, len); // s1 ainda é válido e pode ser usado
}

Neste exemplo, a função calcula_tamanho recebe uma referência (&String). Isso significa que ela pode ler a string, mas não pode modificá-la ou descartá-la. A posse de s1 permanece na função main.

Referências Mutáveis vs. Imutáveis

Rust faz uma distinção crucial entre referências imutáveis e mutáveis:

Referências Imutáveis (&)

  • Permitem apenas a leitura do valor.
  • Você pode ter múltiplas referências imutáveis ao mesmo valor ao mesmo tempo.
  • Isso é seguro, pois leituras simultâneas não causam problemas de concorrência.
fn main() {
    let s = String::from("hello");
 
    let r1 = &s; // Referência imutável
    let r2 = &s; // Outra referência imutável
 
    println!("{}, {}", r1, r2); // Ambos podem ser usados
    // s.push_str(" world"); // ERRO: Não podemos modificar 's' enquanto há referências imutáveis
}

Referências Mutáveis (&mut)

  • Permitem a leitura E a modificação do valor.
  • Você só pode ter UMA referência mutável a um valor em um determinado escopo.
  • Isso garante que não haverá data races: se apenas uma referência pode modificar os dados por vez, não há chance de duas modificações conflitarem.
fn muda_string(s: &mut String) { // s é uma referência mutável
    s.push_str(", mundo"); // Podemos modificar a String
}
 
fn main() {
    let mut s = String::from("olá"); // s precisa ser mutável para que possamos pegar uma ref mutável
 
    muda_string(&mut s); // Emprestamos s como mutável
    println!("{}", s); // Saída: olá, mundo
}

As Regras de Empréstimo (Borrowing Rules) 🚨

Esta é a parte mais importante para entender a segurança do Rust. Em qualquer momento, você pode ter:

  1. Uma (e apenas uma) referência mutável para um determinado dado.
  2. Muitas referências imutáveis para o mesmo dado.

Você NÃO pode ter uma referência mutável E referências imutáveis para o mesmo dado ao mesmo tempo.

Vamos ver um exemplo de código que não compila devido a essas regras:

fn main() {
    let mut s = String::from("hello");
 
    let r1 = &s; // Ok: primeira referência imutável
    let r2 = &s; // Ok: segunda referência imutável
 
    println!("{} e {}", r1, r2); // Ok: r1 e r2 são usados aqui
 
    // Após r1 e r2 serem usados pela última vez, seus escopos "terminam" para o borrow checker.
    // Isso é importante: o escopo de uma referência não é apenas onde ela é declarada,
    // mas sim onde ela é usada pela última vez.
 
    let r3 = &mut s; // ERRO se r1 ou r2 ainda estiverem em uso!
                     // Se r1 e r2 não forem mais usados, isso seria permitido.
 
    // Exemplo que GERA ERRO DE COMPILAÇÃO:
    // let mut s = String::from("hello");
    // let r1 = &s; // no problem
    // let r2 = &s; // no problem
    // let r3 = &mut s; // BIG PROBLEM! Não pode ter ref mutável enquanto imutáveis estão ativas.
 
    // println!("{}, {}, e {}", r1, r2, r3); // ERRO: r1 e r2 são usados aqui, mas r3 já foi criado como mutável.
}

O compilador Rust é inteligente o suficiente para determinar o tempo de vida de uma referência. Se as referências imutáveis r1 e r2 não forem usadas após a criação de r3, o código pode compilar. No entanto, se r1 ou r2 forem usados depois da criação de r3, o compilador emitirá um erro.

Referências Penduradas (Dangling References) 👻

Uma "referência pendurada" (dangling reference) é uma referência que aponta para um local de memória que foi liberado, ou seja, o dado que ela referia já não existe mais. Isso é uma fonte comum de bugs em outras linguagens (como C/C++).

Rust garante que você nunca terá uma referência pendurada. Se você tentar criar uma, o compilador não permitirá.

// Este código NÃO COMPILA!
fn dangle() -> &String { // dangle retorna uma referência a uma String
    let s = String::from("hello"); // s é uma nova String
 
    &s // Retornamos uma referência a s
} // s sai de escopo AQUI. Sua memória é liberada.
  // A referência que retornamos estaria apontando para memória inválida!
  // Rust previne isso em tempo de compilação.
 
fn main() {
    // let reference_to_nothing = dangle(); // Isso resultaria em um erro de compilação.
    println!("Rust impede referências penduradas! 🎉");
}

O compilador Rust detecta que s será descartado ao final da função dangle, mas você está tentando retornar uma referência a ele. Ele te força a corrigir isso, por exemplo, retornando a String diretamente (transferindo posse) ou garantindo que a String viva por tempo suficiente.

Lifetimes (Tempo de Vida) - Uma Prévia ⏳

Embora não seja o foco principal desta aula, o conceito de lifetimes (tempos de vida) está intrinsecamente ligado ao borrowing. Lifetimes são uma forma que o Rust usa para garantir que todas as referências são válidas pelo tempo que são usadas.

Você verá anotações de lifetime como 'a em assinaturas de funções que usam referências, especialmente quando o compilador não consegue inferir o lifetime por si só. Não se preocupe muito com isso agora; o importante é saber que o Rust usa lifetimes para aplicar as regras de borrowing e prevenir referências penduradas.

3. Código de Exemplo Oficial 📖

Os exemplos que usamos acima são adaptações dos exemplos do The Rust Programming Language Book, que é a documentação oficial do Rust. Eles ilustram perfeitamente os conceitos de referências imutáveis, mutáveis e as regras de borrowing.

Aqui está um exemplo combinado que mostra a regra de uma referência mutável ou muitas imutáveis, e como o Rust ajuda a gerenciar isso:

fn main() {
    let mut s = String::from("olá");
 
    let r1 = &s; // OK
    let r2 = &s; // OK
    println!("r1: {}, r2: {}", r1, r2);
    // As referências r1 e r2 não são mais usadas depois deste println!,
    // então seus "lifetimes" efetivamente terminam aqui para o borrow checker.
 
    let r3 = &mut s; // OK, porque r1 e r2 não estão mais em uso ativo
    r3.push_str(", mundo");
    println!("r3: {}", r3);
 
    // Se tentarmos usar r1 ou r2 AQUI, teríamos um erro!
    // Ex: println!("r1: {}", r1); // ERRO: r1 foi invalidado pela criação de r3
}

Este exemplo é crucial para entender como o compilador Rust é inteligente. Ele não apenas olha para onde as referências são declaradas, mas para onde elas são usadas pela última vez. Isso permite mais flexibilidade do que uma regra estrita de escopo léxico.

4. Resumo e Próximos Passos 🚀

Parabéns! Você dominou os conceitos de Referências e Borrowing, um dos pilares da segurança e performance do Rust.

Pontos Chave para Lembrar:

  • Referências (&) permitem que você acesse dados sem tomar posse.
  • Borrowing é o ato de criar uma referência.
  • Referências Imutáveis (&): Permitem múltiplas referências ao mesmo tempo para leitura.
  • Referências Mutáveis (&mut): Permitem apenas uma referência por vez para leitura e escrita.
  • Regras de Borrowing: Em qualquer momento, você pode ter uma referência mutável OU muitas referências imutáveis, mas nunca ambas ao mesmo tempo.
  • Dangling References: Rust previne referências penduradas em tempo de compilação.
  • Lifetimes: Mecanismo do Rust para garantir a validade das referências.

Compreender o borrowing é essencial para escrever código Rust seguro e eficiente. É a forma como o Rust evita muitos dos problemas de memória que afligem outras linguagens.

Próximos Passos: Slices 🔪

No próximo módulo, vamos explorar um tipo especial de referência: os Slices. Slices permitem que você se refira a uma parte contígua de uma coleção (como uma String ou um Vec) sem copiar os dados. Eles são referências, e, portanto, as regras de borrowing se aplicam a eles também!

Até lá, continue praticando e explorando! 🧑‍💻✨

© 2025 Escola All Dev. Todos os direitos reservados.

Referências e Borrowing: Empréstimo de Dados - Curso gratuito de Rust: A linguagem mais amada | escola.all.dev.br