quarta-feira, 31 de março de 2021

Um programa de exemplo usando structs em Rust

Para entender quando podemos usar structs, vamos escrever um programa que calcule a área de um retângulo. Começaremos com variáveis ​​únicas e, em seguida, refatoraremos o programa até usarmos structs.

Vamos fazer um novo projeto binário com Cargo chamado retângulos que pegará a largura e a altura de um retângulo especificado 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 largura1 = 30;
    let altura1 = 50;

    println!(
        "A área do retângulo é de {} pixels quadrados.",
        area(largura1, altura1)
    );
}

fn area(largura: u32, altura: u32) -> u32 {
    largura * altura
}

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:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/structs`
A área do retângulo é de 1500 pixels quadrados.

Embora a Listagem 5-8 funcione e descubra a área do retângulo chamando a função area com cada dimensão, podemos fazer melhor. A largura e a altura estão relacionadas entre si porque, juntas, elas descrevem um retângulo.

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

fn main() {
    let largura1 = 30;
    let altura1 = 50;

    println!(
        "A área do retângulo é de {} pixels quadrados.",
        area(largura1, altura1)
    );
}

fn area(largura: u32, altura: u32) -> u32 {
    largura * altura
}

A função area deve calcular a área de um retângulo, mas a função que escrevemos tem dois parâmetros. Os parâmetros estão relacionados, mas não são expressos em nenhum lugar do nosso programa. Seria mais legível e gerenciável agrupar largura e altura. Já discutimos uma maneira de fazer isso na seção “O tipo tupla” do Capítulo 3: usando tuplas.

Refatorando com Tuplas

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

Nome do arquivo: src/main.rs

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

    println!(
        "A área do retângulo é de {} pixels quadrados.",
        area(retangulo1)
    );
}

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

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

Por um lado, este programa é melhor. As tuplas nos permitem adicionar um pouco de estrutura e agora estamos passando apenas um argumento. Mas, por outro lado, esta versão é menos clara: as tuplas não nomeiam seus elementos, então nosso cálculo se tornou mais confuso porque temos que indexar nas partes da tupla.

Não importa se misturamos largura e altura para o cálculo da área, mas se quisermos desenhar o retângulo na tela, isso faria diferença! Teríamos que ter em mente que largura é o índice da tupla 0 e altura é o índice da tupla 1. Se outra pessoa trabalhou neste código, ela teria que descobrir isso e mantê-lo em mente também. Seria fácil esquecer ou misturar esses valores e causar erros, porque não transmitimos o significado de nossos dados em nosso código.

Refatorando com Structs: Adicionando Mais Significado

Usamos estruturas para adicionar significado ao rotular os dados. Podemos transformar a tupla que estamos usando em um tipo de dados com um nome para o todo e também nomes para as partes, conforme mostrado na Listagem 5-10.

Nome do arquivo: src/main.rs

struct Retangulo {
    largura: u32,
    altura: u32,
}

fn main() {
    let retangulo1 = Retangulo {
        largura: 30,
        altura: 50,
    };

    println!(
        "A área do retângulo é de {} pixels quadrados.",
        area(&retangulo1)
    );
}

fn area(retangulo: &Retangulo) -> u32 {
    retangulo.largura * retangulo.altura
}

Listagem 5-10: Definindo uma estrutura Retangulo

Aqui, definimos uma estrutura e a nomeamos Retangulo. Dentro das chaves, definimos os campos como largura e altura, ambos com tipo u32. Em seguida em main, criamos uma instância específica de Retangulo que tem uma largura de 30 e uma altura de 50.

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

A função area acessa os campos largura e altura da instância Retangulo. Nossa assinatura da função area agora diz exatamente o que queremos dizer: calcular a área de Retangulo, usando seus campos largura e altura. Isso indica 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 de 0 e 1. Esta é uma vitória para maior clareza.

Adicionando Funcionalidade Útil com Características Derivadas

Seria bom poder imprimir uma instância de Retangulo 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. Isso não funcionará, no entanto.

Nome do arquivo: src/main.rs

Esse código não compila Esse código não compila.

struct Retangulo {
    largura: u32,
    altura: u32,
}

fn main() {
    let retangulo1 = Retangulo {
        largura: 30,
        altura: 50,
    };

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

Listagem 5-11: Tentativa de imprimir uma Retangulo instância

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

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

A macro println! pode fazer muitos tipos de formatação e, por padrão, as chaves indicam para println! 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 mostrar um 1 ou qualquer outro tipo primitivo para um usuário. Mas com structs, a forma como a saída de println! deve ser formatada é 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, Rust não tenta adivinhar o que queremos e as estruturas não têm uma implementação fornecida de Display.

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

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

Vamos tentar! A chamada da macro println! agora será semelhante a println!("retangulo1 is {:?}", retangulo1);. Colocar o especificador :? dentro das chaves indica para a macro println! que queremos usar um formato de saída chamado Debug. A característica Debug nos permite imprimir nossa estrutura de uma forma que seja útil para desenvolvedores, para que possamos ver seu valor enquanto estamos depurando nosso código.

Compile o código com esta mudança. Droga! Ainda recebemos um erro:

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

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

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

Rust faz incluir a funcionalidade para imprimir informações de depuração, mas nós temos que explicitamente fazer essa funcionalidade disponível para a nossa struct. Para fazer isso, adicionamos a anotação #[derive(Debug)] logo antes da definição da estrutura, conforme mostrado na Listagem 5-12.

Nome do arquivo: src/main.rs

#[derive(Debug)]
struct Retangulo {
    largura: u32,
    altura: u32,
}

fn main() {
    let retangulo1 = Retangulo {
        largura: 30,
        altura: 50,
    };

    println!("retangulo1 é {:?}", retangulo1);
}

Listagem 5-12: Adicionar a anotação para derivar o traço Debug e imprimir a instância Retangulo usando formatação de depuração

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

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/structs`
retangulo1 é Retangulo { largura: 30, altura: 50 }

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

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/structs`
retangulo1 é Retangulo {
    largura: 30,
    altura: 50,
}

Rust forneceu uma série de características para usarmos com a anotação derive que podem adicionar um comportamento útil aos nossos tipos personalizados. Essas características e seus comportamentos estão listados no Apêndice C. Cobriremos como implementar essas características com comportamento personalizado e também como criar suas próprias características no Capítulo 10.

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

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

0 comentários:

Postar um comentário