I am learning Rust
and made some notes about Ownership
and Borrowing
concepts
note that I am still coming to grips with these concepts while "fighting with the Rust borrow checker" at the same time ;)
Ownership in Rust
Ownership = binding/association of value to a variable. The rules are:
- Only one owner at a time
- If the binding is "released" ownership is "gone" and data is "freed"
let p1 = Person::new(); //1
p2 = p1; //2
let p1 = Person::new();
do_something(p1); //out of scope
On line 2, p1
will be freed - it's out of scope since it has transferred ownership to p2
. Passing p1
into another function also has the same effect
let p1 = Person::new();
p2 = p1;
println!("person {#:?}", p2)
println!("person {#:?}", p1) //compile error
That's the reason Rust does not allow you to use that value again. It enforces this at compile time using borrow checker
Drop
It is possible to implement the
drop
method from theDrop
trait if custom cleanup logic is required. It will be automatically called by Rust when that value goes out of scope
Once a variable is out-of-scope i.e. its ownership has been relinquished, the Rust compiler will not allow you to use it again, since it needs to be "freed". This is also called Moving
i.e. ownership of p1
moved to p2
Ownership is applicable to heap data only, not stack data since it (stack data) is "copied", not "moved"
Copy
The
Copy
trait allows a value to be copied. All the primitive types (e.g.i32
,bool
etc.) implementCopy
implicitly. Custom types (e.g.struct
s) can implementCopy
if all its components also implementCopy
- to implement, you can either use#[derive(Copy)]
or use an explicit implementation
Ways of passing ownership
... and keeping the compiler happy
- use the
clone()
method - return from function
- passing a reference
- passing a mutable reference
Use clone()
Think of it as a deep copy
like operation where it creates a separate copy of the data on the heap and points the new owner to it, leaving the old value untouched (or unmoved). Some types implement Clone
trait by default (e.g. String
), but you can mark your custom types as clone-able using #[derive(Clone)]
let p1 = Person::new();
let p2 = p1.clone();
do_something(p2);
println!("{:?}, p1); //works ok
Return from function
let p1 = Person::new();
do_something(p1);
...
fn do_something(p: Person) -> Person {
//logic
}
...
println!("p = {}",p1); //continue using p1 since ownership has been passed back
But this is just wired! You're forced to return something just because you want to keep the variable in scope
Passing a reference
let p1 = Person::new();
do_something(&p1);
println!("p = {}",p1); //continue using p1 since ownership has been was never passed on
...
fn do_something(p: &Person) {
//logic
}
...
The value of p1
did not move - this is called Borrowing
. do_something
never got the ownership, since all we did was pass a reference to p1
- notice the &
Mutable References
If you want do_something
logic to update the name of the Person
reference, you'll need to pass a mutable reference
let mut p1 = Person::new();
do_something(&mut p1);
println!("mutated info {}",p2)
...
fn do_something(p: &mut Person, new_name: String) {
p.name = String::from(new_name);
}
...
Changes:
- Variable
p1
was declared asmut
(mutable) - Passed
&mut
todo_something
instead of just&
- signifies mutable reference -
do_something
signature updated to&mut
- signifies mutable reference
To dive in further, you can read up the following chapters from "The Rust Programming Language" book
- Chapter 4 (ownership and borrowing)
- Chapter 10 (lifetimes)
- Chapter 15 (smart pointers)