Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Behavior Lives in Impl Blocks

Behavior is separate from data - it lives in impl blocks. You can have multiple impl blocks for the same type, organized however makes sense.

struct Player {
    name: String,
    health: i32,
    max_health: i32,
}

Associated Functions

Associated functions don't take self - they're like static methods in C#. By convention, new is the constructor name:

impl Player {
    fn new(name: String) -> Player {
        Player {
            name,
            health: 100,
            max_health: 100,
        }
    }
}

In C#, static methods are often used because there are no free functions - everything must belong to a class. This leads to "static utility classes" that are really just namespaces for functions:

// C# - static class as a container for functions
public static class MathUtils {
    public static double Lerp(double a, double b, double t) { ... }
    public static double Clamp(double value, double min, double max) { ... }
}

In Rust, the equivalent of a C# static class is simply a module (mod) with free functions and constants. Types should be containers for data (and highly related behavior), not just containers for behavior:

// Rust - module with free functions
mod math_utils {
    pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
        a + (b - a) * t
    }
    
    pub fn clamp(value: f64, min: f64, max: f64) -> f64 {
        if value < min { min } else if value > max { max } else { value }
    }
}

Use associated functions on types when the function is conceptually tied to the type itself (like constructors, or type-specific utilities). Use free functions in modules when the behavior is more general-purpose or doesn't conceptually "belong" to a single type.


The Self Parameter

The self parameter is explicit - there's no hidden this. This tells you exactly how data flows:

impl Player {
    // &self = borrows immutably (read-only access)
    fn is_alive(&self) -> bool {
        self.health > 0
    }

In C#, methods are an inherent part of a class - this is magical and implicit. In Rust, methods are really just syntax sugar for bare functions that happen to take Self as their first parameter:

impl Player {
    // This method...
    fn is_alive(&self) -> bool {
        self.health > 0
    }
}

// ...is essentially equivalent to this free function:
fn is_alive(player: &Player) -> bool {
    player.health > 0
}

The only difference is namespacing and call syntax. Both work:

struct Player {
    name: String,
    health: i32,
    max_health: i32,
}

impl Player {
    fn new(name: String) -> Player {
        Player { name, health: 100, max_health: 100 }
    }
}

impl Player {
    fn take_damage(&mut self, amount: i32) {
        self.health = (self.health - amount).max(0);
    }
}

fn main() {
    let mut player = Player::new(String::from("Alice"));
    
    // Method syntax - what you'll usually write
    player.take_damage(10);
    
    // But this is exactly equivalent - calling it as a function!
    Player::take_damage(&mut player, 20);
    
    println!("{} has {} health", player.name, player.health);
}

This isn't just an implementation detail - it reinforces that behavior is attached to data, not embedded in it. The impl block is organizational, not fundamental.


Self Variants

The three forms of self control ownership, just like any other parameter:

impl Player {
    // &self = borrows immutably (read-only access)
    fn is_alive(&self) -> bool {
        self.health > 0
    }

    fn health_percentage(&self) -> f32 {
        self.health as f32 / self.max_health as f32 * 100.0
    }

    // &mut self = borrows mutably (read-write access)
    fn heal(&mut self, amount: i32) {
        self.health = (self.health + amount).min(self.max_health);
        println!("{} heals {} HP! Health: {}", self.name, amount, self.health);
    }

    // self (no &) = takes ownership, consuming the value
    // after this call, self is destroyed and can't be used again
    fn into_name(self) -> String {
        self.name
    }
}

In Rust, there are conventional method name prefixes used to indicate some of these ownership semantics:

PrefixParameterMeaning
into_selfconsumes and transforms
as_&selfconverts a reference to ourself into another type
to_&selfcreates an owned copy of ourself as a different type

This does not mean that all methods with this kind of self parameter use this prefix, but it's a common pattern used for transformative methods.

This even goes beyond just ownership and standard borrows. You can have self parameters use other smart pointers too, to enforce certain usage patterns:

use std::sync::Arc;

pub struct Cache {
    val: usize
}

impl Cache {
    pub fn read(self: Arc<Self>) -> usize { self.val }
}

pub fn main() {
    let cache = Arc::new(Cache { val: 42 });
    println!("Value: {:?}", cache.read());
}

The main function would fail to compile if we were to call cache.read() without wrapping self in Arc:

use std::sync::Arc;
pub struct Cache {
    val: usize
}
impl Cache {
    pub fn read(self: Arc<Self>) -> usize { self.val }
}
pub fn main() {
    let cache = Cache { val: 42 };
    println!("Value: {:?}", cache.read());
}

Use this with care - You're enforcing a specific ownership model on users of your type. Only do this when it's really necessary for safety or correctness.


Using Methods

struct Player {
    name: String,
    health: i32,
    max_health: i32,
}

impl Player {
    fn new(name: String) -> Player {
        Player { name, health: 100, max_health: 100 }
    }
    fn is_alive(&self) -> bool { self.health > 0 }
    fn health_percentage(&self) -> f32 {
        self.health as f32 / self.max_health as f32 * 100.0
    }
    fn take_damage(&mut self, amount: i32) {
        self.health = (self.health - amount).max(0);
    }
    fn heal(&mut self, amount: i32) {
        self.health = (self.health + amount).min(self.max_health);
        println!("{} heals {} HP! Health: {}", self.name, amount, self.health);
    }
    fn into_name(self) -> String { self.name }
}

fn main() {
    // Call associated function with :: syntax
    let mut player = Player::new(String::from("Alice"));

    // &self methods - can call multiple times, player is just borrowed
    println!("{} at {}% health", player.name, player.health_percentage());
    println!("Alive? {}", player.is_alive());

    // &mut self method - needs `mut` binding to call
    player.heal(10);

    // You can also call methods as functions - it's the same thing!
    Player::take_damage(&mut player, 30);

    // self method - consumes the player
    let name = player.into_name();
    println!("Player's name was: {}", name);

    // Uncommenting this would fail - player was consumed:
    // println!("{}", player.health);
    // ERROR: borrow of moved value: `player`
}

Function signatures tell you everything about how data flows.