Rust track

Lesson 11 · Part 3: Structs, Enums & Traits

Structs

Defining structs, creating instances, methods and associated functions

Part 3: Structs, Enums & Traits5 min readFree lesson

Structs are the most common tool to represent a domain by grouping multiple pieces of data into a single type. Rust structs come in three forms — named structs with named fields, tuple structs with only positions, and unit structs without fields. Each has its suitable use case, and you can attach methods using the impl block to use them in an object-oriented manner.

Learning Objectives

  • Define named/tuple/unit structs and create instances.
  • Define and use methods and associated functions in the impl block.
  • Understand the difference between Self and &self / &mut self / self.
  • Understand the meaning of frequently used derive macros such as #[derive(Debug)].

Key Concepts

1) Named-field struct

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

let r = Rectangle { width: 30, height: 20 };
println!("{}", r.width);

Since it has field names, its intention is clear. It is used most frequently.

2) Tuple struct

struct Color(u8, u8, u8);
let red = Color(255, 0, 0);
red.0 == 255;

Access is by position without field names. Even if the shapes of types are the same, if the names are different, they are not compatible, making it useful for the newtype pattern.

3) Unit struct

struct AlwaysReady;

A type with no fields. Used as a marker for trait implementation, as a "state" in a state machine, or for empty handles.

4) impl block and methods

impl Rectangle {
    fn new(w: u32, h: u32) -> Self { Self { width: w, height: h } }
    fn area(&self) -> u32 { self.width * self.height }
}
  • Self is an alias referring to the type being implemented (Rectangle).
  • If the first argument is of the self series, method; if not, associated function (such as a constructor).
  • &self is borrowing, &mut self is mutable borrowing, self is ownership-taking method.

5) derive Macro

Frequently used traits can be automatically implemented with macros. The most common one at the beginner level is Debug.

#[derive(Debug)]
struct Point { x: i32, y: i32 }
let p = Point { x: 1, y: 2 };
println!("{:?}", p); // Point { x: 1, y: 2 }

Examples

Example 1 — ex01_named.rs : Rectangle and Methods

//Example 1 of Chapter 11: Named-field structures and methods (impl)
//
//A structure is the most common tool to express a domain concept as a type.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    //Associated function — Constructors like Self::new
    fn new(w: u32, h: u32) -> Self {
        Self { width: w, height: h }
    }

    //Method — The first argument is &self
    fn area(&self) -> u32 {
        self.width * self.height
    }

    //Method to compare with another rectangle
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let r1 = Rectangle::new(30, 20);
    let r2 = Rectangle { width: 10, height: 5 }; //Direct field initialization is also possible

    println!("Width of {:?} = {}", r1, r1.area());
    println!("Does {:?} contain {:?}? {}", r1, r2, r1.can_hold(&r2));
}

Example 2 — ex02_tuple.rs : Tuple Struct

//Example 11-2: Tuple Struct — Has a name but no field names

#[derive(Debug)]
struct Color(u8, u8, u8);

#[derive(Debug)]
struct Point(f64, f64);

impl Color {
    fn brightness(&self) -> u16 {
        //Adding u8 risks overflow → Expand to u16
        self.0 as u16 + self.1 as u16 + self.2 as u16
    }
}

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

    println!("red = {:?}, brightness = {}", red, red.brightness());
    println!("origin = ({}, {})", origin.0, origin.1);

    //Tuple structs are not compatible with each other if their types are different
    // let x: Color = origin; // ❌
}

Example 3 — ex03_unit.rs : Unit Struct and State Type

//Example 3 of Chapter 11: Unit Struct — Type with No Fields
//
//Often used for markers for trait implementations, state machine indicators, etc.

struct AlwaysReady; //No fields

impl AlwaysReady {
    fn ping(&self) -> &'static str {
        "pong"
    }
}

//A pattern that expresses "state" itself as a type
struct Connected;
struct Disconnected;

fn describe<T>(_state: &T) -> &'static str
where
    T: HasName,
{
    T::name()
}

trait HasName {
    fn name() -> &'static str;
}

impl HasName for Connected {
    fn name() -> &'static str { "Connected" }
}

impl HasName for Disconnected {
    fn name() -> &'static str { "Disconnected" }
}

fn main() {
    let svc = AlwaysReady;
    println!("svc.ping() = {}", svc.ping());

    let on = Connected;
    let off = Disconnected;
    println!("on  → {}", describe(&on));
    println!("off → {}", describe(&off));
}

