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.
#[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.login_count, 0);
assert_eq!(user.is_verified, false);
}
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).
#[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.
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.
#[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);
}
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.
#[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);
// Record enough logins
for _ in 0..5 {
user.record_login();
}
// Now has premium access
assert_eq!(user.can_access_premium(), true);
}
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 the moves and borrowing chapters 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.