Um Programa de Exemplo Usando Structs

Beginner

This tutorial is from open-source community. Access the source code

Introdução

Bem-vindo(a) a Um Programa de Exemplo Usando Structs. Este laboratório faz parte do Livro de Rust. Você pode praticar suas habilidades em Rust no LabEx.

Neste laboratório, escreveremos um programa usando structs para calcular a área de um retângulo, refatorando o código inicial que usava variáveis separadas para largura e altura.

Um Programa de Exemplo Usando Structs

Para entender quando podemos querer usar structs, vamos escrever um programa que calcula a área de um retângulo. Começaremos usando variáveis individuais e, em seguida, refatoraremos o programa até usarmos structs.

Vamos criar um novo projeto binário com o Cargo chamado rectangles que receberá a largura e a altura de um retângulo especificados em pixels e calculará a área do retângulo. A Listagem 5-8 mostra um programa curto com uma maneira de fazer exatamente isso no src/main.rs do nosso projeto.

Nome do arquivo: src/main.rs

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

Listagem 5-8: Calculando a área de um retângulo especificado por variáveis separadas de largura e altura

Agora, execute este programa usando cargo run:

The area of the rectangle is 1500 square pixels.

Este código consegue descobrir a área do retângulo chamando a função area com cada dimensão, mas podemos fazer mais para tornar este código claro e legível.

O problema com este código é evidente na assinatura de area:

fn area(width: u32, height: u32) -> u32 {

A função area deve calcular a área de um retângulo, mas a função que escrevemos tem dois parâmetros, e não está claro em nenhum lugar do nosso programa que os parâmetros estão relacionados. Seria mais legível e mais gerenciável agrupar largura e altura. Já discutimos uma maneira de fazer isso em "O Tipo Tupla": usando tuplas.

Refatorando com Tuplas

A Listagem 5-9 mostra outra versão do nosso programa que usa tuplas.

Nome do arquivo: src/main.rs

fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
      1 area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
  2 dimensions.0 * dimensions.1
}

Listagem 5-9: Especificando a largura e a altura do retângulo com uma tupla

De certa forma, este programa é melhor. As tuplas nos permitem adicionar um pouco de estrutura, e agora estamos passando apenas um argumento [1]. Mas, de outra forma, esta versão é menos clara: as tuplas não nomeiam seus elementos, então temos que indexar as partes da tupla [2], tornando nosso cálculo menos óbvio.

Trocar a largura e a altura não faria diferença para o cálculo da área, mas se quisermos desenhar o retângulo na tela, faria! Teríamos que ter em mente que largura é o índice da tupla 0 e altura é o índice da tupla 1. Isso seria ainda mais difícil para outra pessoa descobrir e ter em mente se fosse usar nosso código. Como não transmitimos o significado de nossos dados em nosso código, agora é mais fácil introduzir erros.

Refatorando com Structs: Adicionando Mais Significado

Usamos structs para adicionar significado rotulando os dados. Podemos transformar a tupla que estamos usando em uma struct com um nome para o todo, bem como nomes para as partes, conforme mostrado na Listagem 5-10.

Nome do arquivo: src/main.rs

1 struct Rectangle {
  2 width: u32,
    height: u32,
}

fn main() {
  3 let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

4 fn area(rectangle: &Rectangle) -> u32 {
  5 rectangle.width * rectangle.height
}

Listagem 5-10: Definindo uma struct Rectangle

Aqui, definimos uma struct e a nomeamos Rectangle [1]. Dentro das chaves, definimos os campos como width e height, ambos do tipo u32 [2]. Então, em main, criamos uma instância específica de Rectangle que tem uma largura de 30 e uma altura de 50 [3].

Nossa função area agora é definida com um parâmetro, que nomeamos rectangle, cujo tipo é um empréstimo imutável de uma instância da struct Rectangle [4]. Como mencionado no Capítulo 4, queremos emprestar a struct em vez de assumir a propriedade dela. Dessa forma, main retém sua propriedade e pode continuar usando rect1, que é a razão pela qual usamos o & na assinatura da função e onde chamamos a função.

A função area acessa os campos width e height da instância Rectangle [5] (observe que acessar campos de uma instância de struct emprestada não move os valores dos campos, e é por isso que você frequentemente vê empréstimos de structs). Nossa assinatura de função para area agora diz exatamente o que queremos dizer: calcular a área de Rectangle, usando seus campos width e height. Isso transmite que a largura e a altura estão relacionadas entre si e dá nomes descritivos aos valores em vez de usar os valores de índice de tupla 0 e 1. Isso é uma vitória para a clareza.

Adicionando Funcionalidade Útil com Traits Derivados

Seria útil poder imprimir uma instância de Rectangle enquanto estamos depurando nosso programa e ver os valores de todos os seus campos. A Listagem 5-11 tenta usar a macro println! como usamos nos capítulos anteriores. No entanto, isso não funcionará.

Nome do arquivo: src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {}", rect1);
}

Listagem 5-11: Tentando imprimir uma instância de Rectangle

Quando compilamos este código, recebemos um erro com esta mensagem principal:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

