Methods Receiver in Rust

In Rust, the term "method receiver" refers to the first parameter of a method or function, which specifies how the method or function is invoked. The method receiver determines how the method or function can access and modify the data associated with the type it operates on.

Method receivers in Rust specify how methods or functions are invoked and define the ownership, borrowing, and mutability rules for the associated data. They are applicable to methods implemented on structs, enums, and trait methods, as well as regular functions.

Methods in Rust provide a way to define behavior associated with a particular struct or enum. They allow you to encapsulate functionality and operate on the data within a specific type. One key aspect of defining methods in Rust is the concept of method receivers, which determine how the method interacts with the object it is called on. Method receivers play a crucial role in managing ownership, borrowing, and mutability of objects in Rust. In this article, we will explore the different method receivers and their implications.

Method receivers are applicable not only to methods implemented on structs and enums but also to normal functions. However, it's important to note that the term "method" is commonly used in the context of functions associated with structs and enums, while regular functions are simply referred to as functions.

Introduction

In Rust, methods are defined within an impl block associated with a struct or enum. The method receiver is specified in the method signature and determines how the method interacts with the object it is called on. Rust provides four different method receivers, each with its own rules and implications for ownership, borrowing, and mutability.

Method receivers allow you to control how methods interact with objects and enforce certain guarantees about the object's state during method execution. By understanding and choosing the appropriate method receiver, we can design safer and more efficient code.

Four Method Receivers in Rust

  • &self - Immutable Borrow
  • &mut self - Mutable Borrow
  • self - Ownership Transfer
  • mut self - Mutable Ownership Transfer

Lets discuss each of them below :

1. &self - Immutable Borrow

The &self method receiver borrows the object from the caller using a shared and immutable reference. This means the method can access the object's data but cannot modify it. Multiple readers can access the object simultaneously.

struct Example {
? ? data: i32,
}

impl Example {
? ? fn get_data(&self) -> i32 {
? ? ? ? self.data
? ? }
}        

2. &mut self - Mutable Borrow

The &mut self method receiver borrows the object from the caller using a unique and mutable reference. This allows the method to modify the object's data. However, it prevents other parts of the program from accessing the object concurrently.

struct Example {
? ? data: i32,
}

impl Example {
? ? fn increment_data(&mut self) {
? ? ? ? self.data += 1;
? ? }
}        

3. self - Ownership Transfer

The self method receiver takes ownership of the object, moving it away from the caller. The method becomes the owner of the object, and the caller relinquishes ownership. The method has full control over the object and can modify or consume it as needed. The object will be dropped (deallocated) when the method returns unless its ownership is explicitly transmitted.

struct Example {
? ? data: i32,
}

impl Example {
? ? fn consume(self) -> i32 {
? ? ? ? self.data
? ? }
}        

4. mut self - Mutable Ownership Transfer

The mut self method receiver is similar to self, but it allows the method to mutate the object while it owns it. The method can modify the object's state, but it doesn't automatically mean that the object is mutable outside of the method. Other parts of the program cannot access the object while the method is executing.

Below example demonstrates how the mut self method receiver allows mutable ownership transfer, providing the ability to modify the struct's internal state and consume the instance in the process.

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }

    fn increment(&mut self) {
        self.count += 1;
    }

    fn reset(mut self) {
        self.count = 0;
    }

    fn get_count(&self) -> u32 {
        self.count
    }
}

fn main() {
    let mut counter = Counter::new();
    counter.increment();
    println!("Count: {}", counter.get_count());

    counter.reset();
    println!("Count after reset: {}", counter.get_count());  // Error: value borrowed here after move

    // Uncomment the line below to see the error
    // counter.increment();  // Error: value used here after move
} 
/*
Compilation error :

error[E0382]: borrow of moved value: `counter`
? --> main.rs:29:39
? ?|
24 |? ? ?let mut counter = Counter::new();
? ?|? ? ? ? ?----------- move occurs because `counter` has type `Counter`, which does not implement the `Copy` trait
...
28 |? ? ?counter.reset();
? ?|? ? ?------- value moved here
29 |? ? ?println!("Count after reset: {}", counter.get_count());? // Error: value borrowed here after move
? ?|? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?^^^^^^^ value borrowed here after move


error: aborting due to previous error


For more information about this error, try `rustc --explain E0382`. 
*/        

In the above code, we have a Counter struct with a count field. The increment method takes &mut self as a receiver, allowing mutable access to the Counter instance and incrementing the count field. The reset method takes mut self as a receiver, indicating that it takes ownership of the Counter instance.

In the main function, we create a mutable counter instance and call the increment method, which increases the count by 1. We then print the count using the get_count method.

Next, we call the reset method on the counter instance. This method takes ownership of self and sets the count to 0. After the reset call, we attempt to print the count again, but this results in a compilation error. The error occurs because the reset method takes ownership of counter, leaving it in an invalid state, and we cannot use it anymore.

