10.2Defining and instantiating structs
Last lesson made the case for structs. This one makes one and uses it.
Defining the struct
A struct definition lists the fields the type holds, each with a name and a type:
struct Point {
x: f64,
y: f64,
}
This is a definition, not a value. It creates no data and reserves no memory; it teaches the compiler what a Point is, the same way fn teaches it what a function is. Put it at the top level of a file, outside main, where types usually live. Each field gets an explicit type, for the same reason function parameters do (lesson 2.3): the compiler refuses to guess, and the guarantee that every Point has exactly two f64 fields is worth typing out.
Note the trailing comma after the last field. Rust allows it everywhere and rustfmt adds it, so that inserting a new field later is a one-line diff. You'll see it throughout the course.
Creating an instance
To make an actual Point, name the type and give every field a value:
fn main() {
let origin = Point { x: 0.0, y: 0.0 };
let corner = Point { x: 3.0, y: 4.0 };
println!("corner is at ({}, {})", corner.x, corner.y);
}corner is at (3, 4)
An instance is a concrete value of a struct type, the way 5 is a concrete value of i32. The syntax reads like the definition turned inside out: where the definition said x: f64 (a name and a type), the instance says x: 0.0 (a name and a value). You must supply every field. Leave one out and the program is refused:
let p = Point { x: 1.0 };error[E0063]: missing field `y` in initializer of `Point`
--> src/main.rs:7:13
|
7 | let p = Point { x: 1.0 };
| ^^^^^ missing `y`
This is lesson 1.4's rule wearing a bigger coat: Rust won't let you read an uninitialized value, and a half-built Point is exactly that. There's no such thing as a Point with an undecided y. (Chapter 10.6 shows the clean way to supply sensible starting values so you don't repeat them at every call.)
Reading fields with dot notation
Reach into an instance with a dot and the field name, which you've already seen above and which behaves just like tuple access from lesson 4.11, except the parts have names instead of numbers:
let corner = Point { x: 3.0, y: 4.0 };
let distance = (corner.x * corner.x + corner.y * corner.y).sqrt();
println!("{distance}");5
corner.x is an f64, usable anywhere an f64 is. The names are the point: corner.x says what it is, where a tuple's corner.0 makes you remember that slot 0 was the x coordinate. Compare a struct field to a tuple element and the case for naming makes itself.
Mutating fields
Reading is one thing; changing a field is where Rust's mutability rule from lesson 1.4 reappears, with a twist worth pausing on. To change any field, the whole binding must be mut:
fn main() {
let mut player = Point { x: 0.0, y: 0.0 };
player.x = 10.0;
player.y = 5.0;
println!("({}, {})", player.x, player.y);
}(10, 5)
Drop the mut and assigning to player.x is refused:
error[E0594]: cannot assign to `player.x`, as `player` is not declared as mutable
--> src/main.rs:3:5
|
3 | player.x = 10.0;
| ^^^^^^^^^^^^^^^ cannot assign
Here's the twist. Rust has no per-field mutability. You cannot declare that x is changeable while y is frozen; mutability is a property of the binding, not of individual fields. Either the whole player is mutable or none of it is.
Key insight
Mutability in Rust attaches to bindings, not to data. A field isn't "a mutable field"; it's a field of a value you happen to hold mutably. Move that same value into an immutable binding (or borrow it with a plain &) and every field is read-only again. This is the lesson 1.4 rule, consistent all the way up: the variable decides, not the type.
If you find yourself wishing one field could be frozen while the rest move, that's usually a sign the frozen field belongs to a different type, or that the struct's fields should be private and changed only through methods (chapter 13 makes fields private; this chapter's methods, lesson 10.5, are how you guard them).
Structs in functions
A struct is a type like any other, so it goes in and out of functions the ordinary way, and the rules from chapters 8 and 9 apply with zero new vocabulary:
fn distance_from_origin(p: &Point) -> f64 {
(p.x * p.x + p.y * p.y).sqrt()
}
fn main() {
let corner = Point { x: 3.0, y: 4.0 };
println!("distance: {}", distance_from_origin(&corner));
println!("still own it: ({}, {})", corner.x, corner.y);
}distance: 5
still own it: (3, 4)
distance_from_origin takes &Point, so the call borrows corner rather than moving it, exactly as &String did in lesson 9.1. corner is still the owner afterward, so main keeps reading its fields on the next line. Taking &Point instead of Point is the same parameter decision from lesson 9.4, made for the same reason: the function only looks, so it borrows.
Quiz time
Question #1
Write a struct Rectangle with width and height fields (both f64), then create one that is 4.0 wide and 2.5 tall and print its area.
Show solution
struct Rectangle {
width: f64,
height: f64,
}
fn main() {
let r = Rectangle { width: 4.0, height: 2.5 };
println!("area: {}", r.width * r.height);
}
Prints area: 10. Rectangles return as the chapter's running example in lesson 10.9.
Question #2
This program is refused. Name the error and fix it.
struct Point {
x: f64,
y: f64,
}
fn main() {
let p = Point { x: 1.0, y: 2.0 };
p.x = 9.0;
println!("{}", p.x);
}Show solution
E0594: p isn't declared mutable, so its field can't be assigned. Fix: let mut p = .... There's no way to make just x mutable; mutability is a property of the binding p, not of the field.
Question #3
True or false: you can create a Point { x: 1.0 } and fill in y later. Explain.
Show solution
False. Every field must be given a value when the instance is created (E0063, missing field y). A struct with an unset field would be an uninitialized value, which lesson 1.4 forbids. If you want default starting values, lesson 10.6 shows the idiomatic way.
Next lesson trims the repetition out of building instances: field shorthand when a variable already has the right name, and update syntax for "the same as this one, but with two fields changed".