A macro println! pode fazer muitos tipos de formatação e, por padrão, as chaves dizem ao println! para usar a formatação conhecida como Display: saída destinada ao consumo direto do usuário final. Os tipos primitivos que vimos até agora implementam Display por padrão porque há apenas uma maneira de você querer mostrar um 1 ou qualquer outro tipo primitivo a um usuário. Mas com structs, a maneira como println! deve formatar a saída é menos clara porque há mais possibilidades de exibição: Você quer vírgulas ou não? Você quer imprimir as chaves? Todos os campos devem ser mostrados? Devido a essa ambiguidade, o Rust não tenta adivinhar o que queremos, e as structs não têm uma implementação fornecida de Display para usar com println! e o espaço reservado {}.

Se continuarmos lendo os erros, encontraremos esta nota útil:

= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for
pretty-print) instead

Vamos tentar! A chamada da macro println! agora se parecerá com println!("rect1 is {:?}", rect1);. Colocar o especificador :? dentro das chaves diz ao println! que queremos usar um formato de saída chamado Debug. O trait Debug nos permite imprimir nossa struct de uma forma que seja útil para os desenvolvedores, para que possamos ver seu valor enquanto estamos depurando nosso código.

Compile o código com essa alteração. Droga! Ainda recebemos um erro:

error[E0277]: `Rectangle` doesn't implement `Debug`

Mas, novamente, o compilador nos dá uma nota útil:

= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` or manually implement `Debug`

Rust inclui funcionalidade para imprimir informações de depuração, mas temos que optar explicitamente por tornar essa funcionalidade disponível para nossa struct. Para fazer isso, adicionamos o atributo externo #[derive(Debug)] logo antes da definição da struct, conforme mostrado na Listagem 5-12.

Nome do arquivo: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:?}", rect1);
}

Listagem 5-12: Adicionando o atributo para derivar o trait Debug e imprimindo a instância Rectangle usando a formatação de depuração

Agora, quando executamos o programa, não receberemos nenhum erro e veremos a seguinte saída:

rect1 is Rectangle { width: 30, height: 50 }

Legal! Não é a saída mais bonita, mas mostra os valores de todos os campos para esta instância, o que definitivamente ajudaria durante a depuração. Quando temos structs maiores, é útil ter uma saída que seja um pouco mais fácil de ler; nesses casos, podemos usar {:#?} em vez de {:?} na string println!. Neste exemplo, usar o estilo {:#?} produzirá a seguinte saída:

rect1 is Rectangle {
    width: 30,
    height: 50,
}

Outra maneira de imprimir um valor usando o formato Debug é usar a macro dbg!, que assume a propriedade de uma expressão (em oposição a println!, que assume uma referência), imprime o arquivo e o número da linha de onde essa chamada de macro dbg! ocorre em seu código, juntamente com o valor resultante dessa expressão, e retorna a propriedade do valor.

Nota: Chamar a macro dbg! imprime no fluxo de console de erro padrão (stderr), em oposição a println!, que imprime no fluxo de console de saída padrão (stdout). Falaremos mais sobre stderr e stdout em "Escrevendo Mensagens de Erro para o Erro Padrão em Vez da Saída Padrão".

Aqui está um exemplo em que estamos interessados no valor que é atribuído ao campo width, bem como no valor da struct inteira em rect1:

Nome do arquivo: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
      1 width: dbg!(30 * scale),
        height: 50,
    };

  2 dbg!(&rect1);
}

Podemos colocar dbg! em torno da expressão 30 * scale [1] e, como dbg! retorna a propriedade do valor da expressão, o campo width receberá o mesmo valor que se não tivéssemos a chamada dbg! lá. Não queremos que dbg! assuma a propriedade de rect1, então usamos uma referência a rect1 na próxima chamada [2]. Veja como a saída deste exemplo se parece:

[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

Podemos ver que a primeira parte da saída veio de [1], onde estamos depurando a expressão 30 * scale, e seu valor resultante é 60 (a formatação Debug implementada para inteiros é imprimir apenas seu valor). A chamada dbg! em [2] produz o valor de &rect1, que é a struct Rectangle. Essa saída usa a formatação Debug bonita do tipo Rectangle. A macro dbg! pode ser realmente útil quando você está tentando descobrir o que seu código está fazendo!

Além do trait Debug, o Rust forneceu vários traits para usarmos com o atributo derive que podem adicionar comportamento útil aos nossos tipos personalizados. Esses traits e seus comportamentos estão listados no Apêndice C. Abordaremos como implementar esses traits com comportamento personalizado, bem como como criar seus próprios traits no Capítulo 10. Também existem muitos atributos além de derive; para obter mais informações, consulte a seção "Atributos" da Referência do Rust em https://doc.rust-lang.org/reference/attributes.html.

Nossa função area é muito específica: ela calcula apenas a área de retângulos. Seria útil vincular esse comportamento mais de perto à nossa struct Rectangle porque ela não funcionará com nenhum outro tipo. Vamos ver como podemos continuar a refatorar este código transformando a função area em um método area definido em nosso tipo Rectangle.

Resumo

Parabéns! Você concluiu o laboratório de Um Programa de Exemplo Usando Structs. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.