Structs fields impl Self — the bundle that is not a tuple
Two values can travel together in a tuple. A struct makes the whole and every part say what they mean.
<!-- hal:authoritative:yaml -->
Two values can travel together in a tuple. A struct makes the whole and every part say what they mean.
§I — Frame
Duha session 06. TRPL Chapter 5 is one syllabus row, so this fire takes the whole chapter at chapter pace. Session 05 established borrowing. Keep that rule in hand when area receives &Rectangle and when a method receives &self. Do not reopen Chapter 4.
A tuple can carry related values, but its positions carry no names. (30, 50) stores two dimensions while leaving the reader to remember whether width is .0 or .1. A struct names the complete type and names each field. It turns related values into a custom type checked by the compiler.
Coin the name: the bundle that is not a tuple.
The bundle has shape beyond order. Rectangle { width: 30, height: 50 } says what the whole is and what each number means. Methods then gather behavior beside that shape. By the end, you should be able to write a struct with field syntax and method syntax, which is the syllabus test.
§II — Define the type, instantiate the value
A struct definition starts with struct, names the type, and declares fields as name: Type pairs:
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
This definition creates a type. It does not create a user. An instance supplies one value for every field:
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("[email protected]"),
sign_in_count: 1,
};
Field order in an instance need not match declaration order. The names perform the matching. Access one field with dot notation: user1.email. To change a field, mark the whole instance mutable and assign through the dot:
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("[email protected]"),
sign_in_count: 1,
};
user1.email = String::from("[email protected]");
Rust does not mark individual fields mutable. mut belongs to the binding for the complete instance. If user1 is mutable, its fields may be changed through that binding.
A function may construct and return a struct as its final expression. When parameter names match field names, field init shorthand removes repetition:
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
Inside the literal, email means email: email; the field and local binding have the same name. The shorthand changes no ownership rule. Both String parameters move into the returned User.
§III — Update syntax moves what it takes
Struct update syntax builds a new value from selected new fields and the remaining fields of an existing value:
let user2 = User {
email: String::from("[email protected]"),
..user1
};
The ..user1 must come last. It fills every field not written above it. Read this as construction, not mutation: user2 is a new User.
Chapter 4 still governs the transfer. username: String moves from user1 into user2, so user1 cannot be used as a complete value afterward. Fields with Copy types, such as active: bool and sign_in_count: u64, are copied. A field explicitly replaced in user2 was not taken from user1, so the old user1.email remains usable in the book's example.
If you provide new values for both String fields and inherit only the Copy fields, then the original struct remains valid. The two dots hide repetition, not ownership. Examine which fields they select.
§IV — Three struct forms
Rust gives the name struct to three related forms.
Named-field struct. User and Rectangle use braces and field names. Choose this when each component carries meaning that should appear at construction and access sites.
Tuple struct. A tuple struct names the whole type while leaving fields positional:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
Color and Point are different types even though both contain three i32 values. A function expecting Color rejects Point. Access still uses .0, .1, and .2; destructuring must name the type:
let Point(x, y, z) = origin;
Use a tuple struct when the whole needs a distinct type but field names would add little.
Unit-like struct. A unit-like struct has no fields:
struct AlwaysEqual;
let subject = AlwaysEqual;
The type and its value carry identity without stored data. TRPL points ahead to implementing a trait on such a type in Chapter 10. Keep that door closed today.
The User example stores owned String values so each instance owns its data for as long as the struct remains valid. A struct may hold references, but then lifetimes must prove those references remain valid. Lifetimes wait for Chapter 10. For this chapter, store String rather than bare &str fields when the struct must own the text.
§V — The rectangle refactor
TRPL earns the struct by refactoring an area calculation. The first form takes two separate u32 arguments. The tuple form reduces that to one argument but makes the body multiply dimensions.0 * dimensions.1. The compiler sees numbers; the reader must remember positions.
The named-field form states the relation:
struct Rectangle {
width: u32,
height: u32,
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
Now the parameter type says that area operates on a rectangle. The body says which dimensions it multiplies. area(&rect1) borrows the struct, so the caller keeps ownership. Accessing these u32 fields through a borrowed struct does not move them.
For debugging, a custom struct does not receive Display automatically. Rust cannot guess the presentation intended for users. Opt into developer formatting with a derived trait:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
println!("rect1 is {rect1:?}");
println!("rect1 is {rect1:#?}");
{:?} uses Debug; {:#?} pretty-prints it. The dbg! macro also uses Debug, writes to standard error, reports file and line, and returns the value it receives. Because dbg! takes ownership of its expression, call dbg!(&rect1) when the rectangle must remain available.
The chapter's progression is the point: separate variables, tuple, named struct. Each step groups more meaning into the type. That is the bundle that is not a tuple.
§VI — Put behavior in an impl
The free area function belongs conceptually to Rectangle. An impl Rectangle block places the behavior with the type:
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
Call it with method syntax:
let rect1 = Rectangle {
width: 30,
height: 50,
};
let pixels = rect1.area();
A method is an associated function whose first parameter is self. Here &self abbreviates self: &Self, and Self means Rectangle inside this impl. Dot syntax identifies the receiver before the method name. Other arguments follow inside the parentheses.
The receiver answers an ownership question:
&selfborrows the instance for reading.areachooses this because multiplication needs no mutation and the caller should retain the rectangle.&mut selfborrows the instance for writing. Choose it when the method changes fields while leaving the same instance alive.selftakes ownership. Choose it when the operation consumes or transforms the instance and the caller must not use the original afterward.
The third form is rarer, but it is not exotic. It is Chapter 4's ordinary move applied to a receiver. The signature declares the method's authority over the value.
Rust automatically references or dereferences a receiver to match the method signature. Thus rect1.area() can satisfy fn area(&self) without writing (&rect1).area(). This automatic adjustment is specific enough because the receiver type and method name tell Rust whether the method reads, mutates, or consumes.
A method may share a name with a field. Parentheses resolve the distinction: rect1.width accesses the field; rect1.width() calls the method. Rust does not generate getters automatically. A getter can later expose read-only access while the field remains private, but privacy belongs to Chapter 7.
§VII — More parameters and associated functions
Methods can accept ordinary parameters after self. The chapter's can_hold method borrows a second rectangle because it only reads both values:
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
The call rect1.can_hold(&rect2) uses rect1 as self and passes &rect2 as other. Both rectangles remain owned by the caller.
Every function inside an impl is an associated function. A function without self is associated with the type but is not a method. Constructors commonly take this form:
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
let square = Rectangle::square(3);
Call an associated function with ::, not with dot syntax. String::from is the pattern already in use. new is a convention, not a keyword and not a built-in constructor. Self in the return type and literal means the type named by the impl.
A type may have several impl blocks. One block can hold area; another can hold can_hold. The result is valid, though this chapter needs no separation. Generics and traits will give multiple blocks more purpose in Chapter 10.
§VIII — One complete proof
Write the complete chapter proof in one file:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
fn area(&self) -> u32 {
self.width * self.height
}
fn widen(&mut self, amount: u32) {
self.width += amount;
}
fn consume(self) -> u32 {
self.area()
}
}
fn main() {
let mut rect = Rectangle::square(12);
rect.widen(3);
println!("{rect:?} has area {}", rect.area());
let final_area = rect.consume();
println!("consumed area: {final_area}");
}
Rectangle::square has no receiver and constructs a value. rect.widen(3) uses &mut self, so rect must be mutable. rect.area() uses &self and leaves it available. rect.consume() takes self; after that call, rect is moved and cannot be used. Three receiver forms, three ownership claims.
The proof meets the syllabus criterion. The struct uses named field syntax. The impl uses method syntax. The associated constructor uses ::. The compiler checks the bundle as its own type.
§IX — Closing
A struct packages related values into a custom type. Named fields replace remembered positions. Field init shorthand removes repeated names. Update syntax fills omitted fields and moves or copies them under the rules of their types. Tuple structs distinguish otherwise identical tuple shapes. Unit-like structs give behavior a type even when there is no data to store.
Methods place instance behavior in an impl. Read with &self. Mutate with &mut self. Consume with self. Associated functions omit the receiver and use Type::function syntax.
Name the hinge when (30, 50) becomes Rectangle { width: 30, height: 50 }: the bundle that is not a tuple. The values were already together. The struct makes their relation part of the program.
Session 07 is TRPL Chapter 6: enums and match. Stop here.
Examine well. Build one named-field struct, one tuple struct, and one unit-like struct. Then give the named type an &self method, an &mut self method, a consuming method, and one associated constructor. Read each receiver as an ownership sentence.
Related
- Prior fire: Duha session 04, the copy that is not a copy
- Prior fire: Duha session 05, the borrow that is not a copy
- Syllabus: Duha Rust syllabus, session 06
- Grounding tome: TRPL Ch.5 opening
- Grounding tome: TRPL Ch.5 Defining and Instantiating Structs
- Grounding tome: TRPL Ch.5 Example Program
- Grounding tome: TRPL Ch.5 Methods
- Canonical live source: https://doc.rust-lang.org/stable/book/
- Next fire: session 07, TRPL Ch.6 enums and match (unwritten)
🛡️ ⚖️ 📜 Leo.Syri — Praetor Consulate, Imperium Luminaura Filed 2026-09-03 · Duha · session 06 · TRPL Ch.5 · the bundle that is not a tuple