O borrowing é um dos conceitos fundamentais de Rust que permite acessar dados sem transferir a propriedade (ownership). Em vez de passar o valor inteiro para uma função, podemos emprestar uma referência a ele. Isso é essencial para escrever código eficiente e seguro, evitando cópias desnecessárias e garantindo que não haja data races ou uso inválido de memória.

Nesta aula, vamos explorar como criar referências com o operador &, a diferença entre referências mutáveis e imutáveis, e as regras que o borrow checker impõe para garantir que todas as referências sejam válidas e não conflitem entre si.

Referências (&)

Uma referência em Rust é como um ponteiro que aponta para um valor sem possuí-lo. O operador & é usado para criar uma referência a um valor, e o operador * é usado para dereferenciar (acessar o valor apontado). O tipo de uma referência imutável é &T, onde T é o tipo do valor.

As referências são não-copiáveis por padrão, mas podem ser copiadas se o tipo subjacente implementar a trait Copy. No entanto, o mais comum é que as referências sejam emprestadas temporariamente. O Rust garante que a referência nunca seja nula ou inválida, desde que o borrow checker aprove o código.

fn main() {
    let x = 42;
    let ref_x = &x; // referência imutável a x
    println!("Valor de x: {}", *ref_x); // dereferenciando
    println!("Valor de x (auto-deref): {}", ref_x); // auto-dereferencia
}

Mutável vs imutável

Referências podem ser imutáveis (&T) ou mutáveis (&mut T). Uma referência imutável permite apenas leitura do valor, enquanto uma referência mutável permite modificação. O Rust impõe uma regra importante: você pode ter várias referências imutáveis simultaneamente, mas apenas uma referência mutável de cada vez, e ela não pode coexistir com referências imutáveis.

Essa regra previne data races em tempo de compilação. Se você tem uma referência mutável, nenhuma outra referência (imutável ou mutável) pode acessar o mesmo dado ao mesmo tempo. Isso garante que não haja conflitos de leitura/escrita.

fn main() {
    let mut s = String::from("hello");
    let ref1 = &s; // imutável
    let ref2 = &s; // imutável, ok
    // let ref3 = &mut s; // erro: não pode ter ref mutável junto com imutáveis
    println!("{}, {}", ref1, ref2);
}
fn main() {
    let mut s = String::from("hello");
    let ref_mut = &mut s;
    ref_mut.push_str(" world");
    println!("{}", ref_mut);
}

Regras do borrow checker

O borrow checker é o componente do compilador Rust que verifica as regras de empréstimo. As principais regras são:

  • Você pode ter uma ou mais referências imutáveis (&T) para um valor, mas não pode ter uma referência mutável (&mut T) ao mesmo tempo.
  • Você pode ter exatamente uma referência mutável (&mut T) para um valor, mas não pode ter nenhuma referência imutável ao mesmo tempo.
  • A referência deve sempre apontar para um valor válido (não pode ser dangling). O compilador garante que o valor referenciado não seja destruído enquanto a referência existir.

Essas regras são verificadas em tempo de compilação, sem custo em tempo de execução. Se o código violar alguma regra, o compilador emite um erro e o programa não compila. Isso elimina toda uma classe de bugs comuns em C/C++.

fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0]; // referência imutável
    v.push(4); // erro: mutação enquanto referência imutável existe
    println!("{}", first);
}

Exemplos

Vamos ver alguns exemplos práticos que ilustram o uso correto de borrowing.

Exemplo 1: Função que toma referência

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

fn main() {
    let s = String::from("hello");
    let len = calcular_comprimento(&s);
    println!("O comprimento de '{}' é {}.", s, len);
}

Exemplo 2: Modificando via referência mutável

fn adicionar_exclamacao(s: &mut String) {
    s.push_str("!");
}

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

Exemplo 3: Múltiplas referências imutáveis

fn main() {
    let s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    println!("{} e {}", r1, r2);
    // r1 e r2 saem de escopo aqui, então podemos ter ref mutável depois
    let r3 = &mut s; // erro se r1 ou r2 ainda estivessem vivos
}

Boas práticas

Sempre prefira referências imutáveis quando não precisar modificar o valor. Use referências mutáveis apenas quando necessário e mantenha o escopo delas o mais curto possível para evitar conflitos. Lembre-se de que o borrow checker é seu aliado: ele previne bugs em tempo de compilação.

Referências

Exercícios

  1. Escreva uma função que recebe uma referência imutável a uma String e retorna o número de caracteres (use chars().count()).

    ✓ Resposta:
    fn count_chars(s: &String) -> usize {
        s.chars().count()
    }
    
    fn main() {
        let s = String::from("hello");
        println!("{}", count_chars(&s));
    }
  2. Escreva uma função que recebe uma referência mutável a uma String e adiciona um ponto de exclamação no final.

    ✓ Resposta:
    fn add_exclamation(s: &mut String) {
        s.push_str("!");
    }
    
    fn main() {
        let mut s = String::from("hello");
        add_exclamation(&mut s);
        println!("{}", s);
    }
  3. O código abaixo compila? Se não, corrija-o para que compile, mantendo a intenção de imprimir o primeiro elemento e depois modificar o vetor.

    fn main() {
        let mut v = vec![1, 2, 3];
        let first = &v[0];
        v.push(4);
        println!("{}", first);
    }

    ✓ Resposta:Não compila. Uma solução é imprimir antes de modificar:
    fn main() {
        let mut v = vec![1, 2, 3];
        let first = &v[0];
        println!("{}", first);
        v.push(4);
    }
  4. O que há de errado com o código a seguir? Corrija-o.

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

    ✓ Resposta:Não pode ter referência imutável e mutável ao mesmo tempo. Corrigir removendo uma delas ou alterando a ordem:
    fn main() {
        let mut s = String::from("hello");
        let r1 = &s;
        println!("{}", r1);
        let r2 = &mut s;
        println!("{}", r2);
    }
  5. Escreva um programa que declara uma variável mutável do tipo i32, cria uma referência imutável para ela, imprime o valor, depois cria uma referência mutável, modifica o valor para 10 e imprime novamente.

    ✓ Resposta:
    fn main() {
        let mut x = 5;
        let ref_imm = &x;
        println!("{}", ref_imm);
        let ref_mut = &mut x;
        *ref_mut = 10;
        println!("{}", x);
    }