Para entender quando podemos querer usar structs, vamos escrever um programa que calcula a área de um retângulo. Vamos começar com as variáveis individuais e em seguida, refazer o programa até usar structs em vez das variáveis.
Vamos fazer um novo projeto binário com Cargo, chamado retângulos que terá o comprimento e a largura do retângulo especificados em pixels e irá calcular a área do retângulo. A Lista 5-8 mostra um programa curto com uma maneira de fazer isso no nosso projeto src/main.rs:
Filename: src/main.rs
fn main() {
let length1 = 50;
let width1 = 30;
println!(
"The area of the rectangle is {} square pixels.",
area(length1, width1)
);
}
fn area(length: u32, width: u32) -> u32 {
length * width
}
Lista 5-8: Calcular a área de um retângulo especificado pelo seu comprimento e largura em variáveis separadas
Agora, executamos este programa usando cargo run
:
The area of the rectangle is 1500 square pixels.
Embora a lista 5-8 funcione e descubra a área do retângulo chamando a função
area
com cada dimensão, nós podemos fazer melhor. O comprimento e a largura
estão relacionados uns aos outros porque juntos eles descrevem um retângulo.
O problema com este método é evidente na assinatura de area
:
fn area(length: u32, width: u32) -> u32 {
A função area
supostamente calcula a área de um retângulo, mas a função
que escrevemos tem dois parâmetros. Os parâmetros estão relacionados, mas isso
não é indicado em qualquer lugar no nosso programa. Seria mais legível e
mais gerenciável agrupar comprimento e largura. Já discutimos uma maneira de
podermos fazer isso no Agrupamento de Valores em Tuplas, na seção do capítulo 3
na página XX: através do uso de tuplas. Lista 5-9 mostra outra versão do
nosso programa que usa tuplas:
Filename: src/main.rs
fn main() {
let rect1 = (50, 30);
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
Lista 5-8: Especificando o comprimento e a largura de um retangulo através de uma Tupla.
Em uma maneira, este programa é melhor. As tuplas deixam-nos adicionar um pouco de estrutura, e agora estamos passando apenas um argumento. Mas esta versão é menos clara: as tuplas não nomeiam os seus elementos, de modo que nosso cálculo tornou-se mais confuso porque temos de indexar as peças da tupla.
Não importa, para o cálculo da área, trocar-se comprimento e largura, mas
se queremos desenhar o retângulo na tela, já importa! Temos de ter em
mente que comprimento
é a tupla índice 0
e largura
é o tupla índice 1
.
Se alguém trabalhar com este código, terá de descobrir isso e mantê-lo em
mente. Seria fácil esquecer ou misturar estes valores e causar erros, porque
não se transmitiu o significado dos nossos dados no nosso código.
Usamos structs para dar significado aos dados usando rótulos. Podemos transformar a tupla que estamos usando em um tipo de dados, com um nome para o conjunto bem como nomes para as partes, como mostra a Lista 5-10:
Filename: src/main.rs
struct Rectangle {
length: u32,
width: u32,
}
fn main() {
let rect1 = Rectangle { length: 50, width: 30 };
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.length * rectangle.width
}
Lista 5-10: Definindo um struct Rectangle
(Rectangulo)
Aqui temos definido um struct denominado Rectangle
. Dentro das {}
definimos os campos como comprimento
e largura
, ambas do tipo u32
.
Em main
, criamos uma instância específica de um Rectangle
que tem um
comprimento de 50 e largura de 30.
A nossa função área
agora é definida com um parâmetro, que chamamos
rectangle
, cujo tipo é um empréstimo de uma instância da struct imutável
Rectangle
. Como mencionado no capítulo 4, queremos usar a struct, em vez de
tomar posse dela. Desta forma, main
mantém-se a sua proprietaria e pode
continuar a usar o rect1
, que é a razão para usar o &
na assinatura
da função e onde chamamos a função.
A função área
acessa os campos comprimento
e largura
da instância
Rectangle
. Nossa função assinatura para área
agora indica exatamente o que
queremos dizer: calcular a área de um Rectangle
usando os campos lenght
(comprimento) e width
(largura). Transmite que o comprimento e a largura são
relacionados uns aos outros, e dá nomes descritivos para os valores em vez de
usar a tupla de valores de índice 0
e 1
-uma vitória para uma maior clareza.
Seria útil para ser capaz de imprimir uma instância do Rectangle
enquanto
estamos depurando o nosso programa, a fim de consultar os valores para todos
os seus campos. Lista 5-11 usa a macro 'println!' como temos usado nos
capítulos anteriores:
Filename: src/main.rs
struct Rectangle {
length: u32,
width: u32,
}
fn main() {
let rect1 = Rectangle { length: 50, width: 30 };
println!("rect1 is {}", rect1);
}
Lista 5-11: Tentativa de impressão de uma instância
de Rectangle
Quando executamos este código, obtemos um erro com esta mensagem interna:
error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied
A macro 'println!' pode fazer muitos tipos de formatação, e por padrão,
{}
diz a println!
, para utilizar a formatação conhecida como Display
:
saída destinada para consumo do utilizador final. Os tipos primitivos que vimos
até agora implementam Display
por padrão, porque só há uma maneira que você
deseja mostrar um 1
ou qualquer outro tipo primitivo para um usuário. Mas com
Structs, a forma como println!
deve formatar a saída é menos clara, pois
existem mais possibilidades de exibição: você quer vírgulas ou não? Deseja
imprimir as chavetas {}
? Todos os campos devem ser mostrados? Devido a esta
ambiguidade, Rust não tenta adivinhar o que queremos e as structs não têm uma
implementação de Display
.
Se continuarmos a ler os erros, encontramos esta nota explicativa:
Avisa que `Rectangle` não pode ser formatado com o formato padrão;
Devemos tentar usar `:?` ao invés, se estivermos a usar um formato de string
Vamos tentar! A chamada da macro println!
agora vai ficar como
println!("rect1 is {:?}", rect1);
. Colocando o especificador :?
dentro
de {}
diz à println!
que nós queremos usar um formato de saída chamado
Debug
. Debug
é uma trait (característica) que nos permite imprimir as nossas
structs de uma maneira útil para os desenvolvedores para que possamos ver o seu
valor enquanto estamos a depurar do nosso código.
Executamos o código com esta mudança. Pô! Nós ainda obtemos um erro:
error: the trait bound `Rectangle: std::fmt::Debug` is not satisfied
Mas, novamente, o compilador dá-nos uma nota útil:
note: `Rectangle` cannot be formatted using `:?`; if it is defined in your
crate, add `#[derive(Debug)]` or manually implement it
nota: Rectangle
não pode ser formatado usando :?
; se estiver definido no
nosso crate, adicionamos #[derive(Debug)]
ou adicionamos manualmente.
Rust inclui funcionalidades para imprimir informações de depuração, mas
temos de inseri-la explicitamente para tornar essa funcionalidade disponível
para nossa struct. Para isso, adicionamos a anotação #[derive(Debug)]
pouco
antes da definição da struct, como mostrado na Lista 5-12:
Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
length: u32,
width: u32,
}
fn main() {
let rect1 = Rectangle { length: 50, width: 30 };
println!("rect1 is {:?}", rect1);
}
Lista 5-12: Adicionando a anotação para derivar
caracteristicaDebug
e imprimir a instância Rectangle
usando a formatação
debug
Agora, quando executamos o programa, não teremos quaisquer erros e vamos ver a seguinte informação:
rect1 is Rectangle { length: 50, width: 30 }
Boa! Não é o mais bonito, mas mostra os valores de todos os campos para essa
instância, que irá ajudar durante a depuração. Quando temos structs maiores,
é útil ter a informação um pouco mais fácil de ler; nesses casos, podemos usar
{:#?}
ao invés de {:?}
na frase println!
. Quando utilizamos o {:#?}
no
exemplo, vamos ver a informação como esta:
rect1 is Rectangle {
length: 50,
width: 30
}
Rust forneceu um número de caracteristicas (traits) para usarmos com a notação
derive
que pode adicionar um comportamento útil aos nossos tipos
personalizados. Esses traits e seus comportamentos estão listadas no Apêndice C.
Abordaremos como implementar estes traits com comportamento personalizado, bem
como a forma de criar as suas próprias traits (características) no Capítulo 10.
A nossa função area
é muito específica: ela apenas calcula a área de
retângulos. Seria útil fixar este comportamento à nossa struc Rectangle
,
porque não vai funcionar com qualquer outro tipo. Vejamos como podemos
continuar a refazer este código tornando a função area
em um método area
definido no nosso Rectangle
.