Um Exemplo de Programa Usando Structs
Para entender quando pode fazer sentido usar structs, vamos escrever um programa que calcula a área de um retângulo. Começaremos usando variáveis soltas e depois vamos refatorar o programa até chegarmos ao uso de structs.
Vamos criar um novo projeto binário com Cargo chamado rectangles, que receberá a largura e a altura de um retângulo, especificadas em pixels, e calculará sua área. A Listagem 5-8 mostra um pequeno programa com uma forma de fazer exatamente isso em 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
}
Agora, execute esse programa com cargo run:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
Esse código consegue descobrir a área do retângulo chamando a função area
com cada dimensão, mas ainda podemos fazer mais para que ele fique claro e
legível.
O problema com esse código fica evidente na assinatura de area:
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
}
A função area deveria calcular a área de um único retângulo, mas a função
que escrevemos recebe dois parâmetros, e em nenhum ponto do programa fica
claro que eles estão relacionados. Seria mais legível e mais fácil de manter
agrupar largura e altura. Já discutimos uma forma 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 do programa usando tuplas.
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
Em certo sentido, esse programa é melhor. As tuplas nos permitem adicionar um pouco de estrutura, e agora estamos passando apenas um argumento. Mas, por outro lado, essa versão é menos clara: tuplas não dão nome aos seus elementos, então precisamos acessar as partes por índice, o que torna o cálculo menos óbvio.
Confundir largura e altura não faria diferença para o cálculo da área, mas,
se quisermos desenhar o retângulo na tela, isso passaria a importar! Teríamos
de lembrar que width corresponde ao índice 0 da tupla e height ao índice
1. Isso seria ainda mais difícil para outra pessoa descobrir e manter em
mente ao usar nosso código. Como não expressamos o significado desses dados no
próprio código, fica mais fácil introduzir erros.
Refatorando com Structs
Usamos structs para adicionar significado ao rotular os dados. Podemos transformar a tupla que estamos usando em uma struct com um nome para o todo e nomes para cada parte, como mostra a Listagem 5-10.
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
RectangleAqui, definimos uma struct chamada Rectangle. Dentro das chaves, definimos os
campos width e height, ambos do tipo u32. Depois, em main, criamos uma
instância específica de Rectangle com largura 30 e altura 50.
Nossa função area agora é definida com um único parâmetro, que chamamos de
rectangle e cujo tipo é um empréstimo imutável de uma instância da struct
Rectangle. Como mencionamos no Capítulo 4, queremos tomar emprestada a
struct, em vez de assumir seu ownership. Assim, main continua com o
ownership e pode seguir usando rect1; é por isso que usamos & tanto na
assinatura da função quanto no ponto em que a chamamos.
A função area acessa os campos width e height da instância Rectangle.
Observe que acessar campos de uma instância de struct emprestada não move os
valores dos campos, e é por isso que você verá com frequência structs sendo
emprestadas. Nossa assinatura de area agora expressa exatamente o que
queremos dizer: calcular a área de um Rectangle usando seus campos width e
height. Isso deixa claro que largura e altura estão relacionadas e dá nomes
descritivos aos valores, em vez de usar os índices 0 e 1 de uma tupla. É
um ganho real de clareza.
Adicionando Funcionalidade com Traits Derivadas
Seria útil poder imprimir uma instância de Rectangle enquanto estivermos
depurando o programa, para enxergar os valores de todos os seus campos. A
Listagem 5-11 tenta fazer isso usando a macro println!, como já fizemos em capítulos anteriores. Mas isso não vai funcionar.
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1}");
}
RectangleQuando compilamos esse 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 informam ao println! que ele deve usar a formatação conhecida como
Display, isto é, uma saída pensada para consumo direto por usuários finais.
Os tipos primitivos que vimos até agora implementam Display por padrão,
porque existe basicamente uma única forma razoável de mostrar um 1, por
exemplo. Mas, com structs, a forma como println! deveria formatar a saída é
menos óbvia, porque há várias possibilidades: você quer vírgulas ou não?
Quer imprimir as chaves? Todos os campos devem aparecer? Por causa dessa
ambiguidade, o Rust não tenta adivinhar o que queremos, e structs não recebem
uma implementação padrão de Display para ser usada com println! e o
placeholder {}.
Se continuarmos lendo as mensagens de erro, encontraremos esta observação útil:
| |`Rectangle` cannot be formatted with the default formatter
| required by this formatting parameter
Vamos tentar! A chamada da macro println! agora ficará assim:
println!("rect1 is {rect1:?}");. Colocar o especificador :? dentro das
chaves informa ao println! que queremos usar um formato de saída chamado
Debug. A trait Debug nos permite imprimir a struct de uma maneira útil
para desenvolvedores, para que possamos inspecionar seu valor enquanto
depuramos o código.
Compile o código com essa mudança. Droga! Ainda recebemos um erro:
error[E0277]: `Rectangle` doesn't implement `Debug`
Mas, de novo, o compilador nos dá uma observação útil:
| required by this formatting parameter
|
O Rust de fato oferece funcionalidade para imprimir informações de depuração,
mas precisamos habilitá-la explicitamente para nossa struct. Para isso,
adicionamos o atributo externo #[derive(Debug)] logo antes da definição da
struct, como mostra a Listagem 5-12.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1:?}");
}
Debug e imprimindo a instância de Rectangle com formatação de depuraçãoAgora, quando executarmos o programa, não veremos mais erros e obteremos a seguinte saída:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }
Ótimo! Não é a saída mais bonita do mundo, mas ela mostra os valores de todos
os campos dessa instância, o que certamente ajuda durante a depuração. Quando
temos structs maiores, é útil contar com uma saída um pouco mais fácil de ler;
nesses casos, podemos usar {:#?} em vez de {:?} na string de println!.
Neste exemplo, usar o estilo {:#?} produz a seguinte saída:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}
Outra forma de imprimir um valor usando o formato Debug é com a macro
dbg!, que assume o ownership de uma expressão
(diferentemente de println!, que recebe uma referência), imprime o arquivo e
o número da linha em que a chamada à macro dbg! ocorre, junto com o valor
resultante da expressão, e então devolve o ownership desse valor.
Observação: a macro
dbg!imprime na saída de erro padrão (stderr), ao contrário deprintln!, que imprime na saída padrão (stdout). Vamos falar mais sobrestderrestdoutna seção “Redirecionando Erros para a Saída de Erro Padrão” do Capítulo 12.
Aqui está um exemplo em que nos interessa tanto o valor atribuído ao campo
width quanto o valor da struct inteira em rect1:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
Podemos colocar dbg! em volta da expressão 30 * scale e, como dbg!
devolve o ownership do valor da expressão, o campo width receberá exatamente
o mesmo valor que receberia se a chamada a dbg! não estivesse ali. Não
queremos que dbg! assuma o ownership de rect1, então usamos uma referência
a rect1 na chamada seguinte. A saída desse exemplo é assim:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
Podemos ver que a primeira parte da saída veio da linha 10 de src/main.rs,
onde estamos depurando a expressão 30 * scale, e que o valor resultante é
60 (a formatação Debug para inteiros imprime apenas o valor). A chamada a
dbg! na linha 14 de src/main.rs imprime o valor de &rect1, que é a
struct Rectangle. Essa saída usa a versão mais legível da formatação
Debug para o tipo Rectangle. A macro dbg! pode ser realmente útil quando
você está tentando entender o que seu código está fazendo.
Além da trait Debug, o Rust fornece várias outras traits que podemos usar com
o atributo derive para adicionar comportamentos úteis aos nossos tipos
personalizados. Essas traits e seus comportamentos estão listados no
Apêndice C. No Capítulo 10, veremos como implementar
essas traits com comportamento personalizado e também como criar suas próprias
traits. Há ainda muitos outros atributos além de derive; para mais
informações, veja a seção “Attributes” da Referência do
Rust.
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, já que ele não faz sentido para outros tipos. Vamos ver
como continuar refatorando esse código, transformando a função area em um
método area definido no tipo Rectangle.