A thought exercise in Rust ownership system for noobs.
Chapter 1: Move
let alice = String::from("happy");
let bob = alice;
// is alice still happy? NO.
// alice is now invalid.
// bob now has alice's livelihood.
println!("alice is {}", alice);
In Rust, a value can always have only one owner.
Let's return the happiness to alice
.
// ERROR!
alice = bob;
All variables are immutable by default.
Therefore, alice
will be sad forever.
Then carol
comes along and asks bob
if she could also be happy. bob
, sunken in regret of killing alice
, agree to try.
fn carol(v: String) {
println("carol is {}", v)
}
fn main() {
let alice = String::from("happy");
let bob = alice;
carol(bob);
// bob is dead.
println!("bob is {}", bob);
// so is alice
println!("alice is {}", alice);
}
carol
, realizing the hard truth of life, stand holding happy until the end of time (or lifetime of main
).
"happy" String was moved from alice
to bob
, then to carol
.
Chapter 2: Copy
let alice = "happy";
let bob = alice;
// Both can be happy!
println!("bob is {}", bob);
println!("alice is {}", alice);
Because Rust knows how big "happy"
, 1
, -20
, 0x08
, 3.1418
are beforehand, it simply creates a new copy for everyone.
Simple fixed size types are not moved; They are copied.
Chapter 3: Clone
In Chapter 1, if both alice
and bob
were to be happy, bob could have clone
d alice
's happiness.
let alice = String::from("happy");
let bob = alice.clone();
println!("bob is {}", bob);
// alice is still alive and well.
println!("alice is {}", alice);
When something is clone
d, it summons an evil doppelganger from the computer's limbo called the Heap. If the thing is huge, it means twice the memory and time.
It is costly to make everyone happy.
Chapter 4: Borrowing
In Chapter 1, what if carol
could have turned back time and chosen to only take happy
from bob
for a while, and return it to him?
let alice = String::from("happy");
// happiness moves from alice to bob.
// bob is also mutable so he can later
// get his `happy` back from carol.
let mut bob = alice;
fn carol(v: String) -> String {
// carol enjoys the happiness
println!("carol is {}", v);
// then returns it.
v
}
bob = carol(bob);
// bob is fine and dandy
println!("bob is {}", bob);
carol
has to return the moved happiness so somebody else can use.
But instead of always returning, which is forget-prone, carol
could just borrow.
First, let's prove that when something is moved, it is gone forever, even after the last owner is dead.
let alice = String::from("happy");
// New scope
{
let bob = alice;
// bob owns happiness.
println!("bob is {}", bob);
// bob is going out of scope.
}
// ERROR! alice is invalid forever.
println!("alice is {}", alice);
To settle for a middle ground, bob
borrows from alice
instead.
let alice = String::from("happy");
{
// bob borrows from alice.
// alice still owns the happiness.
// She just can't use it now.
let bob = &alice;
// bob enjoys happiness.
println!("bob is {}", bob);
// ERROR! alice tries to use happy
println!("alice is {}", alice);
// bob is out of scope, returning what
// he borrowed automatically.
}
// alice's livelihood is back :).
println!("alice is {}", alice);
carol
decides she better borrow from bob
while he is at it.
fn carol(v: &String) {
// carol enjoys the happiness she borrowed.
println!("carol is {}", v);
// carol is going out of scope.
}
fn main() {
let alice = String::from("happy");
{
let bob = &alice;
// bob enjoys happiness.
println!("bob is {}", bob);
// while carol is borrowing, bob has to wait.
// carol does not have to return ownership back to bob.
carol(&bob)
// bob now has the happiness because carol is done.
println!("bob is {} again", bob);
// bob is out of scope.
}
// alice can now be happy again, after bob and carol are done.
println!("alice is {}", alice);
}
Anyone can borrow taking a reference (&
) to a thing, but the lender must outlive the borrower.
Chapter 6: Lifetime
In Rust, a borrowed reference has a lifetime.
fn carol<'carol_t>(v: &'carol_t String) {
// 'carol_t is a lifetime of carol.
// This reads "carol has a lifetime `carol_t`,
// and she will only borrow `v` that lives at
// least `carol_t` long.
}
A lifetime is the time and scope of when a referenced value is created until the end.
carol
is hopeless at her pursue of happy-ness. So she gets into the loan business.
She creates an account with money inside and gives out to anyone who wants to borrow from her.
She uses reference so whoever borrows from her will always return the money when he/she dies.
// ERROR!
fn carol() -> &i32 {
let carol_reserve = 10;
&carol_reserve
}
Alas, because carol_reserve
has a short lifetime inside carol
, it cannot be borrowed from anyone. :(
// With lifetime annotation revealed.
fn carol<'carol_t>() -> &'carol_t i32 {
let carol_reserve: 'carol_t i32 = 25;
let carol_reserve_ref: &'carol_t i32 = &carol_reserve;
carol_reserve_ref
// But 'carol_t finishes here.
}
// let's say `main` has a lifetime named `main_t`.
fn main<'main_t>() {
// It is impossible for `broke_bob` to borrow
// from `carol()` because he lives longer than
// `carol_t`.
// (He lives slightly after `main_t` and dies just before)
let broke_bob<'bob_t> = &carol<'carol_t>();
}
The only way carol
could have pulled this off is to borrow the amount from somewhere that lives longer than herself (carol_t
) and broke_bob
(bob_t
).
// `CENTRAL_RESERVE` lives for the entire lifetime
// of a program.
static CENTRAL_RESERVE: i32 = 1000_000_000;
fn carol(money: &i32) -> &i32 {
money
}
fn main() {
broke_bob = carol(&CENTRAL_RESERVE);
}
The main point of lifetimes is to make sure a reference outlives whoever borrows it.
If you're interested in my non-programming thoughts, you can subscribe to my newsletter BETA School.
Top comments (0)