A struct (short for structure) groups several related values under one name, so that a user's name, email and age travel together as one User instead of three loose variables. Structs are Rust's main way to define your own data types. On their own they only hold data; you add behaviour by writing methods in an impl block. If you come from Java or Python, a struct with an impl block plays the role of a class, but without inheritance, without constructors built into the language, and with ownership rules that apply to every field.
This lesson covers defining and creating structs, the field init shorthand and struct update syntax, tuple structs and unit structs, impl blocks, the difference between methods and associated functions, and what self, &self and &mut self mean for ownership. You will also learn to print structs with #[derive(Debug)], see how structs are laid out in memory, and build a small shopping cart as a worked example. Interviewers commonly ask you to model something with a struct and then ask why a method takes &self rather than self; by the end you will be able to answer that from first principles.
Defining and creating a struct
You define a struct with the struct keyword, a name in UpperCamelCase, and a list of fields, each with a name and a type:
struct User {
name: String,
email: String,
age: u32,
active: bool,
}
fn main() {
let mut user = User {
email: String::from("priya@example.com"),
name: String::from("Priya"),
active: true,
age: 24,
};
user.age += 1;
user.active = false;
println!("{} <{}>", user.name, user.email);
println!("age = {}, active = {}", user.age, user.active);
}
Output:
Priya <priya@example.com>
age = 25, active = false
Step by step:
- The definition describes the shape of a
User: four fields, each with a type. Defining it creates no value; it only teaches the compiler a new type. User { ... }creates an instance, a concrete value of that type. You must give every field a value, but in any order: hereemailcomes beforename.- Dot notation reads a field (
user.name) and, if the instance is mutable, writes one (user.age += 1).
Mutability belongs to the whole instance
Rust has no way to mark one field as mutable and another as fixed. The binding decides: let mut user makes every field writable, and let user makes every field read-only. This does not compile:
struct User {
name: String,
age: u32,
}
fn main() {
let user = User {
name: String::from("Priya"),
age: 24,
};
user.age += 1;
println!("{} is {}", user.name, user.age);
}
error[E0594]: cannot assign to `user.age`, as `user` is not declared as mutable
--> immut_field_err.rs:11:5
|
11 | user.age += 1;
| ^^^^^^^^^^^^^ cannot assign
|
help: consider changing this to be mutable
|
7 | let mut user = User {
| +++
E0594 means "you tried to assign through a path that is not mutable". The help line is the usual fix. If you want some data to stay fixed while other data changes, that is a design question you answer with privacy and methods (lesson 10), not with per-field mut.
Every field must be initialised
Rust has no default null or zero for fields you forget. This does not compile:
struct User {
name: String,
email: String,
age: u32,
}
fn main() {
let user = User {
name: String::from("Priya"),
age: 24,
};
println!("{} is {}", user.name, user.age);
}
error[E0063]: missing field `email` in initializer of `User`
--> missing_field_err.rs:8:16
|
8 | let user = User {
| ^^^^ missing `email`
In Java, a forgotten field silently becomes null and fails later with a NullPointerException. In Rust, an instance either has every field set or it does not exist. When a field is genuinely optional, its type says so, with Option<T> (lesson 7).
Field init shorthand and struct update syntax
Two pieces of syntax make creating instances less repetitive.
Field init shorthand: when a variable has the same name as a field, you can write the name once. User { name, email, age, active: true } means User { name: name, email: email, age: age, active: true }.
Struct update syntax: ..other at the end of a struct expression means "take every field I did not list from other".
struct User {
name: String,
email: String,
age: u32,
active: bool,
}
fn build_user(name: String, email: String, age: u32) -> User {
User {
name,
email,
age,
active: true,
}
}
fn main() {
let first = build_user(String::from("Arjun"), String::from("arjun@example.com"), 30);
let second = User {
name: String::from("Meera"),
email: String::from("meera@example.com"),
..first
};
println!(
"{}: {}, {}, {}",
second.name, second.email, second.age, second.active
);
println!("first still has {} and {}", first.name, first.email);
}
Output:
Meera: meera@example.com, 30, true
first still has Arjun and arjun@example.com
build_user uses the shorthand for three fields. second gives its own name and email and takes age and active from first. Because those two fields are u32 and bool, which are Copy types, they are copied, and first is still fully usable afterwards.
Reading the compiler error: struct update moves fields
Struct update syntax behaves like assignment, field by field. A Copy field is copied; any other field is moved. Change the example so that second takes email from first, and it no longer compiles:
struct User {
name: String,
email: String,
age: u32,
}
fn main() {
let first = User {
name: String::from("Arjun"),
email: String::from("arjun@example.com"),
age: 30,
};
let second = User {
name: String::from("Meera"),
..first
};
println!("{} {} {}", second.name, second.email, second.age);
println!("{}", first.email);
}
error[E0382]: borrow of moved value: `first.email`
--> update_move_err.rs:20:20
|
14 | let second = User {
| __________________-
15 | | name: String::from("Meera"),
16 | | ..first
17 | | };
| |_____- value moved here
...
20 | println!("{}", first.email);
| ^^^^^^^^^^^ value borrowed here after move
|
= note: move occurs because `first.email` has type `String`, which does not implement the `Copy` trait
Notice the path in the message: first.email, not first. Rust tracks moves per field. After the update, first.email is gone, but first.name and first.age are still valid, and you could print them. What you cannot do is use first as a whole, or the moved field. This state is called a partial move. The fixes are the ones from lesson 4: clone the field (email: first.email.clone()), or stop using first.email after the update.
before: first { name ──> "Arjun", email ──> "arjun@...", age: 30 }
after: second { name ──> "Meera", email ──> "arjun@...", age: 30 }
^ heap buffer now owned here
first { name ──> "Arjun", email: (moved), age: 30 }
Tuple structs and unit structs
Not every struct needs named fields.
A tuple struct has a name and positional fields. You read the fields with .0, .1 and so on, like a tuple. A unit struct has no fields at all. It is useful when you need a type but no data, for example as a marker or to implement a trait (lesson 12).
struct Rgb(u8, u8, u8);
struct Metres(f64);
struct Kilometres(f64);
struct AlwaysEqual;
fn to_metres(distance: Kilometres) -> Metres {
Metres(distance.0 * 1000.0)
}
fn main() {
let saffron = Rgb(255, 153, 51);
println!(
"red = {}, green = {}, blue = {}",
saffron.0, saffron.1, saffron.2
);
let Rgb(r, g, b) = saffron;
println!("hex = #{r:02X}{g:02X}{b:02X}");
let run = to_metres(Kilometres(5.2));
println!("run = {} m", run.0);
let marker = AlwaysEqual;
let _copy_of_marker = marker;
println!(
"size of AlwaysEqual = {} bytes",
std::mem::size_of::<AlwaysEqual>()
);
}
Output:
red = 255, green = 153, blue = 51
hex = #FF9933
run = 5200 m
size of AlwaysEqual = 0 bytes
Rgb(255, 153, 51)creates a tuple struct;saffron.0reads the first field.let Rgb(r, g, b) = saffron;destructures it: the pattern on the left takes the value apart into three new variables. Lesson 7 covers patterns in depth.{r:02X}formats a number as two uppercase hexadecimal digits, padded with zeros.AlwaysEqualtakes zero bytes. Rust has true zero-sized types; they exist only at compile time.
The newtype pattern
A tuple struct with a single field, such as Metres(f64), is called a newtype. It costs nothing at run time (a Metres is exactly an f64 in memory) but it is a different type to the compiler. That turns unit mix-ups into compile errors. This does not compile:
struct Metres(f64);
struct Kilometres(f64);
fn to_metres(distance: Kilometres) -> Metres {
Metres(distance.0 * 1000.0)
}
fn main() {
let walk = Metres(800.0);
let total = to_metres(walk);
println!("{}", total.0);
}
error[E0308]: mismatched types
--> newtype_err.rs:10:27
|
10 | let total = to_metres(walk);
| --------- ^^^^ expected `Kilometres`, found `Metres`
| |
| arguments to this function are incorrect
|
note: function defined here
--> newtype_err.rs:4:4
|
4 | fn to_metres(distance: Kilometres) -> Metres {
| ^^^^^^^^^ --------------------
With plain f64 parameters, passing metres where kilometres were expected would compile and silently give a result 1,000 times too large. Unit confusion of exactly this kind has caused real engineering failures, which is why newtypes for IDs, units and money are a common Rust idiom.
| Kind | Syntax | Field access | Typical use |
|---|---|---|---|
| Named-field struct | struct User { name: String } | user.name | Most data types |
| Tuple struct | struct Rgb(u8, u8, u8); | colour.0 | Short, obvious fields |
| Newtype | struct Metres(f64); | m.0 | Units, IDs, type-safe wrappers |
| Unit struct | struct AlwaysEqual; | none | Markers, trait implementations |
Printing structs with Debug
println!("{}", x) uses the Display trait, meant for output shown to users. println!("{:?}", x) uses the Debug trait, meant for programmers. A trait is a named set of behaviour a type can implement (lesson 12 covers them fully). Your own structs implement neither by default. This does not compile:
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 3, y: -4 };
println!("{p:?}");
}
error[E0277]: `Point` doesn't implement `Debug`
--> debug_err.rs:8:15
|
8 | println!("{p:?}");
| ^^^^^ `Point` cannot be formatted using `{:?}` because it doesn't implement `Debug`
|
= help: the trait `Debug` is not implemented for `Point`
help: consider annotating `Point` with `#[derive(Debug)]`
|
1 + #[derive(Debug)]
2 | struct Point {
|
The fix is in the help line. #[derive(Debug)] is an attribute that asks the compiler to write the Debug implementation for you, printing every field:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
#[derive(Debug)]
struct Line {
start: Point,
end: Point,
label: String,
}
fn main() {
let p = Point { x: 3, y: -4 };
println!("{p:?}");
let line = Line {
start: Point { x: 0, y: 0 },
end: p,
label: String::from("diagonal"),
};
println!("{line:#?}");
println!(
"{} starts at ({}, {})",
line.label, line.start.x, line.start.y
);
let doubled = dbg!(line.end.x * 2);
println!("doubled = {doubled}");
}
Output (standard output and standard error together):
Point { x: 3, y: -4 }
Line {
start: Point {
x: 0,
y: 0,
},
end: Point {
x: 3,
y: -4,
},
label: "diagonal",
}
diagonal starts at (0, 0)
[debug_print.rs:30:19] line.end.x * 2 = 6
doubled = 6
Three ways to look at a value:
{:?}prints on one line:Point { x: 3, y: -4 }.{:#?}pretty-prints over several lines, which is far easier to read for nested structs. Note thatStringfields are shown with quotes.dbg!(expr)prints the file, line, column, the expression's source text and its value to standard error, then returns the value, so you can wrap it around an expression without changing the program's logic. It takes ownership of its argument unless you pass a reference (dbg!(&value)).
Every field of a derived Debug struct must itself implement Debug. That is why Line works: Point and String both do.
Pitfall: derived Debug does not count as reading a field
If a field is only ever printed through a derived Debug, the compiler still warns "field is never read". The dead-code check deliberately ignores derived implementations. In the example above, the line printing line.label and line.start is what keeps the build warning-free.
Display for user-facing output
For {} you implement Display by hand, because only you know how your type should look to a user. A short preview of trait syntax:
use std::fmt;
struct Money {
paise: u64,
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Rs {}.{:02}", self.paise / 100, self.paise % 100)
}
}
fn main() {
let price = Money { paise: 49_950 };
println!("Price: {price}");
let label = price.to_string();
println!("Label has {} characters", label.len());
}
Output:
Price: Rs 499.50
Label has 9 characters
write! works like format! but writes into the formatter f. Implementing Display also gives you to_string() for free. "Rs 499.50" is 9 bytes, all ASCII, so len() is 9.
Other useful derives
Several standard traits can be derived the same way. You will meet them properly in lesson 12, but four are worth knowing now:
#[derive(Debug, Clone, PartialEq, Default)]
struct Settings {
theme: String,
font_size: u8,
autosave: bool,
}
fn main() {
let defaults = Settings::default();
println!("{defaults:?}");
let mine = Settings {
theme: String::from("dark"),
..Default::default()
};
let copy = mine.clone();
println!("{mine:?}");
println!("equal? {}", mine == copy);
println!("same as defaults? {}", mine == defaults);
}
Output:
Settings { theme: "", font_size: 0, autosave: false }
Settings { theme: "dark", font_size: 0, autosave: false }
equal? true
same as defaults? false
| Derive | Gives you | Requirement |
|---|---|---|
Debug | {:?} and {:#?} | Every field is Debug |
Clone | .clone(), a deep copy | Every field is Clone |
PartialEq | == and !=, comparing field by field | Every field is PartialEq |
Default | Type::default(), every field set to its default (0, false, "") | Every field is Default |
Copy | Implicit copies instead of moves | Every field is Copy, and Clone is also derived |
..Default::default() combines struct update syntax with Default: set the fields you care about and take the rest from the defaults. It is the closest Rust gets to optional named arguments.
Methods and impl blocks
A method is a function attached to a type. You write methods inside an impl block (short for implementation) for that type. The first parameter of a method is always some form of self, the instance the method is called on.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
fn square(side: u32) -> Self {
Self::new(side, side)
}
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
fn scale(&mut self, factor: u32) {
self.width *= factor;
self.height *= factor;
}
fn into_dimensions(self) -> (u32, u32) {
(self.width, self.height)
}
}
fn main() {
let mut room = Rectangle::new(12, 10);
let tile = Rectangle::square(2);
println!("room area = {}", room.area());
println!("same call = {}", Rectangle::area(&room));
println!("room holds tile? {}", room.can_hold(&tile));
room.scale(2);
println!("after scale: {room:?}, area = {}", room.area());
let (w, h) = room.into_dimensions();
println!("w = {w}, h = {h}");
}
Output:
room area = 120
same call = 120
room holds tile? true
after scale: Rectangle { width: 24, height: 20 }, area = 480
w = 24, h = 20
Reading the impl block from top to bottom:
Self(capital S) is an alias for the type the block is for, hereRectangle. Using it means you do not repeat the type name, and the code keeps working if you rename the type.newandsquarehave noselfparameter, so they are associated functions, not methods. You call them with the type name and::, as inRectangle::new(12, 10).newis only a convention; Rust has no constructor keyword, and nothing stops you from also writingsquare,with_capacityorfrom_parts.areaandcan_holdtake&self: they only read the rectangle.scaletakes&mut self: it changes the rectangle in place.into_dimensionstakesself: it consumes the rectangle and returns its parts.
The area works out to 12 × 10 = 120. After scaling by 2 the rectangle is 24 × 20, so the area is 480.
Method call syntax is sugar
room.area() is shorthand for Rectangle::area(&room); the example prints both to show they are the same call. When you write value.method(), Rust looks at the method's self type and automatically adds &, &mut or * so the receiver matches. This is called automatic referencing, and it is why you never write (&room).area() or (&mut room).scale(2). In C++ you would choose between . and ->; in Rust there is only ..
self, &self and &mut self
The receiver type is an ownership decision, exactly like choosing a parameter type in lesson 5:
| Receiver | Short for | The method can... | The caller... | Use it for |
|---|---|---|---|---|
&self | self: &Self | Read fields | Keeps the value; needs no mut | Getters, calculations, checks |
&mut self | self: &mut Self | Read and change fields | Keeps the value; needs a mut binding | Setters, adding items, updating state |
self | self: Self | Do anything, including move fields out | Gives the value away for good | Conversions (into_...), builders, final steps |
Choose the weakest receiver that works. A method that only reads should take &self, because then it can be called while other shared references exist, and on values the caller does not own.
room.area() room.scale(2) room.into_dimensions()
| | |
v v v
&room (shared) &mut room (exclusive) room moved in
room still usable room still usable room is gone
Reading the compiler error: E0596, calling a &mut self method
A &mut self method needs a mutable borrow of the receiver, so the receiver must be declared mut. This does not compile:
struct Counter {
count: u32,
}
impl Counter {
fn increment(&mut self) {
self.count += 1;
}
}
fn main() {
let counter = Counter { count: 0 };
counter.increment();
println!("{}", counter.count);
}
error[E0596]: cannot borrow `counter` as mutable, as it is not declared as mutable
--> method_mut_err.rs:13:5
|
13 | counter.increment();
| ^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
12 | let mut counter = Counter { count: 0 };
| +++
The error is the same E0596 you met in lesson 5, because counter.increment() is really Counter::increment(&mut counter). The method call syntax hides the &mut, but the borrow checker still sees it.
Reading the compiler error: E0382, a method that consumes self
A method that takes self moves the receiver into the method. This does not compile:
struct Ticket {
seat: String,
}
impl Ticket {
fn redeem(self) -> String {
format!("Seat {} redeemed", self.seat)
}
}
fn main() {
let ticket = Ticket {
seat: String::from("H12"),
};
println!("{}", ticket.redeem());
println!("{}", ticket.redeem());
}
error[E0382]: use of moved value: `ticket`
--> consume_err.rs:16:20
|
12 | let ticket = Ticket {
| ------ move occurs because `ticket` has type `Ticket`, which does not implement the `Copy` trait
...
15 | println!("{}", ticket.redeem());
| -------- `ticket` moved due to this method call
16 | println!("{}", ticket.redeem());
| ^^^^^^ value used here after move
|
note: `Ticket::redeem` takes ownership of the receiver `self`, which moves `ticket`
--> consume_err.rs:6:15
|
6 | fn redeem(self) -> String {
| ^^^^
The note is the useful part: it points at the self in the method signature and says the method takes ownership of the receiver. Consuming methods are a deliberate design tool. A ticket that has been redeemed should not be redeemable again, and taking self turns "redeemed twice" into a compile error rather than a run-time check. When you see this error on your own types, ask whether the method really needs to consume the value. If not, change it to &self.
Reading the compiler error: E0502, a method borrows the whole struct
The borrow checker tracks fields separately when you access them directly, but a method call borrows the whole value. This does not compile:
struct Playlist {
name: String,
songs: Vec<String>,
}
impl Playlist {
fn add(&mut self, song: &str) {
self.songs.push(song.to_string());
}
}
fn main() {
let mut list = Playlist {
name: String::from("Focus"),
songs: Vec::new(),
};
let name = &list.name;
list.add("Raga Yaman");
println!("{name}: {} song(s)", list.songs.len());
}
error[E0502]: cannot borrow `list` as mutable because it is also borrowed as immutable
--> field_borrow_err.rs:18:5
|
17 | let name = &list.name;
| ---------- immutable borrow occurs here
18 | list.add("Raga Yaman");
| ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
19 | println!("{name}: {} song(s)", list.songs.len());
| ---- immutable borrow later used here
name borrows list.name. list.add(...) needs &mut list, all of it, because the compiler checks borrows using the method's signature, not its body. It does not look inside add to see that only songs changes. Accessing the field directly works, because then the compiler can see that name and songs are different fields:
struct Playlist {
name: String,
songs: Vec<String>,
}
fn main() {
let mut list = Playlist {
name: String::from("Focus"),
songs: Vec::new(),
};
let name = &list.name;
list.songs.push(String::from("Raga Yaman"));
println!("{name}: {} song(s)", list.songs.len());
}
Output:
Focus: 1 song(s)
Inside your own methods you can use this freely: in a &mut self method, let n = &self.name; self.songs.push(...) compiles. From outside, the usual fixes are to finish using the field reference before calling the method, to clone the small value you need (let name = list.name.clone();), or to split the struct so that the data you borrow and the data you change live in different structs.
Interview tip
When asked why a method takes &self, &mut self or self, answer in terms of what the caller can do afterwards: "&self lets many callers read at once, &mut self needs exclusive access but the caller keeps the value, and self hands the value over so it cannot be used again." Then mention the cost: none of the three copies the struct; the first two pass a pointer.
Multiple impl blocks and getters
A type can have several impl blocks, and they are combined. This is mostly used to group methods, and later to implement traits (each trait gets its own impl Trait for Type block).
You can give a method the same name as a field. rect.width reads the field, and rect.width() calls the method. This is how Rust writes getters: the field is private (lesson 10) and a pub fn width(&self) -> u32 method exposes it read-only. Unlike Java, where getters are routine, in Rust you add them only when you need to control access.
Structs in memory
A struct's fields are stored together, inline, in one block of memory. There is no object header, no hidden pointer to a class, and no boxing. A Rectangle is exactly two u32s, 8 bytes, wherever it lives: on the stack, inside a Vec, or inside another struct. Only the fields that are themselves heap-owning, like a String, point to the heap.
let user = User { name, email, age: 24, active: true };
stack (one block) heap
+---------------------------+
| name: ptr | len | cap |-------> "Priya"
| email: ptr | len | cap |-------> "priya@example.com"
| age: 24 |
| active: true |
+---------------------------+
Fields have alignment requirements: a u32 must start at an address that is a multiple of 4. To meet them, the compiler may insert padding bytes. With Rust's default layout, the compiler is also free to reorder fields to reduce padding, and it does not promise any particular order:
use std::mem::size_of;
#[allow(dead_code)]
struct Mixed {
a: u8,
b: u32,
c: u8,
}
#[allow(dead_code)]
#[repr(C)]
struct MixedC {
a: u8,
b: u32,
c: u8,
}
#[allow(dead_code)]
struct Wrapper(u64);
fn main() {
println!("Mixed = {} bytes", size_of::<Mixed>());
println!("MixedC = {} bytes", size_of::<MixedC>());
println!(
"Wrapper = {} bytes, u64 = {} bytes",
size_of::<Wrapper>(),
size_of::<u64>()
);
}
Output:
Mixed = 8 bytes
MixedC = 12 bytes
Wrapper = 8 bytes, u64 = 8 bytes
#[repr(C)]: declaration order kept (12 bytes)
| a | pad pad pad | b b b b | c | pad pad pad |
default layout: free to reorder (8 bytes with rustc 1.99)
| b b b b | a | c | pad pad |
#[repr(C)] asks for C's rules: fields in declaration order, each aligned, so a is followed by 3 bytes of padding, b takes 4, c takes 1, and the total is rounded up to a multiple of 4, giving 12. The default layout put the two u8s together, giving 8. Use #[repr(C)] only when you need a predictable layout, for example when sharing a struct with C code (lesson 19). The newtype Wrapper(u64) is 8 bytes, the same as u64, which confirms that newtypes are free.
Worked example: a shopping cart
Let us put the pieces together. The task: model a shopping cart that holds line items, merges repeated items, applies a percentage discount, and prints a receipt. Money is stored as an integer number of paise (1 rupee = 100 paise), because floating-point numbers cannot represent most decimal fractions exactly, and rounding errors in money are bugs.
Design decisions, made before writing code:
- A
LineItemowns its name (String), because the cart keeps items long after the caller's text is gone. - A
Cartowns aVec<LineItem>and a discount percentage stored asu8, since it can never exceed 100. - Adding an item and setting the discount change the cart, so they take
&mut self. Every total is calculated from the items on demand, so those methods take&self. Storing totals in fields would risk them getting out of date. addtakesname: &strbecause it only needs to read the text, and converts it to an ownedStringonly when a new item is created.
#[derive(Debug, Clone)]
struct LineItem {
name: String,
unit_price_paise: u64,
quantity: u32,
}
impl LineItem {
fn total_paise(&self) -> u64 {
self.unit_price_paise * u64::from(self.quantity)
}
}
#[derive(Debug, Default)]
struct Cart {
items: Vec<LineItem>,
discount_percent: u8,
}
impl Cart {
fn new() -> Self {
Self::default()
}
fn add(&mut self, name: &str, unit_price_paise: u64, quantity: u32) {
for item in self.items.iter_mut() {
if item.name == name {
item.quantity += quantity;
return;
}
}
self.items.push(LineItem {
name: name.to_string(),
unit_price_paise,
quantity,
});
}
fn set_discount(&mut self, percent: u8) {
self.discount_percent = percent.min(100);
}
fn subtotal_paise(&self) -> u64 {
self.items.iter().map(LineItem::total_paise).sum()
}
fn discount_paise(&self) -> u64 {
self.subtotal_paise() * u64::from(self.discount_percent) / 100
}
fn total_paise(&self) -> u64 {
self.subtotal_paise() - self.discount_paise()
}
fn item_count(&self) -> u32 {
self.items.iter().map(|item| item.quantity).sum()
}
}
fn rupees(paise: u64) -> String {
format!("{}.{:02}", paise / 100, paise % 100)
}
fn main() {
let mut cart = Cart::new();
cart.add("Notebook", 4_500, 3);
cart.add("Gel pen", 1_250, 4);
cart.add("Notebook", 4_500, 1);
cart.set_discount(10);
for item in &cart.items {
println!(
"{:<10} {:>2} x {:>6} = {:>7}",
item.name,
item.quantity,
rupees(item.unit_price_paise),
rupees(item.total_paise())
);
}
println!("items: {}", cart.item_count());
println!("subtotal: {}", rupees(cart.subtotal_paise()));
println!("discount: {}", rupees(cart.discount_paise()));
println!("total: {}", rupees(cart.total_paise()));
}
Output:
Notebook 4 x 45.00 = 180.00
Gel pen 4 x 12.50 = 50.00
items: 8
subtotal: 230.00
discount: 23.00
total: 207.00
Checking the numbers by hand:
| Step | Calculation | Result (paise) |
|---|---|---|
| Notebook added twice, merged | 3 + 1 = 4 units at 4,500 | 18,000 |
| Gel pens | 4 units at 1,250 | 5,000 |
| Item count | 4 + 4 | 8 items |
| Subtotal | 18,000 + 5,000 | 23,000 = Rs 230.00 |
| Discount | 23,000 × 10 / 100 | 2,300 = Rs 23.00 |
| Total | 23,000 − 2,300 | 20,700 = Rs 207.00 |
A few details worth noticing:
#[derive(Default)]onCartletsnewbe one line: an emptyVecand a discount of 0.addloops withiter_mut()to get&mut LineItemreferences, so it can increase the quantity of an existing item in place. The earlyreturnstops it from also pushing a duplicate.set_discountclamps the input withpercent.min(100), so the cart can never reach an invalid state through this method. Lesson 10 shows how making the fields private guarantees that no other code can bypass it.subtotal_paisepasses the methodLineItem::total_paisestraight tomap. A method is just a function whose first parameter is&LineItem, so the path works where a closure|item| item.total_paise()would.u64::from(self.quantity)widens au32tou64without the risk of truncation thatascarries in other directions.{:<10}left-aligns in 10 columns and{:>7}right-aligns in 7, which is how the receipt lines up.
Idiom: compute, do not cache
Prefer methods that calculate a value from the struct's fields over extra fields that store the result. Stored results must be updated by every method that changes the inputs, and forgetting one is a classic bug. Cache only when profiling shows the calculation is too slow.
Structs compared with other languages
| C struct | Java class | Python class | Rust struct | |
|---|---|---|---|---|
| Methods | No (free functions) | Yes, inside the class | Yes, inside the class | Yes, in separate impl blocks |
| Constructor | No | Built-in constructor | __init__ | Ordinary associated function, by convention new |
| Missing field | Garbage or zero | null or zero | Attribute does not exist | Compile error |
| Inheritance | No | Yes | Yes | No; use traits and composition |
| Stored | Inline | On the heap, behind a reference | On the heap, behind a reference | Inline, wherever you put it |
| Printing | Manual | toString() | __repr__, __str__ | Debug and Display traits |
The absence of inheritance surprises people from Java. Rust shares behaviour through traits (lesson 12) and shares data by putting one struct inside another, which is called composition.
Idioms and pitfalls
Pitfall: storing &str in a struct
Beginners often write struct User { name: &str } to avoid allocating. That needs a lifetime annotation (lesson 13) and ties every User to the string it borrows from. Until you have a specific reason, make struct fields owned: String, Vec<T>, and so on.
Pitfall: taking self when you meant &self
A method declared fn total(self) compiles fine, and the problem only shows up at the call site, where the value is moved on the first call. Unless the method converts or finishes the value, take &self.
Idiom: name constructors after what they do
Use new for the obvious constructor and descriptive names for the others: Rectangle::square(5), Vec::with_capacity(10), String::from_utf8(bytes). When a type has a sensible empty value, derive or implement Default as well. For a public type with a public new() that takes no arguments, Clippy's new_without_default lint reminds you.
Idiom: newtypes for meaning
Wrap raw numbers and strings that mean something specific, such as UserId(u64), Paise(u64) or Email(String). They cost nothing at run time and stop you passing one where another was expected.
Exercises
Exercise 1: temperatures
Write a Temperature struct that stores degrees Celsius, with an associated function from_fahrenheit, a method fahrenheit that converts back, and a method is_fever that returns true at 38.0 °C or above. Test it with 101.3 °F and 25 °C.
Solution
from_fahrenheit has no self, so it is an associated function; the other two only read, so they take &self.
#[derive(Debug)]
struct Temperature {
celsius: f64,
}
impl Temperature {
fn from_fahrenheit(f: f64) -> Self {
Self {
celsius: (f - 32.0) * 5.0 / 9.0,
}
}
fn fahrenheit(&self) -> f64 {
self.celsius * 9.0 / 5.0 + 32.0
}
fn is_fever(&self) -> bool {
self.celsius >= 38.0
}
}
fn main() {
let body = Temperature::from_fahrenheit(101.3);
println!("{:.1} C, fever: {}", body.celsius, body.is_fever());
let room = Temperature { celsius: 25.0 };
println!("{:.1} F, fever: {}", room.fahrenheit(), room.is_fever());
}
38.5 C, fever: true
77.0 F, fever: false
(101.3 − 32) × 5 / 9 = 69.3 × 5 / 9 = 38.5 °C, and 25 × 9 / 5 + 32 = 77 °F.
Exercise 2: a student's average
Write a Student with a name and a Vec<u32> of marks. Add new(name: &str), add_mark(&mut self, mark: u32) and average(&self) -> f64, which returns 0.0 when there are no marks.
Solution
struct Student {
name: String,
marks: Vec<u32>,
}
impl Student {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
marks: Vec::new(),
}
}
fn add_mark(&mut self, mark: u32) {
self.marks.push(mark);
}
fn average(&self) -> f64 {
if self.marks.is_empty() {
return 0.0;
}
let total: u32 = self.marks.iter().sum();
f64::from(total) / self.marks.len() as f64
}
}
fn main() {
let mut s = Student::new("Kavya");
println!("{} average before marks: {}", s.name, s.average());
s.add_mark(78);
s.add_mark(91);
s.add_mark(84);
println!("{} average: {:.2}", s.name, s.average());
}
Kavya average before marks: 0
Kavya average: 84.33
(78 + 91 + 84) / 3 = 253 / 3 = 84.33. The empty check avoids dividing by zero, which for floats would give NaN rather than a panic. f64 prints 0.0 as 0 with plain {}.
Exercise 3: a money newtype
Write a Copy newtype Rupees(u64) with add(self, other: Rupees) -> Rupees and split(self, people: u64) -> (Rupees, Rupees) that returns each person's share and the remainder. Split a bill of 850 + 395 between 4 people.
Solution
Because Rupees derives Copy, methods that take self copy the value instead of consuming it, so bill is still usable after bill.split(4).
#[derive(Debug, Clone, Copy, PartialEq)]
struct Rupees(u64);
impl Rupees {
fn add(self, other: Rupees) -> Rupees {
Rupees(self.0 + other.0)
}
fn split(self, people: u64) -> (Rupees, Rupees) {
(Rupees(self.0 / people), Rupees(self.0 % people))
}
}
fn main() {
let bill = Rupees(850).add(Rupees(395));
let (each, left_over) = bill.split(4);
println!("bill = {bill:?}");
println!("each pays {each:?}, left over {left_over:?}");
}
bill = Rupees(1245)
each pays Rupees(311), left over Rupees(1)
850 + 395 = 1,245; 1,245 / 4 = 311 remainder 1. Once you know traits, you would implement std::ops::Add instead so that + works.
Exercise 4: a bank account
Write an Account with an owner, a balance and a history of String entries. deposit adds money; withdraw returns false and records "declined" when the balance is too low. Deposit 5,000, then withdraw 1,200 and 9,000.
Solution
#[derive(Debug)]
struct Account {
owner: String,
balance: u64,
history: Vec<String>,
}
impl Account {
fn open(owner: &str) -> Self {
Self {
owner: owner.to_string(),
balance: 0,
history: Vec::new(),
}
}
fn deposit(&mut self, amount: u64) {
self.balance += amount;
self.history.push(format!("+{amount}"));
}
fn withdraw(&mut self, amount: u64) -> bool {
if amount > self.balance {
self.history.push(format!("declined {amount}"));
return false;
}
self.balance -= amount;
self.history.push(format!("-{amount}"));
true
}
}
fn main() {
let mut acc = Account::open("Rohan");
acc.deposit(5_000);
println!("withdraw 1200: {}", acc.withdraw(1_200));
println!("withdraw 9000: {}", acc.withdraw(9_000));
println!(
"{} has {}; history {:?}",
acc.owner, acc.balance, acc.history
);
}
withdraw 1200: true
withdraw 9000: false
Rohan has 3800; history ["+5000", "-1200", "declined 9000"]
Returning bool works, but it says nothing about why a withdrawal failed. Lesson 9 replaces it with a Result that carries a WithdrawError explaining the failure.
Exercise 5: explain the error
Without compiling, explain why the Playlist example with list.add("Raga Yaman") fails, and give two fixes that keep the add method.
Solution
name is a shared borrow of list.name that is used in the final println!. list.add takes &mut self, so it needs an exclusive borrow of the whole list, including list.name, while the shared borrow is alive: E0502. The compiler judges the call by the signature, not by what add actually touches. Fix one: print or finish using name before calling add. Fix two: clone the name first (let name = list.name.clone();), so name no longer borrows list.
Interview questions
Q1. What is the difference between a method and an associated function?
Both are defined in an impl block. A method has a self, &self or &mut self first parameter and is called on an instance with dot syntax (rect.area()). An associated function has no self and is called on the type with :: (Rectangle::new(1, 2)); constructors are the usual example. A method can also be called in associated-function style, as in Rectangle::area(&rect).
Q2. When should a method take self, &self or &mut self?
Take &self when the method only reads, &mut self when it changes the value in place, and self when it consumes or converts the value, such as into_... methods and builders. Choose the weakest receiver that works, because &self places the fewest restrictions on callers. Taking self on a non-Copy type means the caller cannot use the value afterwards.
Q3. Does Rust have constructors?
Not as a language feature. A struct is created with a struct literal that sets every field, and by convention a type provides an associated function such as new that builds one. You can have several, with descriptive names, and they can return Option or Result when construction can fail, which Java constructors cannot do without exceptions.
Q4. What is struct update syntax, and what is its ownership pitfall?
Type { field: value, ..other } fills the unlisted fields from other. Each such field is moved or copied like an assignment, so non-Copy fields such as String move out of other, leaving it partially moved. You can still use other's remaining fields but not other as a whole or the moved fields.
Q5. What is a tuple struct, and what is the newtype pattern?
A tuple struct has a name and unnamed positional fields accessed as .0, .1. A newtype is a tuple struct with one field, such as struct Metres(f64). It has the same memory layout as the inner type, so it is free at run time, but the compiler treats it as a distinct type, which prevents mixing up units or IDs and lets you implement traits on it.
Q6. Why does printing a struct with {:?} fail until you add #[derive(Debug)]?
{:?} requires the Debug trait, and Rust does not implement traits for your types automatically. #[derive(Debug)] generates an implementation that prints the type name and each field, which requires every field type to implement Debug too. For user-facing {} output you implement Display by hand.
Q7. Can you make only one field of a struct mutable?
No. Mutability is a property of the binding or reference: through a mut binding or a &mut reference, every field can be changed, and through an immutable one, none can. To protect individual fields you make them private and expose methods, or use interior mutability types such as Cell and RefCell (lesson 15) in the rare cases that need it.
Q8. Why can you borrow two fields of a struct separately but not call a &mut self method while a field is borrowed?
The borrow checker tracks direct field accesses as separate places, so &s.a and &mut s.b can coexist. A method call is checked only by its signature: &mut self borrows the entire struct, and the compiler does not look into the method body to see which fields it touches. Keeping function signatures as the contract makes compile times and error messages predictable.
Q9. How is a Rust struct laid out in memory?
Fields are stored inline in one block, with padding to satisfy alignment, and no hidden header or class pointer. With the default repr(Rust) layout the compiler may reorder fields to reduce padding and makes no layout guarantee. #[repr(C)] keeps declaration order and C's rules, which is needed for FFI.
Q10. Does Rust support inheritance?
No. Rust shares behaviour through traits, which can have default method implementations, and shares data through composition, putting one struct inside another. Polymorphism comes from generics (static dispatch) or trait objects (dynamic dispatch). This avoids the fragile base class problem and deep hierarchies.
Key takeaways
- A struct groups named fields into a new type; every field must be initialised, and mutability applies to the whole instance.
- Field init shorthand and
..otherreduce repetition; update syntax moves non-Copyfields and can leave the source partially moved. - Tuple structs have positional fields; a newtype wraps one value at zero cost to give it a distinct type; unit structs have no data.
#[derive(Debug)]enables{:?},{:#?}anddbg!; implementDisplayyourself for{}.- Methods live in
implblocks; associated functions withoutself, such asnew, are called withType::. &selfreads,&mut selfchanges in place, andselfconsumes; choose the weakest that works.- A
&mut selfmethod borrows the whole struct, so it conflicts with any outstanding borrow of a field. - Structs are stored inline without headers; the default layout may reorder fields, and
#[repr(C)]fixes the order.
Next lesson
Continue with Enums and pattern matching in Rust.

