A struct works as a waiter at a bar.
“Why do you look so sad?” asks a colleague.
“A tuple just treated me like an object.”
“Don’t worry. It has no class.”
In Rust, a method's signature tells you whether it borrows a value, changes it,
or takes ownership of it. You group related fields in a struct and attach
methods with an impl block.
struct Playlist {
tracks: Vec<String>,
}
impl Playlist {
/// No receiver is needed to create an empty playlist.
fn new() -> Self {
Self { tracks: Vec::new() }
}
/// A shared borrow is enough to inspect the playlist.
fn len(&self) -> usize {
self.tracks.len()
}
/// Adding a track changes the playlist, so it needs a mutable borrow.
fn add(&mut self, track: String) {
self.tracks.push(track);
}
}
let mut playlist = Playlist::new();
playlist.add("Blue in Green".to_string());
assert_eq!(playlist.len(), 1);
The form of self tells you what access the method receives:
&self takes a shared borrow. You'll use this form for methods that only need
to inspect fields.&mut self takes a mutable borrow, allowing the method to modify the struct
in place. The caller therefore needs a mutable binding.self (no reference) consumes the struct, taking ownership. Choose this form
when the method returns a transformed value and the original shouldn't be
reused.Field access uses dot notation (playlist.tracks). Inside impl you write
self.field for the same thing.
Self (capital S) is shorthand for "the type I'm impling". Playlist and
Self are interchangeable inside impl Playlist.
0..5The record_login test calls the method five times in a loop:
for _ in 0..5 {
user.record_login();
}
0..5 is a range expression. It produces the values 0, 1, 2, 3, 4
(end-exclusive). 0..=5 is the inclusive variant if you want 5 too. The loop
variable is _ here because the body doesn't need it; we just want "do this
thing five times." Ranges are useful as iterators but also work as slice indices
(v[0..3]).
From this chapter onward the files get longer, and the in-browser editor starts feeling cramped. The Open in Web Editor button above each editor opens this file on github.dev, a full browser-based VS Code with proper find-in-file, multi-cursor, and the rest of the keyboard shortcuts you'd expect. Clone the repo locally if you want
rust-analyzerand on-save formatting.
A struct groups related fields under one name. Rust has no built-in
constructors; the convention is an associated function called new that returns
Self. "Associated" means it lives in the impl block but doesn't take self.
You call it as User::new(..).
You'll write a constructor for User so every new account starts unverified,
with zero logins recorded.
Useful Resources
- The Rust Book on structs covers struct literals and field shorthand: write
email: emailwhen the names match.Self(capital S) is interchangeable with the struct's name inside animplblock. ReturningSelfkeeps the constructor signature stable if you ever rename the type.
#[derive(Debug, PartialEq)]
struct User {
email: String,
name: String,
is_verified: bool,
login_count: u32,
}
impl User {
/// Creates a new user account. New users start unverified with 0 logins.
fn new(email: String, name: String) -> Self {
Self {
email,
name,
is_verified: false,
login_count: 0,
}
}
}
#[test]
fn test_new() {
let user = User::new("alice@example.com".to_string(), "Alice".to_string());
assert_eq!(user.email, "alice@example.com");
assert_eq!(user.name, "Alice");
assert_eq!(user.login_count, 0);
assert_eq!(user.is_verified, false);
}
A method taking &self reads the struct's fields without modifying or consuming
it. You'll use this form whenever a method only needs to inspect the value.
Inside the method, self behaves like any other reference, so you can read
fields freely and the caller keeps ownership.
display_name formats two fields into a new String. Use format! rather than
building the string by hand.
Useful from the Standard Library
format!builds a newStringfrom a template and arguments. It uses the same syntax asprintln!, but returns the string instead of printing it.- Field access uses dot notation:
self.name,self.email.
#[derive(Debug, PartialEq)]
struct User {
email: String,
name: String,
is_verified: bool,
login_count: u32,
}
impl User {
const fn new(email: String, name: String) -> Self {
Self {
email,
name,
is_verified: false,
login_count: 0,
}
}
/// Returns the user's display name for the UI. Format: "{name} ({email})"
fn display_name(&self) -> String {
format!("{} ({})", self.name, self.email)
}
}
#[test]
fn test_display_name() {
let user = User::new("alice@example.com".to_string(), "Alice".to_string());
assert_eq!(user.display_name(), "Alice (alice@example.com)");
}
When a method needs to change the struct's data, it takes &mut self. Declare
user with let mut so the method can borrow it mutably. At
user.record_login(), Rust supplies the &mut borrow automatically.
record_login bumps a counter and flips a flag. You can set
is_verified = true even if the user is already verified. That assignment is
idempotent, so you don't need a branch to check the flag first.
+= 1 operator updates a numeric field in place; the same works through
self.self.is_verified = true) is enough for the bool. There's
no separate "setter" syntax in Rust; methods on &mut self just assign.
#[derive(Debug, PartialEq)]
struct User {
email: String,
name: String,
is_verified: bool,
login_count: u32,
}
impl User {
const fn new(email: String, name: String) -> Self {
Self {
email,
name,
is_verified: false,
login_count: 0,
}
}
/// Records a successful login attempt.
///
/// Increments `login_count` and sets `is_verified` to `true`. The
/// verification flag is idempotent: setting it on every login is fine
/// because once you're verified you stay verified. (A real system would
/// only set it on first login; we keep it simple here.)
fn record_login(&mut self) {
self.login_count += 1;
self.is_verified = true;
}
}
#[test]
fn test_record_login() {
let mut user = User::new("alice@example.com".to_string(), "Alice".to_string());
user.record_login();
assert_eq!(user.login_count, 1);
assert_eq!(user.is_verified, true);
user.record_login();
assert_eq!(user.login_count, 2);
// Verification stays on across subsequent logins (idempotent).
assert!(user.is_verified);
}
If you keep writing user.is_verified && .. in different places, give that rule
a name. A method lets you keep the checks together and tells callers what
they're asking: can this user access premium features?
can_access_premium combines two conditions into a single bool. In Rust, the
body of a function is an expression, so you can just write the boolean
expression with no return and no semicolon.
&& operator short-circuits, so a false condition on the left skips the
condition on the right. Here an unverified user doesn't need a login-count
check.return for the boolean.&self means callers can ask the question without giving up ownership
or providing a mutable borrow.
#[derive(Debug, PartialEq)]
struct User {
email: String,
name: String,
is_verified: bool,
login_count: u32,
}
impl User {
const fn new(email: String, name: String) -> Self {
Self {
email,
name,
is_verified: false,
login_count: 0,
}
}
const fn record_login(&mut self) {
self.login_count += 1;
self.is_verified = true;
}
/// Checks if user can access premium features. Requires verification and at
/// least 5 logins.
fn can_access_premium(&self) -> bool {
self.is_verified && self.login_count >= 5
}
}
#[test]
fn test_can_access_premium() {
let mut user = User::new("alice@example.com".to_string(), "Alice".to_string());
// Not enough logins yet
assert_eq!(user.can_access_premium(), false);
user.record_login();
assert_eq!(user.can_access_premium(), false);
for _ in 0..3 {
user.record_login();
}
// Verified, but still one login short.
assert_eq!(user.can_access_premium(), false);
user.record_login();
assert_eq!(user.can_access_premium(), true);
user.record_login();
assert_eq!(user.can_access_premium(), true);
// Enough logins alone cannot grant access if verification is revoked.
user.is_verified = false;
assert_eq!(user.can_access_premium(), false);
}
You defined a struct, wrote a new constructor, added a &self method that
formatted fields into a String, mutated state with &mut self, and combined
two fields into a predicate.
What We Learned
- A
structgroups related fields under a single named type. Build instances with a struct literal, supplying every field, and read fields with dot notation.- An
implblock attaches functions to the type. Withoutself, it's an associated function (called asUser::new(..)); withself, it's a method (called asuser.method()).- The three flavors of
selfsay what the method intends to do:&selfreads,&mut selfmutates in place, plainselfconsumes. These are the same ownership choices you've already made with function parameters.Self(capital S) inside animplblock is shorthand for the type. ReturningSelfkeeps the constructor signature stable if the type is later renamed.format!is the idiomatic way to build aStringfrom a template; same syntax asprintln!but returns the string.#[derive(Debug, PartialEq)]covers the common pair:{:?}printing for debugging and==for tests. DeriveDefault,Clone, orCopyonly when the generated behavior matches the type.- Encoding business rules as predicates on the type (
user.can_access_premium()) keeps the checks in one place and gives callers a name for the rule.
Extra practice to explore at your own pace. These chapters do not count toward course progress.