Rust Structs by Example
The previous chapter covered the syntax of a struct: how to define one, create one, change one, and print one.
This one covers something harder to teach and more useful to have — the instinct for when to reach for one.
We are going to write one small program three times. Same output every single time. What changes is how much the compiler understands about what you meant, and by the third version it understands enough to catch a bug you would otherwise ship.
Everything here builds on Structs, Ownership and References and Borrowing. No new rules — just a good reason to use the ones you already know.
The Program
Here is the job: given a screen’s width and height in pixels, work out how many pixels it has in total.
A 1920 by 1080 laptop screen has 2,073,600 of them. That is the number we want, and it never changes across all three versions.
Version 1: Two Loose Numbers
The obvious first attempt:
fn main() {
let width_px = 1920;
let height_px = 1080;
println!("{} x {}", width_px, height_px);
println!("total pixels: {}", pixel_count(width_px, height_px));
}
fn pixel_count(width_px: u32, height_px: u32) -> u32 {
width_px * height_px
}Output:
1920 x 1080
total pixels: 2073600It works. Ship it?
Look at the signature on its own, the way another developer will:
fn pixel_count(width_px: u32, height_px: u32) -> u32Two u32 values. Nothing in the type system says they describe one screen. As far as the compiler is concerned you could pass it a font size and a port number.
The Bug That Compiles
Now suppose the screen is a phone held upright — 1080 wide, 1920 tall — and someone swaps the two arguments by accident:
fn main() {
let width_px = 1080;
let height_px = 1920;
println!("pixels: {}", pixel_count(height_px, width_px));
println!("ratio: {:.2}", aspect_ratio(height_px, width_px));
}
fn pixel_count(width_px: u32, height_px: u32) -> u32 {
width_px * height_px
}
fn aspect_ratio(width_px: u32, height_px: u32) -> f64 {
width_px as f64 / height_px as f64
}Output:
pixels: 2073600
ratio: 1.78Read that carefully. pixel_count gives the right answer, because multiplication does not care about order. So the mistake leaves no trace there at all.
aspect_ratio gives 1.78, which is a widescreen TV. The real answer for a phone in portrait is 0.56. The program now confidently reports that a tall phone is a wide monitor — and it compiled without a single warning.
Two parameters of the same type sitting next to each other are a trap. The compiler will never catch a swap, because as far as it knows there is nothing to swap — just two numbers that happen to be adjacent.
Version 2: A Tuple
A tuple is the first honest improvement. It keeps the two numbers together as one value:
fn main() {
let laptop = (1920, 1080);
println!("{} x {}", laptop.0, laptop.1);
println!("total pixels: {}", pixel_count(laptop));
}
fn pixel_count(screen: (u32, u32)) -> u32 {
screen.0 * screen.1
}Output:
1920 x 1080
total pixels: 2073600Genuinely better. One argument instead of two, so it is now impossible to pass a width without a height. The pair travels through the program as a unit.
But two things still bother me.
First, .0 and .1 mean nothing. In screen.0 * screen.1 you cannot tell which is which, and you do not need to — multiplication hides it. The moment you write a function where order does matter, you are back to remembering a convention that is written down nowhere.
Second, (u32, u32) is an extremely popular type. A cursor position is a (u32, u32). A grid cell is a (u32, u32). A date range is a (u32, u32). All of them fit through this function without complaint:
fn main() {
let cursor = (40, 12);
println!("total pixels: {}", pixel_count(cursor));
}
fn pixel_count(screen: (u32, u32)) -> u32 {
screen.0 * screen.1
}Output:
total pixels: 480Complete nonsense, delivered with total confidence. The tuple bundled the values but never gave them an identity.
Version 3: A Struct
Now give the shape a name and give each field a name:
struct Screen {
width_px: u32,
height_px: u32,
}
fn main() {
let laptop = Screen {
width_px: 1920,
height_px: 1080,
};
println!("{} x {}", laptop.width_px, laptop.height_px);
println!("total pixels: {}", pixel_count(&laptop));
println!("aspect ratio: {:.2}", aspect_ratio(&laptop));
println!("laptop is still mine: {}px wide", laptop.width_px);
}
fn pixel_count(screen: &Screen) -> u32 {
screen.width_px * screen.height_px
}
fn aspect_ratio(screen: &Screen) -> f64 {
screen.width_px as f64 / screen.height_px as f64
}Output:
1920 x 1080
total pixels: 2073600
aspect ratio: 1.78
laptop is still mine: 1920px wideSame number as version 1. Everything else improved:
- The signature is documentation now.
fn pixel_count(screen: &Screen) -> u32tells you what goes in, what comes out, and that nothing is consumed. No comment needed. - The body says what it does.
screen.width_px * screen.height_pxneeds no lookup table. &Screenis a real fence. A cursor position does not fit through it. Neither does a font size. Only aScreendoes.- The caller keeps the screen.
&laptopborrows, solaptopis still usable on the next line.
The answer never changes. What changes is how much the signature tells you.
The Fence Is Real
To see that last point in action, define a second struct with identical fields and try to sneak it through:
struct Screen {
width_px: u32,
height_px: u32,
}
struct Cursor {
width_px: u32,
height_px: u32,
}
fn main() {
let cursor = Cursor { width_px: 40, height_px: 12 };
println!("total pixels: {}", pixel_count(&cursor));
}
fn pixel_count(screen: &Screen) -> u32 {
screen.width_px * screen.height_px
}error[E0308]: mismatched types
--> src/main.rs:14:46
|
14 | println!("total pixels: {}", pixel_count(&cursor));
| ----------- ^^^^^^^ expected `&Screen`, found `&Cursor`
| |
| arguments to this function are incorrect
|
= note: expected reference `&Screen`
found reference `&Cursor`Same fields, same memory layout, same everything — and still rejected. A struct definition creates a brand new type, not an alias for its contents. That is the whole difference between version 2 and version 3, and it costs nothing at runtime.
Why &Screen and Not Screen
Worth pausing on, because it is the single most common beginner mistake with structs.
Drop the & and the function takes the screen by value, which means it takes ownership:
struct Screen {
width_px: u32,
height_px: u32,
}
fn main() {
let laptop = Screen {
width_px: 1920,
height_px: 1080,
};
println!("total pixels: {}", pixel_count(laptop));
println!("{}px wide", laptop.width_px);
}
fn pixel_count(screen: Screen) -> u32 {
screen.width_px * screen.height_px
}error[E0382]: borrow of moved value: `laptop`
--> src/main.rs:13:27
|
7 | let laptop = Screen {
| ------ move occurs because `laptop` has type `Screen`, which does not implement the `Copy` trait
...
12 | println!("total pixels: {}", pixel_count(laptop));
| ------ value moved here
13 | println!("{}px wide", laptop.width_px);
| ^^^^^^^^^^^^^^^ value borrowed here after move
|
note: consider changing this parameter type in function `pixel_count` to borrow instead if owning the value isn't necessary
--> src/main.rs:16:24
|
16 | fn pixel_count(screen: Screen) -> u32 {
| ----------- ^^^^^^ this parameter takes ownership of the valueRead the compiler’s note — it has already worked out the fix for you. pixel_count only reads the screen, so it has no business owning it.
If a function only looks at a value, borrow it. Take it by value only when the function is meant to keep it.
Notice there is no * anywhere in screen.width_px, even though screen is a reference. Rust dereferences automatically when you use ., so working through a &Screen reads exactly like working through a Screen.
Printing What You Built
You now have a nice type. The first thing anybody wants to do is look inside it while debugging — and the first attempt does not work:
struct Screen {
width_px: u32,
height_px: u32,
}
fn main() {
let laptop = Screen { width_px: 1920, height_px: 1080 };
println!("{}", laptop);
}error[E0277]: `Screen` doesn't implement `std::fmt::Display`
--> src/main.rs:9:20
|
9 | println!("{}", laptop);
| ^^^^^^ `Screen` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Screen`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead{} uses the Display trait, and there is no sensible way for Rust to guess how you want a screen shown to a user. 1920x1080? 1920 by 1080 pixels? 2.1 MP? It refuses to pick.
So take the note’s advice and use the debug placeholder {:?}:
println!("{laptop:?}");error[E0277]: `Screen` doesn't implement `Debug`
--> src/main.rs:9:15
|
9 | println!("{laptop:?}");
| ^^^^^^^^^^ `Screen` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `Screen`
= note: add `#[derive(Debug)]` to `Screen` or manually `impl Debug for Screen`
help: consider annotating `Screen` with `#[derive(Debug)]`
|
1 + #[derive(Debug)]
2 | struct Screen {
|Still not automatic — but this time the compiler hands you the exact line to paste:
#[derive(Debug)]
struct Screen {
width_px: u32,
height_px: u32,
}
fn main() {
let laptop = Screen {
width_px: 1920,
height_px: 1080,
};
println!("{laptop:?}");
println!("{laptop:#?}");
}Output:
Screen { width_px: 1920, height_px: 1080 }
Screen {
width_px: 1920,
height_px: 1080,
}Two placeholders, two jobs:
| Placeholder | Prints | Reach for it when |
|---|---|---|
{:?} | everything on one line | the value is small, or you are printing inside a loop |
{:#?} | one field per line, indented | the value is big, or has other structs inside it |
Why {:#?} Earns Its Keep
With two fields the difference looks cosmetic. Nest one struct inside another and it stops being cosmetic:
#[derive(Debug)]
struct Screen {
width_px: u32,
height_px: u32,
}
#[derive(Debug)]
struct Device {
name: String,
screen: Screen,
touch: bool,
}
fn main() {
let tablet = Device {
name: String::from("Tab S9"),
screen: Screen { width_px: 2360, height_px: 1640 },
touch: true,
};
println!("{tablet:?}");
println!();
println!("{tablet:#?}");
}Output:
Device { name: "Tab S9", screen: Screen { width_px: 2360, height_px: 1640 }, touch: true }
Device {
name: "Tab S9",
screen: Screen {
width_px: 2360,
height_px: 1640,
},
touch: true,
}The one-liner is already a squint. Add a Vec of devices and it becomes unreadable. {:#?} indents each level, so structure stays visible however deep it goes.
Put #[derive(Debug)] on essentially every struct you write. It costs nothing at runtime unless something actually prints, and plenty of standard tooling quietly requires it — assert_eq! cannot show you what went wrong without it.
dbg! — The Macro Built For This Exact Moment
println! is fine, but when you are chasing a bug it makes you do clerical work: type the variable name, type a label so you can tell one line from another, and find a spot in the code where a statement will fit.
The dbg! macro does all of that for you:
fn main() {
let base = 960;
let width = dbg!(base * 2);
println!("width is {width}");
}Output:
[src/main.rs:3:17] base * 2 = 1920
width is 1920One short call, and you got four things: the file, the line and column, the source text of what you wrapped, and the value. You never typed a label.
Then look at the second line. width is still 1920 — because dbg! gives the value back. The expression dbg!(base * 2) evaluates to exactly what base * 2 evaluated to, so you can wrap it around anything, anywhere, without restructuring a single line.
Three Properties Worth Memorising
1. It works on whole structs, and always pretty-prints.
#[derive(Debug)]
struct Screen {
width_px: u32,
height_px: u32,
}
fn main() {
let scale = 2;
let laptop = Screen {
width_px: dbg!(960 * scale),
height_px: 1080,
};
dbg!(&laptop);
println!("total pixels: {}", laptop.width_px * laptop.height_px);
}Output:
[src/main.rs:11:19] 960 * scale = 1920
[src/main.rs:15:5] &laptop = Screen {
width_px: 1920,
height_px: 1080,
}
total pixels: 2073600Note where the first dbg! sits — inside the struct literal, wrapped around one field’s value. That is the move you cannot make with println! without pulling the expression out into its own variable first.
2. It prints to standard error, not standard output.
println! writes to stdout. dbg! writes to stderr. That separation is the whole point, and you can see it in one run:
$ cargo run > pixels.txt
[src/main.rs:11:19] 960 * scale = 1920
[src/main.rs:15:5] &laptop = Screen {
width_px: 1920,
height_px: 1080,
}
$ cat pixels.txt
total pixels: 2073600The println! line went into the file. The dbg! lines stayed on screen. Your debugging never pollutes the output your program is actually for — which matters enormously the day you write a command-line tool whose output someone pipes into another program.
Two streams, two purposes. Redirect one and the other still reaches you.
3. It takes ownership — so pass a reference.
This is the one thing that trips people up:
#[derive(Debug)]
struct Screen {
width_px: u32,
height_px: u32,
}
fn main() {
let laptop = Screen { width_px: 1920, height_px: 1080 };
dbg!(laptop);
println!("{}px wide", laptop.width_px);
}error[E0382]: borrow of moved value: `laptop`
--> src/main.rs:12:27
|
8 | let laptop = Screen { width_px: 1920, height_px: 1080 };
| ------ move occurs because `laptop` has type `Screen`, which does not implement the `Copy` trait
9 |
10 | dbg!(laptop);
| ------------ value moved here
11 |
12 | println!("{}px wide", laptop.width_px);
| ^^^^^^^^^^^^^^^ value borrowed here after move
|
help: consider borrowing instead of transferring ownership
|
10 | dbg!(&laptop);
| +dbg! takes ownership and returns it — but written as a bare statement, that returned value goes nowhere and is dropped. Exactly the ownership rules you already know, with no exception carved out for debugging.
The fix is the one the compiler prints: dbg!(&laptop). Make borrowing your default habit.
println! or dbg!?
println!("{x:?}") | dbg!(&x) | |
|---|---|---|
| Goes to | stdout | stderr |
Survives > file.txt | no, it lands in the file | yes, stays on screen |
| Shows file and line | no | yes, automatically |
| Shows the source text | no | yes |
| Returns a value | no | yes — the value itself |
| Formatting | you choose | always pretty-printed |
| Meant to be | kept | deleted |
Delete every dbg! before you commit. It is not a logger: there is no level to filter it out with, and a release build does not strip it. Treat it like a breakpoint. Clippy’s dbg_macro lint exists precisely to catch the ones you forget.
The quick rule:
Output for a user goes through Display. Everything else is debugging, and debugging needs Debug.
Other Traits You Can Derive
Debug is the one you will reach for first, but it is not alone. derive is a general mechanism: you name a trait, and the compiler writes the obvious implementation by looking at your fields.
Try comparing two screens without asking for it and you get a now-familiar shape of error:
#[derive(Debug)]
struct Screen {
width_px: u32,
height_px: u32,
}
fn main() {
let laptop = Screen { width_px: 1920, height_px: 1080 };
let monitor = Screen { width_px: 1920, height_px: 1080 };
println!("same screen? {}", laptop == monitor);
}error[E0369]: binary operation `==` cannot be applied to type `Screen`
--> src/main.rs:11:40
|
11 | println!("same screen? {}", laptop == monitor);
| ------ ^^ ------- Screen
| |
| Screen
|
note: an implementation of `PartialEq` might be missing for `Screen`
help: consider annotating `Screen` with `#[derive(PartialEq)]`Add it to the list — derive takes as many traits as you want:
#[derive(Debug, Clone, Copy, PartialEq)]
struct Screen {
width_px: u32,
height_px: u32,
}
fn main() {
let laptop = Screen { width_px: 1920, height_px: 1080 };
let monitor = Screen { width_px: 1920, height_px: 1080 };
let phone = Screen { width_px: 1080, height_px: 1920 };
println!("laptop == monitor? {}", laptop == monitor);
println!("laptop == phone? {}", laptop == phone);
let spare = laptop;
println!("both still usable: {spare:?} {laptop:?}");
}Output:
laptop == monitor? true
laptop == phone? false
both still usable: Screen { width_px: 1920, height_px: 1080 } Screen { width_px: 1920, height_px: 1080 }Two things happened there. PartialEq compares field by field, so two separately built screens with the same numbers are equal. And Copy changed the rules from the previous section — let spare = laptop; now duplicates instead of moving, so both variables still work.
The ones worth knowing:
| Derive | Gives you | Requires |
|---|---|---|
Debug | {:?} and {:#?} printing, and dbg! | every field is Debug |
Clone | an explicit .clone() | every field is Clone |
Copy | assignment duplicates instead of moving | every field is Copy, and Clone is derived too |
PartialEq | == and !=, compared field by field | every field is PartialEq |
Eq | marks equality as total — needed by HashMap keys | PartialEq |
PartialOrd, Ord | <, >, and .sort() | fields compared in declaration order |
Hash | use as a HashMap or HashSet key | every field is Hash |
Default | Screen::default(), every field at its own default | every field is Default |
Default is the one people are usually delighted to discover:
#[derive(Debug, Default)]
struct Screen {
width_px: u32,
height_px: u32,
}
fn main() {
let blank = Screen::default();
println!("{blank:?}");
}Output:
Screen { width_px: 0, height_px: 0 }A derive only ever writes the obvious implementation. If two screens should count as equal when they have the same pixel count regardless of orientation, no derive will do that — you write impl PartialEq for Screen by hand. Deriving is the shortcut, not the ceiling.
Putting It All Together
Everything from this chapter in one program — a struct, borrowed parameters, a function that builds a new instance, derived Debug, Copy and PartialEq:
#[derive(Debug, Clone, Copy, PartialEq)]
struct Screen {
width_px: u32,
height_px: u32,
}
fn main() {
let laptop = Screen { width_px: 1920, height_px: 1080 };
let phone = Screen { width_px: 1080, height_px: 1920 };
let tablet = Screen { width_px: 2360, height_px: 1640 };
for screen in [laptop, phone, tablet] {
report(&screen);
}
println!();
println!("phone is a sideways laptop? {}", phone == rotate(&laptop));
println!("laptop is still here: {laptop:?}");
}
fn report(screen: &Screen) {
let size = format!("{} x {}", screen.width_px, screen.height_px);
println!(
"{size:<12} {:>4.1} MP {:.2}:1 {}",
megapixels(screen),
aspect_ratio(screen),
orientation(screen),
);
}
fn megapixels(screen: &Screen) -> f64 {
(screen.width_px * screen.height_px) as f64 / 1_000_000.0
}
fn aspect_ratio(screen: &Screen) -> f64 {
screen.width_px as f64 / screen.height_px as f64
}
fn orientation(screen: &Screen) -> &str {
if screen.width_px >= screen.height_px {
"landscape"
} else {
"portrait"
}
}
fn rotate(screen: &Screen) -> Screen {
Screen {
width_px: screen.height_px,
height_px: screen.width_px,
}
}Output:
1920 x 1080 2.1 MP 1.78:1 landscape
1080 x 1920 2.1 MP 0.56:1 portrait
2360 x 1640 3.9 MP 1.44:1 landscape
phone is a sideways laptop? true
laptop is still here: Screen { width_px: 1920, height_px: 1080 }Four things worth noticing:
for screen in [laptop, phone, tablet]works without any cloning becauseScreenisCopy. Remove that one word from the derive list and the array moves all three screens, and the last two lines stop compiling.rotatereturns a brand newScreen. A function can return your type as easily as it returns a number — that is the expression-based return with a struct literal as the final expression.phone == rotate(&laptop)istruebecausePartialEqcompares field by field, and a rotated 1920×1080 really is 1080×1920.- Every function takes
&Screen. None of them keep anything, so none of them ask for ownership.
What Is Still Wrong
Read that program once more and something should nag at you.
megapixels, aspect_ratio, orientation and rotate are only ever useful for a Screen. Every single one takes a screen as its first argument. Yet they float around at the top level of the file, mixed in with main, as though they might apply to anything.
That is a strong hint from the code itself. These functions belong to the type, and Rust has a place to put them — an impl block — which turns this:
megapixels(&laptop)into this:
laptop.megapixels()That is the next chapter, and it is the last piece that makes structs feel like a complete feature rather than a bag of fields.
Error Cheat Sheet
| Error | What it means | Usual fix |
|---|---|---|
| E0308 mismatched types | You passed a different struct with identical fields | Working as intended — that is the fence doing its job |
| E0382 borrow of moved value | A function, or dbg!, took your struct by value | Pass &value instead of value |
E0277 doesn’t implement Display | You printed a struct with {} | Use {:?} with #[derive(Debug)], or write impl Display |
E0277 doesn’t implement Debug | You used {:?} or dbg! with no derive | Add #[derive(Debug)] above the struct |
E0369 == cannot be applied | You compared two structs | Add #[derive(PartialEq)] |
E0599 no associated item named default | You called Type::default() with no derive | Add #[derive(Default)] |
⚠️ dbg! prints nothing in a test | Test output is captured by default | Run cargo test -- --nocapture |
| ⚠️ fields are never read | Nothing actually reads those fields yet | Harmless while prototyping; #[derive(Debug)] alone does not silence it |
Every E#### code can be explained in your terminal: run rustc --explain E0382 for a full write-up with examples, no internet needed.
Summary
| You write | What it does |
|---|---|
fn f(a: u32, b: u32) | Two loose values — a swap compiles silently |
fn f(pair: (u32, u32)) | One value, no names — any pair fits |
fn f(s: &Screen) | One named type, borrowed — nothing else fits |
#[derive(Debug)] + {:?} | Print the whole value on one line |
#[derive(Debug)] + {:#?} | Print it with one field per line |
dbg!(&value) | File, line, source text and value — to stderr, value handed back |
#[derive(PartialEq)] | Compare two instances with ==, field by field |
#[derive(Copy, Clone)] | Assignment duplicates instead of moving |
#[derive(Default)] | Type::default() with every field at its own default |
The mental shortcuts worth keeping:
- Two parameters of the same type are a bug waiting to happen. Name them into a struct.
- A struct is a new type, not an alias. Identical fields are still not interchangeable.
- Borrow by default.
&Screenunless the function genuinely keeps the value. - Derive
Debugon everything. It is free until you print, and tooling expects it. {:?}for small,{:#?}for nested.dbg!is a breakpoint, not a logger. It goes to stderr, hands the value back, and gets deleted.
Frequently Asked Questions
When should you use a struct instead of separate variables in Rust?
As soon as two or more values only make sense together. Separate parameters of the same type can be passed in the wrong order and still compile, because the compiler has no idea they are related. A struct turns them into one named type, so a function takes a single argument that cannot be assembled wrongly — and the signature documents itself for the next reader.
What does the dbg! macro do in Rust?
It prints the file name, the line and column, the source text of the expression you wrapped, and that expression’s value — then hands the value back so the surrounding code carries on working. It needs Debug on the type, always pretty-prints, and writes to standard error rather than standard output. It has been in the standard library since Rust 1.32, so there is nothing to import.
What is the difference between dbg! and println! in Rust?
println! writes to stdout, prints only what you tell it to, and returns nothing — it is for output your program is meant to produce. dbg! writes to stderr, adds the file, line and source text for free, and returns the value it was given so you can wrap it around any expression in place. Because they use different streams, cargo run > out.txt puts the println! lines in the file while the dbg! lines stay on your screen.
Why does dbg! say “borrow of moved value”?
Because dbg! takes ownership of what you pass it and returns ownership as its result. Written as a bare statement, that returned value goes nowhere and is dropped, so the next use of the variable is error E0382. Pass a reference instead — dbg!(&laptop) — which is what the compiler’s own help: line suggests. Types that implement Copy are unaffected, since they are duplicated rather than moved.
Why can’t I print a Rust struct with {}?
{} uses the Display trait, which is for a single obvious human-readable form, and Rust will not guess what that should be for a type you invented. Add #[derive(Debug)] and print with {:?} instead, or write impl Display for YourType by hand when you want a specific presentation for your users.
What is the difference between {:?} and {:#?} in Rust?
They print the same information. {:?} puts the whole value on one line, which suits something small or a value printed inside a loop. {:#?} switches on pretty-printing: one field per line, with nested structs indented. Use it for anything with more than about three fields or with other structs inside it.
Why do Rust functions take a struct by reference?
Because a struct that is not Copy is moved when passed by value, so the caller cannot use it afterwards and later lines fail with E0382. A shared reference lets the function read every field while the caller keeps ownership. Take a struct by value only when the function is genuinely meant to keep it — pushing it into a collection, for example.
Which traits can you derive on a Rust struct?
The common ones are Debug for printing, Clone and Copy for duplicating, PartialEq and Eq for equality, PartialOrd and Ord for ordering, Hash for use as a map key, and Default for a default-value constructor. A derive only works when every field already implements the same trait, and it only ever writes the obvious implementation — anything more specific has to be hand-written.
Should you leave dbg! in production Rust code?
No. Unlike a logging library, dbg! has no level to filter it out and is not removed in a release build, so it keeps printing and keeps paying the formatting cost. Treat it like a breakpoint and delete it once the bug is found. Clippy’s dbg_macro lint can fail your build if one is left behind.
Why does dbg! print nothing when I run my tests?
Because cargo test captures the output of passing tests by default — including stderr. Run cargo test -- --nocapture to see it, or let the test fail, since output from failing tests is always shown.
What’s Next?
You have now seen the whole argument for structs, not just the syntax: the same program written three times, ending with a version where the compiler understands enough to reject a wrong call. And you have the two tools that make a custom type pleasant to work on — #[derive(Debug)] for looking inside it, and dbg! for looking inside it mid-expression.
The loose ends are the functions. megapixels(&screen) should be screen.megapixels(), and that means methods — the impl block, the self parameter, and associated functions like Screen::new(). That is next.
In the meantime, go back over Structs with version 3 in mind, or revisit References and Borrowing now that you have seen why every function in this chapter took a &. The full Rust tutorial is here whenever you want the next piece.