Learning Rust's Ownership and Borrowing: Learn from the Book "Rust in Action"
In Rust, every variable has ownership, which defines when the variable can be used and modified. When ownership of a variable is transferred, its lifetime is also transferred. This allows Rust to check ownership and lifetimes of code at compile-time to prevent unexpected errors at runtime. Recently, I read a book called "Rust in Action," which had a chapter on ownership that gave me a deeper understanding of this topic.
Using references when ownership is not necessary
Using borrowing can avoid unnecessary ownership transfers, while also improving code readability and safety. For example, if a variable only needs to be read in a function, it can be passed as a parameter to the function without transferring ownership to the function. This can prevent the risk of forgetting to release memory after the function call ends. In addition, borrowing can allow multiple variables to share the same memory, reducing memory usage.
Use fewer long-lived values
In Rust, when a variable has ownership, it is responsible for releasing resources. Therefore, when a variable no longer needs ownership, returning ownership to release resources can effectively avoid wasting resources. In fact, even when ownership of a variable is not needed, the variable can still be used. This is useful for situations where resource ownership needs to be passed.
Let's consider an example of a classroom and students. In this example, the classroom is long-lived data, while the students are short-lived data. When a student needs to leave the classroom, there is no need to destroy the classroom, only to return ownership of the student.
#[derive(Debug)]
struct Student {
name: String,
}
impl Student {
fn new(name: String) -> Student {
Student { name }
}
fn leave(&self) {
println!("{} has left the classroom", self.name);
}
}
#[derive(Debug)]
struct Classroom {
students: Vec<Student>,
}
impl Classroom {
fn new() -> Classroom {
Classroom { students: vec![] }
}
fn add_student(&mut self, student: Student) {
self.students.push(student);
}
fn remove_student(&mut self, student: &Student) {
self.students.retain(|s| s.name != student.name);
student.leave();
}
}
fn main() {
let mut classroom = Classroom::new();
let alice = Student::new("Alice".into());
let bob = Student::new("Bob".into());
classroom.add_student(alice);
classroom.add_student(bob);
let alice = Student::new("Alice".into());
classroom.remove_student(&alice);
}
Wrap data within specialty types:
We can also use Rc<RefCell<T>>
to share a struct and modify its value when needed.
In the example, Thermometer
is a struct with a temperature value.
We create a variable
thermometer
of typeRc<RefCell<Thermometer>>
.We use the
borrow_mut()
method to modify the temperature value. Note that we useborrow_mut()
instead ofborrow()
because we need to make changes to the temperature value.In this example, we modify the temperature value to
25.0
.We use the
clone()
method to clonethermometer
, so we can access the temperature value without owningthermometer
.We use the
borrow()
method to obtain an immutable reference tothermometer
, and call theget_temperature()
method to obtain the temperature value. Note that we useborrow()
instead ofborrow_mut()
because we do not need to modify the temperature value.```rust
use std::cell::RefCell; use std::rc::Rc;
#[derive(Debug)] struct Thermometer { temperature: f64, }
impl Thermometer { fn new(temperature: f64) -> Self { Thermometer { temperature } }
fn get_temperature(&self) -> f64 { self.temperature }
fn set_temperature(&mut self, temperature: f64) { self.temperature = temperature; } }
fn main() { // 1 let thermometer = Rc::new(RefCell::new(Thermometer::new(20.0))); // 4 // clone Rc for future use let thermometer_clone = Rc::clone(&thermometer);
// modify temperature through Rc> { // 2 let mut borrowed_thermometer = thermometer.borrow_mut(); // 3 borrowed_thermometer.set_temperature(25.0); }
// 4 // access temperature through Rc> let temperature = thermometer_clone.borrow().get_temperature();
//The current temperature is 25. println!("The current temperature is {}.", temperature); }
```
Ownership is an important feature of the Rust language that allows Rust to ensure memory safety and avoid common concurrency issues at compile time. In Rust, variables must be passed or moved for ownership, rather than simply copied or referenced. Through the concept of the ownership model, Rust provides a powerful way for programmers to manage resources in their programs. If you want to dive deeper into Rust's ownership features, it is recommended to read the book Rust in Action. This book not only provides a detailed introduction to the concept of ownership but also offers rich examples and code samples to help readers better understand how ownership is used in Rust.