Common Mistakes

Q. I'm confused between Self and self inside impl.

A. Uppercase Self is the name of the type, lowercase self is the name of the instance. Self { x: 0 } means "create a new instance of this type", self.x refers to "the x of the current instance".

Q. Can I use impl multiple times in the same struct?

A. Yes. It's often used when writing code in separated modules. The compiler collects all of them and treats them as a single definition.

Q. {:?} does not compile without #[derive(Debug)].

A. Correct. Implementation of the Debug trait is required. Usually, it's automatically implemented with derive, and manual impl std::fmt::Debug is needed only when customization is required.

Q. I want to update only some fields and use the existing values for the rest.

A. Use struct update syntax ...

let r2 = Rectangle { width: 100, ..r1 };

However, note that if it includes non-Copy fields, r1 might be moved.

Summary

  • Three kinds: named-field / tuple / unit structs. Choose according to the situation.
  • Attach methods and associated functions with a impl block.
  • Self(type) ≠ self(instance). Use &self / &mut self / self according to intent.
  • Automatically implement common traits with #[derive(Debug, Clone, ...)].

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/structs/Cargo.tomlTOML
[package]
name = "structs"
version = "0.1.0"
edition = "2021"
default-run = "structs"

[dependencies]
src/structs/src/main.rsRust
fn main() {
    println!("This crate has three executable examples.");
    println!();
    println!("  cargo run --bin ex01_named");
    println!("  cargo run --bin ex02_tuple");
    println!("  cargo run --bin ex03_unit");
}

Assignment

Problem 1 — Area of a Circle

  • File name: homework_01/src/main.rs
  • Key concepts: named-field struct, impl, associated functions

Requirements

  • Define the Circle { radius: f64 } struct.
  • Define the associated function Circle::new(r: f64) -> Self.
  • Define the methods area(&self) -> f64, circumference(&self) -> f64.
  • Use std::f64::consts::PI for Pi.
  • In main, create a circle with radius 5.0 and print its area and circumference.

Expected output

Area: 78.54
Perimeter: 31.42

Hint

  • area = π * r², circumference = 2 * π * r.
  • f64 exponentiation: r.powi(2) or r * r.
  • Format: {:.2}.

Problem 2 — Rgb Tuple Struct and Color Inversion

  • File name: homework_02/src/main.rs
  • Key concepts: tuple struct, impl

Requirements

  • Define struct Rgb(u8, u8, u8);.
  • Method invert(&self) -> Rgb returns a new color that is (255-r, 255-g, 255-b).
  • In main, print the result of inverting red (255,0,0).

Expected output

Original: Rgb(255, 0, 0)
Inverted: Rgb(0, 255, 255)

Hint

  • #[derive(Debug)].
  • New instance: Rgb(255 - self.0, 255 - self.1, 255 - self.2).

Check your answers

Try solving it yourself first, then compare your answer with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/Cargo.tomlTOML
[package]
name = "homework_01"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_01/src/main.rsRust
//Key Points:
//- Associated functions like `Self::new` are common constructor patterns.
//- `std::f64::consts::PI` is a standard library constant with high accuracy.

use std::f64::consts::PI;

struct Circle {
    radius: f64,
}

impl Circle {
    fn new(r: f64) -> Self {
        Self { radius: r }
    }

    fn area(&self) -> f64 {
        PI * self.radius * self.radius
    }

    fn circumference(&self) -> f64 {
        2.0 * PI * self.radius
    }
}

fn main() {
    let c = Circle::new(5.0);
    println!("Area: {:.2}", c.area());
    println!("Perimeter: {:.2}", c.circumference());
}
homework_02/Cargo.tomlTOML
[package]
name = "homework_02"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_02/src/main.rsRust
//Key Points:
//- Accessing fields of a tuple struct is index-based, like `.0`, `.1`, `.2`.
//- If you attach `#[derive(Debug)]`, it can be printed nicely with `{:?}`.

#[derive(Debug)]
struct Rgb(u8, u8, u8);

impl Rgb {
    fn invert(&self) -> Rgb {
        Rgb(255 - self.0, 255 - self.1, 255 - self.2)
    }
}

fn main() {
    let red = Rgb(255, 0, 0);
    let inv = red.invert();
    println!("Original: {:?}", red);
    println!("Reversed: {:?}", inv);
}

Learn this lesson with a mentor

Work through the Rust track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now