Methods Receiver in Rust
Amit Nadiger
Polyglot(Rust??, C++ 11,14,17,20, C, Kotlin, Java) Android TV, Cas, Blockchain, Polkadot, UTXO, Substrate, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Engineering management.
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
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:
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
*/
2. add_lap method:
3. print_laps method:
4. finish method:
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!!