If you uncomment the line counter.increment(); after the reset call, you will see another compilation error. This error occurs because the counter variable has been moved and is no longer accessible.

Choosing the Appropriate Method Receiver

When defining methods for your types in Rust, it's important to choose the appropriate method receiver based on the desired behavior and requirements of your code. Consider the following guidelines:

  • Use &self when the method only needs read-only access to the object and doesn't modify its state.
  • Use &mut self when the method needs to modify the object's state, but other parts of the program should not have access to the object concurrently.
  • Use self when the method needs full ownership and control over the object, consuming it in the process.
  • Use mut self when the method needs both ownership and mutability, allowing the method to modify the object while it owns it.

By selecting the appropriate method receiver, you can enforce the desired behavior and ensure the correctness and safety of your code.

Method Receivers and Structs

Method receivers are commonly used with structs in Rust to define behavior specific to that struct. By defining methods on a struct, you encapsulate functionality related to the struct's data and provide a more intuitive interface for working with instances of the struct.

For example, consider a Person struct with a method to calculate the person's age based on their birth year:

struct Person {
? ? name: String,
? ? birth_year: u32,
}

impl Person {
? ? fn calculate_age(&self, current_year: u32) -> u32 {
? ? ? ? current_year - self.birth_year
? ? }
}        

The calculate_age method takes an immutable reference to self (&self) since it only needs to read the person's birth year and the current year.

Method Receivers and Enums

Enums in Rust can also have methods associated with them. The method receivers work in a similar way for enums, allowing you to define behavior specific to each variant of the enum.

For example, consider an enum Shape with two variants: Circle and Rectangle. Each variant can have its own methods:

enum Shape {
? ? Circle { radius: f64 },
? ? Rectangle { width: f64, height: f64 },
}


impl Shape {
? ? fn area(&self) -> f64 {
? ? ? ? match self {
? ? ? ? ? ? Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
? ? ? ? ? ? Shape::Rectangle { width, height } => width * height,
? ? ? ? }
? ? }
}        

The area method calculates the area of a shape. Depending on the variant, the calculation differs.


Below example showcase all 4 methods of methods receiver.

Four different methods in the Race struct, each using a different method receiver. Let's go through each method and discuss their behavior and usage in the main function:

Copied from: Example - Comprehensive Rust ?? (google.github.io)

#[derive(Debug)]
struct Race {
? ? name: String,
? ? laps: Vec<i32>,
}

impl Race {
? ? fn new(name: &str) -> Race {? // No receiver, a static method
? ? ? ? Race { name: String::from(name), laps: Vec::new() }
? ? }

? ? fn add_lap(&mut self, lap: i32) {? // Exclusive borrowed read-write access to self
? ? ? ? self.laps.push(lap);
? ? }

? ? fn print_laps(&self) {? // Shared and read-only borrowed access to self
? ? ? ? println!("Recorded {} laps for {}:", self.laps.len(), self.name);
? ? ? ? for (idx, lap) in self.laps.iter().enumerate() {
? ? ? ? ? ? println!("Lap {idx}: {lap} sec");
? ? ? ? }
? ? }

? ? fn finish(self) {? // Exclusive ownership of self
? ? ? ? let total = self.laps.iter().sum::<i32>();
? ? ? ? println!("Race {} is finished, total lap time: {}", self.name, total);
? ? }
}

fn main() {
? ? let mut race = Race::new("Monaco Grand Prix");
? ? race.add_lap(70);
? ? race.add_lap(68);
? ? race.print_laps();
? ? race.add_lap(71);
? ? race.print_laps();
? ? race.finish();
? ? // race.add_lap(42);
} 
/*
Recorded 2 laps for Monaco Grand Prix:
Lap 0: 70 sec
Lap 1: 68 sec
Recorded 3 laps for Monaco Grand Prix:
Lap 0: 70 sec
Lap 1: 68 sec
Lap 2: 71 sec
Race Monaco Grand Prix is finished, total lap time: 209 
*/        

  1. new method:

  • Method receiver: No receiver (self: &str)
  • This is a static method, meaning it doesn't require an instance of the struct to be called.
  • It creates a new Race instance with the given name and an empty vector of laps.
  • The created instance is returned and can be used in the main function as a regular variable.
  • Example usage: let mut race = Race::new("Monaco Grand Prix");

2. add_lap method:

  • Method receiver: &mut self
  • This method requires mutable access to the Race instance.
  • It adds a new lap to the laps vector of the Race instance.
  • Since it takes a mutable reference to self, it can modify the laps vector.
  • The modified Race instance can be used again in the main function after calling this method.
  • Example usage: race.add_lap(70);

3. print_laps method:

  • Method receiver: &self
  • This method only requires shared and immutable access to the Race instance.
  • It prints the recorded laps of the race.
  • Since it takes an immutable reference to self, it cannot modify the Race instance or its fields.
  • The Race instance can be used again in the main function after calling this method.
  • Example usage: race.print_laps();

