rustingcrab

Rusting with style - Curso básico de linguagem Rust

Descrição da imagem

Cleuton Sampaio

Veja no GitHub

Menu do curso

VÍDEO DESTA AULA

Borrow check hell

“Borrow Check Hell” é uma expressão utilizada pela comunidade Rust para descrever a frustração que muitos desenvolvedores enfrentam ao lidar com as rigorosas regras do borrow checker do Rust. O borrow checker é uma parte fundamental do compilador Rust que assegura a segurança de memória, garantindo que não haja referências inválidas ou concorrências inseguras no código. No entanto, essas mesmas regras podem, às vezes, tornar o desenvolvimento mais desafiador, especialmente para quem está começando ou para projetos mais complexos.

Essa “agonia” ocorre principalmente quando o borrow checker impede que certas operações sejam realizadas porque elas violam as regras de propriedade e empréstimo do Rust. Por exemplo, tentar modificar uma estrutura de dados enquanto ainda existem referências imutáveis a ela pode resultar em erros que, embora importantes para a segurança, podem ser difíceis de resolver inicialmente. Esses erros costumam ser acompanhados de mensagens de compilação detalhadas, mas que nem sempre são fáceis de entender, aumentando a sensação de “cegueira” para o desenvolvedor.

Para mitigar o “Borrow Check Hell”, é essencial entender profundamente como o sistema de propriedade e empréstimo do Rust funciona. Reestruturar o código para reduzir a complexidade das referências, limitar o escopo das variáveis e utilizar ferramentas como RefCell ou Rc quando apropriado são estratégias eficazes. Além disso, com a prática e a experiência, os desenvolvedores aprendem a antecipar e evitar esses conflitos, tornando o processo de desenvolvimento mais fluido e menos frustrante. Em suma, embora o “Borrow Check Hell” represente um desafio inicial, ele é um reflexo das poderosas garantias de segurança que Rust oferece, contribuindo para a criação de softwares mais robustos e confiáveis.

Vamos explicar os conceitos de movimento, propriedade e empréstimo em Rust de forma simples e resumida, além de abordar como eles se aplicam a tipos primitivos.

Propriedade (Ownership)

Movimento (Move)

Empréstimo (Borrowing)

Tipos Primitivos e Copy

Entender esses conceitos é fundamental para escrever código seguro e eficiente em Rust, aproveitando ao máximo seu sistema de propriedade e gerenciamento de memória.

Regras de propriedade e empréstimo

Resumir de maneira simples as principais regras de propriedade (ownership) e empréstimo (borrowing) em Rust, acompanhadas de exemplos para facilitar a compreensão.

Regras de Propriedade (Ownership)

  1. Cada valor tem um único proprietário.
  2. Só pode haver um proprietário de cada vez.
  3. Quando o proprietário sai de escopo, o valor é descartado (drop).

1. Cada valor tem um único proprietário

Cada valor em Rust é “possuído” por uma única variável. Essa variável é responsável por gerenciar a memória do valor.

Exemplo:

fn main() {
    let s = String::from("Olá, Rust!");
    // Aqui, `s` é o proprietário da String.
}

2. Só pode haver um proprietário por vez

Quando você atribui um valor de uma variável para outra, a propriedade é transferida (movida) para a nova variável. A variável original deixa de ser válida.

Exemplo:

fn main() {
    let s1 = String::from("Olá");
    let s2 = s1; // Movimento: s1 deixa de ser válido e s2 passa a ser o proprietário.

    // println!("{}", s1); // Erro! `s1` não é mais válido.
    println!("{}", s2); // Funciona, `s2` é o novo proprietário.
}

3. Quando o proprietário sai de escopo, o valor é descartado

Quando a variável que possui um valor sai do escopo (termina sua execução), Rust automaticamente limpa a memória desse valor.

Exemplo:

fn main() {
    {
        let s = String::from("Desaparecendo");
        // `s` é válido dentro deste bloco.
    } // `s` sai de escopo aqui e a memória é liberada.

    // println!("{}", s); // Erro! `s` não existe mais.
}

Regras de Empréstimo (Borrowing)

  1. Você pode ter múltiplas referências imutáveis ou uma única referência mutável, mas não ambas simultaneamente.
  2. Referências devem sempre ser válidas.

1. Múltiplas referências imutáveis ou uma única referência mutável

2. Referências devem sempre ser válidas

