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
}
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
:
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
}
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
}
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.
struct Retangulo {
largura: u32,
altura: u32,
}
fn main() {
let retangulo1 = Retangulo {
largura: 30,
altura: 50,
};
println!("retangulo1 is {}", retangulo1);
}
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);
}
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.