Updated Aug 5, 2026

06. Structs and Methods

📋 Jump to Takeaways

🎁 In most languages, you group related data into objects and hope inheritance doesn't turn into a tangled mess. Rust gives you structs instead: a way to model your data precisely, attach behavior directly to it, and skip the inheritance chaos entirely.

Defining Structs

A struct groups related fields under one name. You define the shape once, then create as many instances as you need.

struct User {
    username: String,
    email: String,
    active: bool,
    login_count: u64,
}

Each field has a name and a type. Unlike tuples, you access fields by name, so there's no guessing which index holds what.

Creating Instances

You create a struct instance by providing values for every field. Order doesn't matter, but you can't skip any.

struct User {
    username: String,
    email: String,
    active: bool,
    login_count: u64,
}

fn main() {
    let user = User {
        email: String::from("[email protected]"),
        username: String::from("alice"),
        active: true,
        login_count: 1,
    };

    println!("{}", user.username); // alice
}

Field Init Shorthand

When a variable has the same name as a struct field, you can skip the repetition.

struct User {
    username: String,
    email: String,
    active: bool,
    login_count: u64,
}

fn build_user(username: String, email: String) -> User {
    User {
        username,   // same as username: username
        email,      // same as email: email
        active: true,
        login_count: 0,
    }
}

fn main() {
    let user = build_user(String::from("bob"), String::from("[email protected]"));
    println!("{} <{}>", user.username, user.email); // bob <[email protected]>
}

This keeps your constructors clean, especially when many parameters match field names.

Methods with impl Blocks

You attach behavior to a struct using an impl block. Methods take &self to read or &mut self to modify.

struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }
}

fn main() {
    let mut rect = Rectangle { width: 10.0, height: 5.0 };
    println!("{}", rect.area()); // 50
    rect.scale(2.0);
    println!("{}", rect.area()); // 200
}

&self borrows immutably, so you can read but not change. &mut self borrows mutably, so you can modify fields.

Associated Functions

Functions inside impl that don't take self are associated functions. You call them with :: syntax, and they're perfect for constructors.

struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn square(size: f64) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Rectangle::square(5.0);
    println!("{}", sq.area()); // 25
}

Self (capital S) is an alias for the type the impl block belongs to. Don't confuse it with self (lowercase), which is the instance. Both are keywords:

  • self — the current instance (like this in other languages). Used as a parameter: &self, &mut self, self
  • Self — the current type name. Used in return types and constructors so you don't repeat the type name

Tuple Structs

When you want a named type but don't need field names, use a tuple struct.

struct Color(u8, u8, u8);
struct Point(f64, f64, f64);

fn main() {
    let red = Color(255, 0, 0);
    let origin = Point(0.0, 0.0, 0.0);

    println!("Red channel: {}", red.0); // 255
    println!("X: {}", origin.0);        // 0
}

Color and Point are distinct types even though both hold three numbers. The compiler won't let you mix them up.

Unit Structs

Sometimes you don't need any fields at all, just a distinct type you can attach behavior to. A unit struct has no data and takes zero bytes at runtime. It's not the same as () (the unit type). () is generic nothingness, while a unit struct is a named type the compiler can distinguish from other types.

struct Meters;
struct Kilometers;

trait Unit {
    fn name(&self) -> &str;
}

impl Unit for Meters {
    fn name(&self) -> &str {
        "m"
    }
}

impl Unit for Kilometers {
    fn name(&self) -> &str {
        "km"
    }
}

fn main() {
    let m = Meters;
    println!("{}", m.name()); // m
}

Meters and Kilometers carry no data, they exist purely as type-level markers. You can still implement traits on them, so the compiler can tell "a distance measured in meters" apart from "a distance measured in kilometers" even though neither struct stores anything.

Key Takeaways

  • Structs group named fields; use field shorthand when variable names match
  • impl block attaches methods (&self, &mut self) and associated functions (no self)
  • Self refers to the implementing type; use it in constructors
  • Tuple structs give you named types without field names
  • Unit structs (struct Marker;) carry no data, use them as zero-sized type markers you can implement traits on

🎁 You've seen structs model "this AND that" (a user has a name AND an email AND a login count). But what about "this OR that"? What if a value could be one of several completely different shapes? That's what enums do, and Rust's version is far more powerful than what you've seen in other languages.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy