A struct groups related fields under a single named type. Once you have
a struct, you can attach methods to it with an impl block.
struct User {
email: String,
name: String,
is_verified: bool,
}
impl User {
/// Associated function: no `self`. Called as `User::new(...)`.
fn new(email: String, name: String) -> Self {
User {
email,
name,
is_verified: false,
}
}
/// Method: takes `&self`, called as `user.display_name()`.
fn display_name(&self) -> String {
format!("{} ({})", self.name, self.email)
}
/// Mutating method: takes `&mut self`.
fn verify(&mut self) {
self.is_verified = true;
}
}
The three flavors of self are the heart of methods:
&self reads the struct without modifying it. Most methods.&mut self modifies the struct in place. Requires the caller to have a
mutable binding.self (no reference) consumes the struct, taking ownership. Use this
when the method returns a transformed version and the original
shouldn't be reused.Field access uses dot notation (user.name). Inside impl you write
self.field for the same thing.
Self (capital S) is shorthand for "the type I'm impling". User and
Self are interchangeable inside impl User.
0..5One of the tests in this chapter calls record_login() 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(..).
Here we model a User and write the constructor that establishes
the sensible starting state: a brand-new account is unverified and
has zero logins recorded.
Useful from the standard library
- The Rust Book on structs covers struct literal syntax, the field shorthand (
User { email, name, .. }), and tuple/unit structs.#[derive(Debug)]and#[derive(PartialEq)]are already on the struct: that's what makes the test'sassert_eq!work and what would let youprintln!("{user:?}").Self(capital S) is interchangeable with the struct's name inside animplblock. ReturningSelfkeeps the constructor signature stable if you ever rename the type.
A method taking &self reads the struct's fields without modifying
or consuming it. The most common kind. 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. It's the
idiomatic tool for this and reads exactly like the format you want.
Useful from the standard library
format!builds a newStringfrom a template and arguments. Same syntax asprintln!, but returns the string instead of printing it.- Field access uses dot notation:
self.name,self.email. Inside aformat!template you can interpolate them inline:format!("{} ({})", self.name, self.email).
When a method needs to change the struct's data, it takes
&mut self. The caller must hold the value in a mut binding for
this to compile. Mutability is opt-in at every layer.
record_login bumps a counter and flips a flag. The flag write is
idempotent: setting is_verified = true on an already-verified
user is harmless, which keeps the code simpler than a branch.
Useful from the standard library
- The
+= 1operator updates a numeric field in place; the same works throughself. NoCellor special-casing required.- Plain assignment (
self.is_verified = true) is enough for the bool. There's no separate "setter" syntax in Rust; methods on&mut selfjust assign.u32::checked_addis the safe-overflow alternative if you're worried about wrapping. Not needed here.
Methods are a natural place to encode business rules that span
several fields. Rather than scattering user.is_verified && ..
checks across the codebase, name the rule once on the type so the
intent is obvious at every call site.
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.
Useful from the standard library
- The
&&operator short-circuits: if the left side isfalse, the right side isn't evaluated. Cheap and matches what you'd write in any other language.- The body is a single expression, so leave off the trailing semicolon.
self.is_verified && self.login_count >= 5is a complete function body.- The expression is idempotent for the caller (
&self), so it's safe to call as many times as you want without worrying about accidental mutation.
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 the literal syntaxUser { email, name, .. }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. The same ownership rules from chapter 12 apply.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. Reach forDefault,Clone, andCopywhen they fit.- Encoding business rules as predicates on the type (
user.can_access_premium()) keeps the rule in one place and makes call sites self-documenting.