Methods and Associated Functions
Defining a struct gives you a data type. To give that type behavior you write an impl block. Inside an impl block you can define two kinds of items: methods (which operate on an instance) and associated functions (which belong to the type itself but do not need an instance).
The impl Block
Everything related to a type's behavior lives inside one or more impl blocks. The block starts with impl TypeName followed by curly braces:
#[derive(Debug)]
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// methods and associated functions go here
}Methods with &self
The most common kind of method borrows the instance immutably using &self. Use this when your method only needs to read data from the struct:
impl Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
let rect = Rectangle { width: 10.0, height: 5.0 };
println!("Area: {}", rect.area());
println!("Perimeter: {}", rect.perimeter());
println!("Square? {}", rect.is_square());Methods with &mut self
When a method needs to change one or more fields of the struct, use &mut self. The instance must also be declared mut at the call site:
impl Rectangle {
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
fn set_width(&mut self, w: f64) {
self.width = w;
}
}
let mut rect = Rectangle { width: 4.0, height: 3.0 };
println!("Before: {:?}", rect);
rect.scale(2.0);
println!("After: {:?}", rect);Methods with self (Taking Ownership)
A method can take ownership of the instance by using self (no &). After the method call the original variable is moved and can no longer be used. This is uncommon but useful for consuming builder types or converting between types:
impl Rectangle {
fn into_square(self) -> Rectangle {
let side = f64::min(self.width, self.height);
Rectangle { width: side, height: side }
}
}
let rect = Rectangle { width: 10.0, height: 4.0 };
let square = rect.into_square();
// println!("{:?}", rect); // compile error — rect was moved
println!("Square: {:?}", square);Associated Functions
Associated functions do not take self as their first parameter — they are called on the type, not on an instance, using the :: syntax. The most common use is as a constructor:
impl Rectangle {
// Associated function — no self parameter
fn new(width: f64, height: f64) -> Rectangle {
Rectangle { width, height }
}
fn square(size: f64) -> Rectangle {
Rectangle { width: size, height: size }
}
}
let rect = Rectangle::new(10.0, 5.0);
let square = Rectangle::square(7.0);
println!("rect: {:?}", rect);
println!("square: {:?}", square);The Self Type Alias
Inside an impl block, Self (capital S) is an alias for the type being implemented. Using Self makes code easier to refactor when you rename a type:
impl Rectangle {
fn new(width: f64, height: f64) -> Self {
Self { width, height }
}
fn clone_rect(&self) -> Self {
Self {
width: self.width,
height: self.height,
}
}
}Automatic Referencing and Dereferencing
Rust automatically adds the right &, &mut, or * when you call a method. The following three calls are equivalent — Rust figures out which one to use based on what the method signature requires:
let rect = Rectangle { width: 10.0, height: 5.0 };
// All three are identical — Rust inserts the & automatically
let a = rect.area();
let b = (&rect).area();
// let c = Rectangle::area(&rect); // explicit form — less commonGetters and Setters
Because struct fields are private by default (within a module), getters and setters are the standard way to expose controlled access to internal state:
pub struct Circle {
radius: f64, // private field
}
impl Circle {
pub fn new(radius: f64) -> Self {
assert!(radius > 0.0, "Radius must be positive");
Self { radius }
}
// getter
pub fn radius(&self) -> f64 {
self.radius
}
// setter with validation
pub fn set_radius(&mut self, r: f64) {
assert!(r > 0.0, "Radius must be positive");
self.radius = r;
}
pub fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
let mut c = Circle::new(5.0);
println!("Radius: {}", c.radius());
println!("Area: {:.2}", c.area());
c.set_radius(10.0);
println!("New area: {:.2}", c.area());The Builder Pattern
When a type has many optional fields the builder pattern keeps construction readable. Each setter method returns &mut Self so calls can be chained:
#[derive(Debug)]
struct QueryBuilder {
table: String,
limit: u32,
offset: u32,
order_by: Option<String>,
}
impl QueryBuilder {
fn new(table: &str) -> Self {
Self {
table: table.to_string(),
limit: 100,
offset: 0,
order_by: None,
}
}
fn limit(&mut self, n: u32) -> &mut Self {
self.limit = n;
self
}
fn offset(&mut self, n: u32) -> &mut Self {
self.offset = n;
self
}
fn order_by(&mut self, col: &str) -> &mut Self {
self.order_by = Some(col.to_string());
self
}
fn build(&self) -> String {
let order = self.order_by
.as_deref()
.unwrap_or("id");
format!(
"SELECT * FROM {} ORDER BY {} LIMIT {} OFFSET {}",
self.table, order, self.limit, self.offset
)
}
}
let query = QueryBuilder::new("users")
.limit(10)
.offset(20)
.order_by("name")
.build();
println!("{}", query);Multiple impl Blocks
A single type can have multiple impl blocks. Rust merges them all. This is helpful for organizing a large type or when generic bounds differ between groups of methods:
#[derive(Debug)]
struct Matrix {
data: Vec<Vec<f64>>,
rows: usize,
cols: usize,
}
// Block 1: constructors
impl Matrix {
fn zeros(rows: usize, cols: usize) -> Self {
Self {
data: vec![vec![0.0; cols]; rows],
rows,
cols,
}
}
}
// Block 2: inspection methods
impl Matrix {
fn rows(&self) -> usize { self.rows }
fn cols(&self) -> usize { self.cols }
fn get(&self, r: usize, c: usize) -> f64 {
self.data[r][c]
}
}
// Block 3: mutation methods
impl Matrix {
fn set(&mut self, r: usize, c: usize, val: f64) {
self.data[r][c] = val;
}
}
let mut m = Matrix::zeros(2, 3);
m.set(0, 1, 42.0);
println!("rows={} cols={}", m.rows(), m.cols());
println!("m[0][1] = {}", m.get(0, 1));Methods That Compare Instances
A method can take another instance of the same type as a parameter, which is useful for comparisons or combinations:
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
fn total_area_with(&self, other: &Rectangle) -> f64 {
self.area() + other.area()
}
}
let big = Rectangle { width: 20.0, height: 10.0 };
let small = Rectangle { width: 5.0, height: 3.0 };
println!("big can hold small: {}", big.can_hold(&small));
println!("small can hold big: {}", small.can_hold(&big));
println!("combined area: {}", big.total_area_with(&small));Quick Reference: self Variants
Parameter | Ownership | Mutates instance? | Typical use |
|---|---|---|---|
&self | Borrows immutably | No | Reading data, calculations |
&mut self | Borrows mutably | Yes | Modifying fields, setters |
self | Takes ownership | Yes (then dropped) | Consuming transformations |
Full Example
#[derive(Debug, Clone)]
struct User {
name: String,
email: String,
age: u32,
active: bool,
}
impl User {
// Associated function — constructor
fn new(name: &str, email: &str, age: u32) -> Self {
Self {
name: name.to_string(),
email: email.to_string(),
age,
active: true,
}
}
// Immutable borrow — reading
fn display_name(&self) -> &str {
&self.name
}
fn is_adult(&self) -> bool {
self.age >= 18
}
// Mutable borrow — modifying
fn birthday(&mut self) {
self.age += 1;
println!("Happy birthday, {}! You are now {}.", self.name, self.age);
}
fn deactivate(&mut self) {
self.active = false;
}
// Takes ownership — consuming conversion
fn into_summary(self) -> String {
format!("{} <{}> age {}", self.name, self.email, self.age)
}
}
fn main() {
let mut user = User::new("Alice", "alice@example.com", 29);
println!("Name: {}", user.display_name());
println!("Adult: {}", user.is_adult());
user.birthday();
let summary = user.into_summary();
// user is moved — cannot use it here
println!("Summary: {}", summary);
}