As referências devem apontar para valores que ainda estão válidos. Rust garante isso durante a compilação para evitar referências pendentes (dangling references).

Exemplo com Erro:

fn main() {
    let r;
    {
        let s = String::from("Desaparecendo");
        r = &s;
    } // `s` sai de escopo aqui.
    println!("{}", r); // Erro! `s` não existe mais.
}

Erro de Compilação:

error[E0597]: `s` does not live long enough
 --> src/main.rs:6:13
  |
5 |         let s = String::from("Desaparecendo");
  |             - `s` declared here
6 |         r = &s;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `s` dropped here while still borrowed
8 |     println!("{}", r);
  |                    - borrow later used here

Corrigindo o erro:

fn main() {
    let r;
    {
        let s = String::from("Desaparecendo");
        r = s;
    } // `s` sai de escopo aqui.
    println!("{}", r); 
}

Agora não dá erro porque estamos movendo o string apontado por “s” para “r”. Mesmo “s” saindo de escopo, o string continua sendo de propriedade de “r”.

Entendendo Copy e Clone

Vamos explorar os traits Copy e Clone em Rust utilizando uma struct simples. Explicarei de maneira clara e com exemplos para facilitar o entendimento.

O que são Traits Copy e Clone?**

Trait Copy

Trait Clone

Diferença Entre Copy e Clone**

Exemplo Prático com uma Struct Simples

Vamos criar uma struct chamada Ponto que representa um ponto 2D com coordenadas x e y.

Implementando Copy e Clone

#[derive(Debug, Copy, Clone)]
struct Ponto {
    x: i32,
    y: i32,
}

fn main() {
    let ponto1 = Ponto { x: 10, y: 20 };
    
    // Usando `Copy`
    let ponto2 = ponto1; // `ponto1` ainda é válido porque `Ponto` implementa `Copy`
    
    println!("ponto1: {:?}", ponto1);
    println!("ponto2: {:?}", ponto2);
    
    // Usando `Clone`
    let ponto3 = ponto1.clone(); // Cria uma cópia explícita de `ponto1`
    
    println!("ponto3: {:?}", ponto3);
}

Explicação do Código

  1. Derivando Copy e Clone:
    #[derive(Debug, Copy, Clone)]
    struct Ponto {
        x: i32,
        y: i32,
    }
    
    • Usamos #[derive(Debug, Copy, Clone)] para automaticamente implementar os traits Copy e Clone para a struct Ponto.
    • Como i32 implementa Copy, a struct Ponto também pode implementar Copy.
  2. Usando Copy:
    let ponto2 = ponto1; // Cópia automática
    
    • Atribuição de ponto1 para ponto2 cria uma cópia completa de ponto1.
    • Ambas as variáveis (ponto1 e ponto2) são válidas e independentes.
  3. Usando Clone:
    let ponto3 = ponto1.clone(); // Cópia explícita
    
    • Chama o método .clone() para criar uma cópia de ponto1.
    • Útil quando você quer deixar claro que está criando uma nova instância.
  4. Imprimindo os Pontos:
    println!("ponto1: {:?}", ponto1);
    println!("ponto2: {:?}", ponto2);
    println!("ponto3: {:?}", ponto3);
    
    • Usa o trait Debug para imprimir os valores das structs.

Saída do Programa:

ponto1: Ponto { x: 10, y: 20 }
ponto2: Ponto { x: 10, y: 20 }
ponto3: Ponto { x: 10, y: 20 }

Quando Usar Copy ou Clone?

Use Copy Quando:

Use Clone Quando:

Exemplo com Tipo que Não Implementa Copy

Vamos ver o que acontece quando tentamos usar Copy com um tipo que não o implementa, como String.

#[derive(Debug, Clone)]
struct Pessoa {
    nome: String,
    idade: u32,
}

fn main() {
    let pessoa1 = Pessoa {
        nome: String::from("Alice"),
        idade: 30,
    };
    
    // let pessoa2 = pessoa1; // ERRO! Movimento: `pessoa1` não é mais válido
    let pessoa2 = pessoa1.clone(); // Cópia explícita
    
    println!("pessoa1: {:?}", pessoa1);
    println!("pessoa2: {:?}", pessoa2);
}

Explicação:

Saída do Programa:

pessoa1: Pessoa { nome: "Alice", idade: 30 }
pessoa2: Pessoa { nome: "Alice", idade: 30 }