Chapter 13

Structs and Methods

👋 Anyone can read and edit this exercise. Sign up to save your progress.

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:

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.

A note on ranges: 0..5

One 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-analyzer and on-save formatting.

Defining a struct and a constructor

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's assert_eq! work and what would let you println!("{user:?}").
  • Self (capital S) is interchangeable with the struct's name inside an impl block. Returning Self keeps the constructor signature stable if you ever rename the type.
Exercise 1 of 4
Open in Web Editor

Results

    Compiler / runtime output
    
                

    Methods that borrow `&self`

    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 new String from a template and arguments. Same syntax as println!, but returns the string instead of printing it.
    • Field access uses dot notation: self.name, self.email. Inside a format! template you can interpolate them inline: format!("{} ({})", self.name, self.email).
    Exercise 2 of 4
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  

      Methods that mutate via `&mut self`

      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 += 1 operator updates a numeric field in place; the same works through self. No Cell or 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 self just assign.
      • u32::checked_add is the safe-overflow alternative if you're worried about wrapping. Not needed here.
      Exercise 3 of 4
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    

        Predicates over multiple fields

        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 is false, 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 >= 5 is 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.
        Exercise 4 of 4
        Open in Web Editor

        Results

          Compiler / runtime output
          
                      

          Wrapping up structs and methods

          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 struct groups related fields under a single named type. Build instances with the literal syntax User { email, name, .. } and read fields with dot notation.
          • An impl block attaches functions to the type. Without self, it's an associated function (called as User::new(..)); with self, it's a method (called as user.method()).
          • The three flavors of self say what the method intends to do: &self reads, &mut self mutates in place, plain self consumes. The same ownership rules from chapter 12 apply.
          • Self (capital S) inside an impl block is shorthand for the type. Returning Self keeps the constructor signature stable if the type is later renamed.
          • format! is the idiomatic way to build a String from a template; same syntax as println! but returns the string.
          • #[derive(Debug, PartialEq)] covers the common pair: {:?} printing for debugging and == for tests. Reach for Default, Clone, and Copy when 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.
          Next chapter 14Traits