The Rust Programming Language
Read original on doc.rust-lang.orgAn Example Program Using Structs
To understand when we might want to use structs, letâs write a program that calculates the area of a rectangle. Weâll start by using single variables and then refactor the program until weâre using structs instead.
Letâs make a new binary project with Cargo called rectangles that will take the width and height of a rectangle specified in pixels and calculate the area of the rectangle. Listing 5-8 shows a short program with one way of doing exactly that in our projectâs src/main.rs.
Now, run this program using 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.
This code succeeds in figuring out the area of the rectangle by calling the
area function with each dimension, but we can do more to make this code clear
and readable.
The issue with this code is evident in the signature of 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
}
The area function is supposed to calculate the area of one rectangle, but the
function we wrote has two parameters, and itâs not clear anywhere in our
program that the parameters are related. It would be more readable and more
manageable to group width and height together. Weâve already discussed one way
we might do that in âThe Tuple Typeâ section
of Chapter 3: by using tuples.
Refactoring with Tuples
Listing 5-9 shows another version of our program that uses tuples.
In one way, this program is better. Tuples let us add a bit of structure, and weâre now passing just one argument. But in another way, this version is less clear: Tuples donât name their elements, so we have to index into the parts of the tuple, making our calculation less obvious.
Mixing up the width and height wouldnât matter for the area calculation, but if
we want to draw the rectangle on the screen, it would matter! We would have to
keep in mind that width is the tuple index 0 and height is the tuple
index 1. This would be even harder for someone else to figure out and keep in
mind if they were to use our code. Because we havenât conveyed the meaning of
our data in our code, itâs now easier to introduce errors.
Refactoring with Structs
We use structs to add meaning by labeling the data. We can transform the tuple weâre using into a struct with a name for the whole as well as names for the parts, as shown in Listing 5-10.
Here, weâve defined a struct and named it Rectangle. Inside the curly
brackets, we defined the fields as width and height, both of which have
type u32. Then, in main, we created a particular instance of Rectangle
that has a width of 30 and a height of 50.
Our area function is now defined with one parameter, which weâve named
rectangle, whose type is an immutable borrow of a struct Rectangle
instance. As mentioned in Chapter 4, we want to borrow the struct rather than
take ownership of it. This way, main retains its ownership and can continue
using rect1, which is the reason we use the & in the function signature and
where we call the function.
The area function accesses the width and height fields of the Rectangle
instance (note that accessing fields of a borrowed struct instance does not
move the field values, which is why you often see borrows of structs). Our
function signature for area now says exactly what we mean: Calculate the area
of Rectangle, using its width and height fields. This conveys that the
width and height are related to each other, and it gives descriptive names to
the values rather than using the tuple index values of 0 and 1. This is a
win for clarity.
Adding Functionality with Derived Traits
Itâd be useful to be able to print an instance of Rectangle while weâre
debugging our program and see the values for all its fields. Listing 5-11 tries
using the println! macro as we have used in
previous chapters. This wonât work, however.
When we compile this code, we get an error with this core message:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
The println! macro can do many kinds of formatting, and by default, the curly
brackets tell println! to use formatting known as Display: output intended
for direct end user consumption. The primitive types weâve seen so far
implement Display by default because thereâs only one way youâd want to show
a 1 or any other primitive type to a user. But with structs, the way
println! should format the output is less clear because there are more
display possibilities: Do you want commas or not? Do you want to print the
curly brackets? Should all the fields be shown? Due to this ambiguity, Rust
doesnât try to guess what we want, and structs donât have a provided
implementation of Display to use with println! and the {} placeholder.
If we continue reading the errors, weâll find this helpful note:
= 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
Letâs try it! The println! macro call will now look like println!("rect1 is {rect1:?}");. Putting the specifier :? inside the curly brackets tells
println! we want to use an output format called Debug. The Debug trait
enables us to print our struct in a way that is useful for developers so that
we can see its value while weâre debugging our code.
Compile the code with this change. Drat! We still get an error:
error[E0277]: `Rectangle` doesn't implement `Debug`
But again, the compiler gives us a helpful note:
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
Rust does include functionality to print out debugging information, but we
have to explicitly opt in to make that functionality available for our struct.
To do that, we add the outer attribute #[derive(Debug)] just before the
struct definition, as shown in Listing 5-12.
Now when we run the program, we wonât get any errors, and weâll see the following output:
$ 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 }
Nice! Itâs not the prettiest output, but it shows the values of all the fields
for this instance, which would definitely help during debugging. When we have
larger structs, itâs useful to have output thatâs a bit easier to read; in
those cases, we can use {:#?} instead of {:?} in the println! string. In
this example, using the {:#?} style will output the following:
$ 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,
}
Another way to print out a value using the Debug format is to use the dbg!
macro, which takes ownership of an expression (as opposed
to println!, which takes a reference), prints the file and line number of
where that dbg! macro call occurs in your code along with the resultant value
of that expression, and returns ownership of the value.
Hereâs an example where weâre interested in the value that gets assigned to the
width field, as well as the value of the whole struct in 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);
}
We can put dbg! around the expression 30 * scale and, because dbg!
returns ownership of the expressionâs value, the width field will get the
same value as if we didnât have the dbg! call there. We donât want dbg! to
take ownership of rect1, so we use a reference to rect1 in the next call.
Hereâs what the output of this example looks like:
$ 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,
}
We can see the first bit of output came from src/main.rs line 10 where weâre
debugging the expression 30 * scale, and its resultant value is 60 (the
Debug formatting implemented for integers is to print only their value). The
dbg! call on line 14 of src/main.rs outputs the value of &rect1, which is
the Rectangle struct. This output uses the pretty Debug formatting of the
Rectangle type. The dbg! macro can be really helpful when youâre trying to
figure out what your code is doing!
In addition to the Debug trait, Rust has provided a number of traits for us
to use with the derive attribute that can add useful behavior to our custom
types. Those traits and their behaviors are listed in Appendix C. Weâll cover how to implement these traits with custom behavior as
well as how to create your own traits in Chapter 10. There are also many
attributes other than derive; for more information, see the âAttributesâ
section of the Rust Reference.
Our area function is very specific: It only computes the area of rectangles.
It would be helpful to tie this behavior more closely to our Rectangle struct
because it wonât work with any other type. Letâs look at how we can continue to
refactor this code by turning the area function into an area method
defined on our Rectangle type.