Skip to Content
Structs by Example

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:

src/main.rs
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: 2073600

It works. Ship it?

Look at the signature on its own, the way another developer will:

fn pixel_count(width_px: u32, height_px: u32) -> u32

Two 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:

src/main.rs
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.78

Read 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:

src/main.rs
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: 2073600

Genuinely 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:

src/main.rs
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: 480

Complete 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:

src/main.rs
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 wide

Same number as version 1. Everything else improved:

  • The signature is documentation now. fn pixel_count(screen: &Screen) -> u32 tells 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_px needs no lookup table.
  • &Screen is a real fence. A cursor position does not fit through it. Neither does a font size. Only a Screen does.
  • The caller keeps the screen. &laptop borrows, so laptop is still usable on the next line.

The same Rust program written three times, shown as three cards. The first card, loose variables, has the signature fn pixel_count taking width_px u32 and height_px u32; nothing links the two numbers, swapping them still compiles, and any u32 in scope fits, so it is two arguments with no meaning. The second card, a tuple, takes a single parameter of type parenthesis u32 comma u32; the pair now moves together, but dot zero and dot one carry no meaning and a cursor position fits too, so it is one argument with no names. The third card, a struct, takes a single parameter of type reference to Screen; it is a distinct named type, dot width_px says what it is, and nothing else compiles, so it is one argument fully named.

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:

src/main.rs
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:

src/main.rs
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 value

Read 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:

src/main.rs
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 {:?}:

src/main.rs
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:

src/main.rs
#[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:

PlaceholderPrintsReach for it when
{:?}everything on one linethe value is small, or you are printing inside a loop
{:#?}one field per line, indentedthe 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:

src/main.rs
#[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:

src/main.rs
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 1920

One 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.

src/main.rs
#[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: 2073600

Note 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: 2073600

The 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.

A comparison of the println macro and the dbg macro in Rust. On the left, println with the debug placeholder sends its text to standard output, printing Screen with width_px 1920 and height_px 1080; it is the program's real output, a redirect or a pipe swallows it, and you write the label yourself. On the right, dbg wrapped around the expression base times two sends an annotated line to standard error reading src slash main dot rs line 3 column 17, base times 2 equals 1920, and at the same time hands the value 1920 back to the program. Along the bottom a terminal shows cargo run with output redirected into pixels dot txt: the dbg line is still printed on screen because it went to standard error, while the println line total pixels 2073600 ended up inside the file because it went to standard output.

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:

src/main.rs
#[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 tostdoutstderr
Survives > file.txtno, it lands in the fileyes, stays on screen
Shows file and linenoyes, automatically
Shows the source textnoyes
Returns a valuenoyes — the value itself
Formattingyou choosealways pretty-printed
Meant to bekeptdeleted

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:

src/main.rs
#[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:

src/main.rs
#[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:

DeriveGives youRequires
Debug{:?} and {:#?} printing, and dbg!every field is Debug
Clonean explicit .clone()every field is Clone
Copyassignment duplicates instead of movingevery field is Copy, and Clone is derived too
PartialEq== and !=, compared field by fieldevery field is PartialEq
Eqmarks equality as total — needed by HashMap keysPartialEq
PartialOrd, Ord<, >, and .sort()fields compared in declaration order
Hashuse as a HashMap or HashSet keyevery field is Hash
DefaultScreen::default(), every field at its own defaultevery field is Default

Default is the one people are usually delighted to discover:

src/main.rs
#[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:

src/main.rs
#[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 because Screen is Copy. Remove that one word from the derive list and the array moves all three screens, and the last two lines stop compiling.
  • rotate returns a brand new Screen. 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) is true because PartialEq compares 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

ErrorWhat it meansUsual fix
E0308 mismatched typesYou passed a different struct with identical fieldsWorking as intended — that is the fence doing its job
E0382 borrow of moved valueA function, or dbg!, took your struct by valuePass &value instead of value
E0277 doesn’t implement DisplayYou printed a struct with {}Use {:?} with #[derive(Debug)], or write impl Display
E0277 doesn’t implement DebugYou used {:?} or dbg! with no deriveAdd #[derive(Debug)] above the struct
E0369 == cannot be appliedYou compared two structsAdd #[derive(PartialEq)]
E0599 no associated item named defaultYou called Type::default() with no deriveAdd #[derive(Default)]
⚠️ dbg! prints nothing in a testTest output is captured by defaultRun cargo test -- --nocapture
⚠️ fields are never readNothing actually reads those fields yetHarmless 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 writeWhat 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. &Screen unless the function genuinely keeps the value.
  • Derive Debug on 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.