4. finish method:

  • Method receiver: self
  • This method takes ownership of the Race instance, consuming it.
  • It calculates the total lap time and prints a message indicating that the race is finished.
  • Since it takes ownership of self, the Race instance cannot be used again in the main function after calling this method.
  • Example usage: race.finish();

Regarding the error when trying to call add_lap after finish, uncommenting the line race.add_lap(42); will result in a compilation error. This is because finish takes ownership of the Race instance, and once ownership is transferred, we can no longer use the instance or call methods on it.

Rust provides automatic referencing and dereferencing when calling methods. It automatically adds the &, *, and mut qualifiers to match the method signature. This makes method calls more convenient and avoids manual borrowing or dereferencing.

In the provided code, print_laps uses a vector that is iterated over to display the recorded laps. However, as mentioned in the code comments, vectors are not covered in detail in this code snippet, so their explanation is deferred to a later section.

=======================================================

In addition to the variants of self as method receivers in Rust, there are also special wrapper types allowed to be receiver types. One such example is Box<Self>, which enables methods to be called on boxed instances of a type.

Box<Self>

When a method is defined with Box<Self> as the receiver, it means the method can only be called on a boxed instance of the type. This provides flexibility in managing ownership and allows for dynamic dispatch.

Let's see an example to understand how Box<Self> can be used as a method receiver:

struct Person {
? ? name: String,
}

impl Person {
? ? fn new(name: String) -> Box<Self> {
? ? ? ? Box::new(Person { name })
? ? }

? ? fn say_hello(self: Box<Self>) {
? ? ? ? println!("Hello, my name is {}", self.name);
? ? }
}

fn main() {
? ? let person = Person::new("John".to_string());
? ? person.say_hello();
}        

In the example above, the new method creates a boxed instance of the Person struct using Box::new. The say_hello method is defined with self: Box<Self> as the receiver, indicating that it can only be called on a boxed instance of Person.

By using Box<Self> as the receiver, the method consumes ownership of the boxed instance. This allows the method to have full control over the object and its lifetime. When the method returns, the boxed instance will be deallocated.

This pattern can be particularly useful in scenarios where dynamic dispatch is required, such as when working with trait objects or implementing polymorphic behavior. It enables you to call methods on boxed instances without knowing the exact concrete type at compile-time.

It's worth noting that Box<Self> is just one example of a wrapper type that can be used as a method receiver. Other wrapper types, such as Rc<Self> or Arc<Self>, can also be used depending on the specific requirements of your code.

Using wrapper types as method receivers provides flexibility in managing ownership and enables dynamic dispatch, allowing for more advanced and flexible usage of methods in

Understanding and utilizing the different method receivers in Rust is essential for designing robust and efficient code. By choosing the appropriate method receiver, you can control ownership, borrowing, and mutability of objects, ensuring correctness and safety in your programs. Whether it's borrowing a reference, transferring ownership, or enabling mutability, method receivers offer flexibility in defining behavior associated with types in Rust.


Thanks for reading till end, lets learn together!!

要查看或添加评论,请登录

Amit Nadiger的更多文章

  • Rust modules

    Rust modules

    Referance : Modules - Rust By Example Rust uses a module system to organize and manage code across multiple files and…

  • List of C++ 17 additions

    List of C++ 17 additions

    1. std::variant and std::optional std::variant: A type-safe union that can hold one of several types, useful for…

  • List of C++ 14 additions

    List of C++ 14 additions

    1. Generic lambdas Lambdas can use auto parameters to accept any type.

    6 条评论
  • Passing imp DS(vec,map,set) to function

    Passing imp DS(vec,map,set) to function

    In Rust, we can pass imp data structures such as , , and to functions in different ways, depending on whether you want…

  • Atomics in C++

    Atomics in C++

    The C++11 standard introduced the library, providing a way to perform operations on shared data without explicit…

    1 条评论
  • List of C++ 11 additions

    List of C++ 11 additions

    1. Smart Pointers Types: std::unique_ptr, std::shared_ptr, and std::weak_ptr.

    2 条评论
  • std::lock, std::trylock in C++

    std::lock, std::trylock in C++

    std::lock - cppreference.com Concurrency and synchronization are essential aspects of modern software development.

    3 条评论
  • std::unique_lock,lock_guard, & scoped_lock

    std::unique_lock,lock_guard, & scoped_lock

    C++11 introduced several locking mechanisms to simplify thread synchronization and prevent race conditions. Among them,…

  • Understanding of virtual & final in C++ 11

    Understanding of virtual & final in C++ 11

    C++ provides powerful object-oriented programming features such as polymorphism through virtual functions and control…

  • Importance of Linux kernal in AOSP

    Importance of Linux kernal in AOSP

    The Linux kernel serves as the foundational layer of the Android Open Source Project (AOSP), acting as the bridge…

    1 条评论

社区洞察

其他会员也浏览了