Ownership + Borrowing
As mentioned, Rust has strict rules for mutability, which also relates to the ownership model.
Rust has rules of ownership:
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
When we pass a value to a function, the default behavior is to transfer ownership to the function:
fn main() {
let s = String::from("hello"); // right now, we own s
process_string(s); // assumes ownership of s
// we no longer own s, ... and so "s" is no longer valid here
}
fn process_string(some_string: String) { // some_string comes into scope
println!("{some_string}");
} // Here, some_string goes out of scope (no return) and `drop` is called. The backing memory is freed.
In this example, if we tried to use s
after calling process_string
, rust would throw a compile-time error.
This is because process_string
assumes ownership of s
, then lets it fall off a cliff, not returning the value.
We can pass ownership back to the caller:
fn main() {
let s = String::from("hello"); // right now, we own s
let s2 = process_string(s); // assumes + returns ownership of s
println!("the result is: {s2}");
// we no longer own s, ... and so "s" is no longer valid here
}
fn process_string(some_string: String) -> String { // some_string comes into scope
println!("{some_string}");
some_string // return ownership back to caller
}
In this example, the value is passed between process_string
and main, it is not copied. In this snippet, s
, s2
and some_string
all refer to the same heap-allocated value.
So, why return the value and share it between functions? Take this example:
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{s2}' is {len}.");
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String
(s, length)
}
In this example, we NEED to return ownershup back to the caller. In order to print s2
in the last line of main
, we need to return s
back to the caller from calculate_length
, otherwise it would be garbage collected at the end of calculate_length
. And, since it’s the same underlying heap-allocated value, we need to keep it alive and transfer ownership back to the calelr
⚠️⚠️ None of this is “borrowing”, this is merely transfer of ownership between functions.
References and borrowing
Back to the calculate_length example, wouldn’t it be nice if we didn’t HAVE to worry about transferring ownership back and forth between the two functions? References help here.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
Notice how we’re using the type definition of &String
instead of String
. This means that s
is a reference to the variable being passed in, not the variable itself. If that makes sense. Note how we no longer have to return the variable, ownershup never leaves main
. In short, references allow us to read a variable without taking ownership of it.
In Rust, the action of creating a reference is called “borrowing”. As in real life, if a person owns something, you can borrow it from them. When you’re done, you have to give it back. You don’t own it.
So, what happens if we try to modify something we don’t own?
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
The result: we can’t. This will fail to compile with an error like: cannot borrow
*some_stringas mutable, as it is behind a
& reference
.
References, just like variables, are immutable by default. But, would you guess…this does not necessarily have to be the case.
Mutable References, for the people
Rust CAN allow us to modify references by using mutable references with the &mut: T
syntax. This looks like this:
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
There is a major caveat with mutable references, though. If you have a mutable refence to a value in flight, you can have no other references to that value. For example, this is invalid Rust code:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
Basically: this is to prevent race conditions/two or more pointers updating the same value resulting in conflict.
However, this restriction to only allow one mutable reference at a time is constrained to scoe. So we can do something like this:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
Note: The opposite of referencing by using &
is dereferencing, which is accomplished with the dereference operator, *
. I will not talk about that here, maybe in a future note.
