Methods & Associated Functions
Methods and associated functions allow you to define behavior for your types. While methods operate on instances of a type, associated functions are called on the type itself. Together, they provide a clean way to organize functionality and create intuitive APIs.
Implementation Blocks
Use impl
blocks to define methods and associated functions for types:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Associated function (constructor)
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
// Associated function (alternative constructor)
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
// Method (takes &self)
fn area(&self) -> u32 {
self.width * self.height
}
// Method (takes &mut self)
fn double_size(&mut self) {
self.width *= 2;
self.height *= 2;
}
// Method (takes self - consumes the instance)
fn into_area(self) -> u32 {
self.width * self.height
}
}
fn main() {
// Using associated functions
let rect = Rectangle::new(30, 50);
let square = Rectangle::square(20);
// Using methods
println!("Area: {}", rect.area());
let mut mutable_rect = Rectangle::new(10, 15);
mutable_rect.double_size();
println!("Doubled size area: {}", mutable_rect.area());
// Consuming method
let final_area = square.into_area();
println!("Final area: {}", final_area);
// square is no longer available here
}
Method Types and Self Parameters
Rust has three types of self
parameters that determine how methods interact with instances:
Immutable Reference (&self)
Most common for methods that read data without modifying it:
struct Person {
name: String,
age: u32,
}
impl Person {
fn name(&self) -> &str {
&self.name
}
fn age(&self) -> u32 {
self.age
}
fn is_adult(&self) -> bool {
self.age >= 18
}
fn greet(&self) {
println!("Hello, my name is {}", self.name);
}
fn full_info(&self) -> String {
format!("{} is {} years old", self.name, self.age)
}
}
fn main() {
let person = Person {
name: "Alice".to_string(),
age: 25,
};
// All these methods can be called multiple times
println!("Name: {}", person.name());
println!("Age: {}", person.age());
println!("Is adult: {}", person.is_adult());
person.greet();
println!("Info: {}", person.full_info());
}
Mutable Reference (&mut self)
For methods that need to modify the instance:
struct Counter {
value: u32,
}
impl Counter {
fn new() -> Counter {
Counter { value: 0 }
}
fn increment(&mut self) {
self.value += 1;
}
fn increment_by(&mut self, amount: u32) {
self.value += amount;
}
fn reset(&mut self) {
self.value = 0;
}
fn set(&mut self, value: u32) {
self.value = value;
}
fn get(&self) -> u32 {
self.value
}
}
fn main() {
let mut counter = Counter::new();
counter.increment();
counter.increment_by(5);
println!("Counter value: {}", counter.get());
counter.reset();
println!("After reset: {}", counter.get());
counter.set(42);
println!("After set: {}", counter.get());
}
Owned Self (self)
For methods that consume the instance:
struct Message {
content: String,
sender: String,
}
impl Message {
fn new(content: String, sender: String) -> Message {
Message { content, sender }
}
// Consuming method - takes ownership
fn send(self) -> Result<(), String> {
println!("Sending message '{}' from {}", self.content, self.sender);
// Simulate sending...
Ok(())
}
// Transform into another type
fn into_archived(self) -> ArchivedMessage {
ArchivedMessage {
content: self.content,
sender: self.sender,
archived_at: std::time::SystemTime::now(),
}
}
// Extract a field, consuming the struct
fn into_content(self) -> String {
self.content
}
}
struct ArchivedMessage {
content: String,
sender: String,
archived_at: std::time::SystemTime,
}
fn main() {
let message = Message::new(
"Hello, world!".to_string(),
"Alice".to_string(),
);
// This consumes the message
let result = message.send();
// message is no longer available here
let another_message = Message::new(
"Goodbye!".to_string(),
"Bob".to_string(),
);
// Transform into archived message
let archived = another_message.into_archived();
}
Associated Functions (Static Methods)
Associated functions don't take self
and are called on the type itself:
struct User {
username: String,
email: String,
active: bool,
}
impl User {
// Constructor
fn new(username: String, email: String) -> User {
User {
username,
email,
active: true,
}
}
// Alternative constructors
fn inactive(username: String, email: String) -> User {
User {
username,
email,
active: false,
}
}
fn from_email(email: String) -> User {
let username = email.split('@').next().unwrap_or("user").to_string();
User::new(username, email)
}
// Factory methods
fn admin() -> User {
User {
username: "admin".to_string(),
email: "[email protected]".to_string(),
active: true,
}
}
fn guest() -> User {
User {
username: "guest".to_string(),
email: "[email protected]".to_string(),
active: false,
}
}
// Utility functions
fn validate_email(email: &str) -> bool {
email.contains('@') && email.contains('.')
}
fn max_username_length() -> usize {
50
}
}
fn main() {
// Using different constructors
let user1 = User::new("alice".to_string(), "[email protected]".to_string());
let user2 = User::from_email("[email protected]".to_string());
let admin = User::admin();
let guest = User::guest();
// Using utility functions
if User::validate_email("[email protected]") {
println!("Valid email");
}
println!("Max username length: {}", User::max_username_length());
}
Method Chaining
Design methods to enable fluent interfaces:
struct QueryBuilder {
table: String,
conditions: Vec<String>,
order_by: Option<String>,
limit: Option<u32>,
}
impl QueryBuilder {
fn new(table: &str) -> Self {
QueryBuilder {
table: table.to_string(),
conditions: Vec::new(),
order_by: None,
limit: None,
}
}
fn where_clause(mut self, condition: &str) -> Self {
self.conditions.push(condition.to_string());
self
}
fn order_by(mut self, column: &str) -> Self {
self.order_by = Some(column.to_string());
self
}
fn limit(mut self, count: u32) -> Self {
self.limit = Some(count);
self
}
fn build(self) -> String {
let mut query = format!("SELECT * FROM {}", self.table);
if !self.conditions.is_empty() {
query.push_str(" WHERE ");
query.push_str(&self.conditions.join(" AND "));
}
if let Some(order) = self.order_by {
query.push_str(&format!(" ORDER BY {}", order));
}
if let Some(limit) = self.limit {
query.push_str(&format!(" LIMIT {}", limit));
}
query
}
}
fn main() {
let query = QueryBuilder::new("users")
.where_clause("age > 18")
.where_clause("active = true")
.order_by("created_at")
.limit(10)
.build();
println!("Query: {}", query);
}
Multiple Implementation Blocks
You can have multiple impl
blocks for the same type:
struct Calculator {
result: f64,
}
// Basic operations
impl Calculator {
fn new() -> Calculator {
Calculator { result: 0.0 }
}
fn add(&mut self, value: f64) -> &mut Self {
self.result += value;
self
}
fn subtract(&mut self, value: f64) -> &mut Self {
self.result -= value;
self
}
fn get_result(&self) -> f64 {
self.result
}
}
// Advanced operations
impl Calculator {
fn multiply(&mut self, value: f64) -> &mut Self {
self.result *= value;
self
}
fn divide(&mut self, value: f64) -> &mut Self {
if value != 0.0 {
self.result /= value;
}
self
}
fn power(&mut self, exponent: f64) -> &mut Self {
self.result = self.result.powf(exponent);
self
}
fn clear(&mut self) -> &mut Self {
self.result = 0.0;
self
}
}
fn main() {
let mut calc = Calculator::new();
let result = calc
.add(10.0)
.multiply(2.0)
.subtract(5.0)
.divide(3.0)
.get_result();
println!("Result: {}", result);
}
Generic Implementation Blocks
Implement methods for generic types:
struct Container<T> {
items: Vec<T>,
}
impl<T> Container<T> {
fn new() -> Self {
Container { items: Vec::new() }
}
fn add(&mut self, item: T) {
self.items.push(item);
}
fn len(&self) -> usize {
self.items.len()
}
fn is_empty(&self) -> bool {
self.items.is_empty()
}
fn get(&self, index: usize) -> Option<&T> {
self.items.get(index)
}
}
// Implementation for specific types
impl Container<String> {
fn add_strings(&mut self, strings: Vec<String>) {
self.items.extend(strings);
}
fn concatenate(&self) -> String {
self.items.join(" ")
}
}
impl<T: Clone> Container<T> {
fn duplicate_last(&mut self) {
if let Some(last) = self.items.last() {
self.items.push(last.clone());
}
}
}
impl<T: std::fmt::Display> Container<T> {
fn print_all(&self) {
for item in &self.items {
println!("{}", item);
}
}
}
fn main() {
let mut string_container = Container::new();
string_container.add("Hello".to_string());
string_container.add("World".to_string());
string_container.add_strings(vec!["Rust".to_string(), "Programming".to_string()]);
println!("Concatenated: {}", string_container.concatenate());
string_container.print_all();
let mut number_container = Container::new();
number_container.add(1);
number_container.add(2);
number_container.duplicate_last();
number_container.print_all();
}
Implementation for Enums
Enums can also have methods and associated functions:
#[derive(Debug)]
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
impl Shape {
// Associated functions for construction
fn circle(radius: f64) -> Shape {
Shape::Circle { radius }
}
fn rectangle(width: f64, height: f64) -> Shape {
Shape::Rectangle { width, height }
}
fn square(side: f64) -> Shape {
Shape::Rectangle { width: side, height: side }
}
fn triangle(base: f64, height: f64) -> Shape {
Shape::Triangle { base, height }
}
// Methods
fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
fn perimeter(&self) -> f64 {
match self {
Shape::Circle { radius } => 2.0 * std::f64::consts::PI * radius,
Shape::Rectangle { width, height } => 2.0 * (width + height),
Shape::Triangle { base, height } => {
// Simplified calculation for isosceles triangle
let side = (height.powi(2) + (base / 2.0).powi(2)).sqrt();
base + 2.0 * side
}
}
}
fn scale(&mut self, factor: f64) {
match self {
Shape::Circle { radius } => *radius *= factor,
Shape::Rectangle { width, height } => {
*width *= factor;
*height *= factor;
}
Shape::Triangle { base, height } => {
*base *= factor;
*height *= factor;
}
}
}
fn shape_type(&self) -> &'static str {
match self {
Shape::Circle { .. } => "Circle",
Shape::Rectangle { .. } => "Rectangle",
Shape::Triangle { .. } => "Triangle",
}
}
}
fn main() {
let mut shapes = vec![
Shape::circle(5.0),
Shape::rectangle(4.0, 6.0),
Shape::square(3.0),
Shape::triangle(4.0, 3.0),
];
for shape in &mut shapes {
println!("{}: Area = {:.2}, Perimeter = {:.2}",
shape.shape_type(), shape.area(), shape.perimeter());
shape.scale(1.5);
println!("After scaling: Area = {:.2}", shape.area());
}
}
Advanced Patterns
Builder Pattern
#[derive(Debug)]
struct HttpRequest {
method: String,
url: String,
headers: Vec<(String, String)>,
body: Option<String>,
}
struct HttpRequestBuilder {
method: Option<String>,
url: Option<String>,
headers: Vec<(String, String)>,
body: Option<String>,
}
impl HttpRequestBuilder {
fn new() -> Self {
HttpRequestBuilder {
method: None,
url: None,
headers: Vec::new(),
body: None,
}
}
fn method(mut self, method: &str) -> Self {
self.method = Some(method.to_string());
self
}
fn url(mut self, url: &str) -> Self {
self.url = Some(url.to_string());
self
}
fn header(mut self, key: &str, value: &str) -> Self {
self.headers.push((key.to_string(), value.to_string()));
self
}
fn body(mut self, body: &str) -> Self {
self.body = Some(body.to_string());
self
}
fn build(self) -> Result<HttpRequest, String> {
let method = self.method.ok_or("Method is required")?;
let url = self.url.ok_or("URL is required")?;
Ok(HttpRequest {
method,
url,
headers: self.headers,
body: self.body,
})
}
}
impl HttpRequest {
fn builder() -> HttpRequestBuilder {
HttpRequestBuilder::new()
}
}
fn main() {
let request = HttpRequest::builder()
.method("POST")
.url("https://api.example.com/users")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body(r#"{"name": "John", "email": "[email protected]"}"#)
.build()
.unwrap();
println!("Request: {:#?}", request);
}
Delegation Pattern
struct Database {
connection_string: String,
}
impl Database {
fn new(connection_string: String) -> Self {
Database { connection_string }
}
fn query(&self, sql: &str) -> Vec<String> {
println!("Executing query: {}", sql);
vec!["result1".to_string(), "result2".to_string()]
}
fn execute(&self, sql: &str) -> bool {
println!("Executing command: {}", sql);
true
}
}
struct UserRepository {
db: Database,
}
impl UserRepository {
fn new(db: Database) -> Self {
UserRepository { db }
}
fn find_by_id(&self, id: u32) -> Option<String> {
let sql = format!("SELECT * FROM users WHERE id = {}", id);
let results = self.db.query(&sql);
results.first().cloned()
}
fn create_user(&self, name: &str, email: &str) -> bool {
let sql = format!("INSERT INTO users (name, email) VALUES ('{}', '{}')", name, email);
self.db.execute(&sql)
}
fn delete_user(&self, id: u32) -> bool {
let sql = format!("DELETE FROM users WHERE id = {}", id);
self.db.execute(&sql)
}
}
fn main() {
let db = Database::new("postgresql://localhost/mydb".to_string());
let user_repo = UserRepository::new(db);
user_repo.create_user("Alice", "[email protected]");
if let Some(user) = user_repo.find_by_id(1) {
println!("Found user: {}", user);
}
}
Best Practices
1. Use Descriptive Method Names
// Good: Clear intent
impl User {
fn is_active(&self) -> bool { self.active }
fn deactivate(&mut self) { self.active = false; }
fn get_email(&self) -> &str { &self.email }
fn update_email(&mut self, email: String) { self.email = email; }
}
// Avoid: Unclear purpose
impl User {
fn check(&self) -> bool { self.active }
fn toggle(&mut self) { self.active = false; }
fn email(&self) -> &str { &self.email }
fn set(&mut self, email: String) { self.email = email; }
}
2. Group Related Functionality
impl Rectangle {
// Constructors
fn new(width: u32, height: u32) -> Self { /* ... */ }
fn square(size: u32) -> Self { /* ... */ }
// Measurements
fn area(&self) -> u32 { /* ... */ }
fn perimeter(&self) -> u32 { /* ... */ }
// Transformations
fn scale(&mut self, factor: f64) { /* ... */ }
fn rotate(&mut self) { /* ... */ }
// Predicates
fn is_square(&self) -> bool { /* ... */ }
fn can_contain(&self, other: &Rectangle) -> bool { /* ... */ }
}
3. Choose Appropriate Self Types
impl Calculator {
// Use &self for queries that don't modify state
fn get_result(&self) -> f64 { self.result }
// Use &mut self for operations that modify state
fn add(&mut self, value: f64) { self.result += value; }
// Use self for transformations that consume the instance
fn into_result(self) -> f64 { self.result }
}
4. Provide Convenient Constructors
impl User {
// Basic constructor
fn new(name: String, email: String) -> Self { /* ... */ }
// Convenience constructors
fn from_email(email: String) -> Self { /* ... */ }
fn guest() -> Self { /* ... */ }
fn admin() -> Self { /* ... */ }
// Builder pattern for complex construction
fn builder() -> UserBuilder { /* ... */ }
}
Methods and associated functions are essential for creating clean, intuitive APIs in Rust. They allow you to encapsulate behavior with data and provide multiple ways to construct and manipulate your types. Use them to create expressive and ergonomic interfaces that make your code easy to use and understand.