Step 7. Ownership and borrowing
Why#
In languages without a garbage collector the programmer is responsible for freeing memory; in languages with one you pay with pauses and unpredictability. OxL takes a third way: every value has exactly one owner; when the owner goes out of scope the value is destroyed. This single rule gives no leaks, no double frees and no data races between tasks - and it is all checked at compile time. There are no explicit lifetimes (as in Rust): the check is simpler (§7.2).
Three rules:
- Assignment, passing to a function and returning move the value (
Copytypes - numbers,str,bool- are simply copied). &xis a shared borrow (read only),&var xan exclusive one (read and write). While a&varexists there are no other references; while a&exists you cannot modify.x.clone()is an explicit independent copy.
Example#
fn total(xs: List<i32>) -> i32 pure { // a by-value parameter: the function becomes the owner
var s = 0
for x in xs { s += x }
s
}
fn count(xs: &List<i32>) -> usize pure => xs.len() // &T reads without taking ownership
fn addOne(xs: &var List<i32>) { // &var T is an exclusive mutable borrow
xs.push(1)
}
fn main() io {
var xs = [10, 20, 30]
print("{count(&xs)}") // &xs lends the value; xs stays ours
addOne(&var xs) // &var must be written at the call site
let copy = xs.clone() // clone makes an independent copy
print("{total(xs)}") // xs is moved into total; it cannot be used afterwards
print("{copy.len()} {copy}")
}3 61 4 [10, 20, 30, 1]
Walkthrough#
total(xs: List<i32>)- a by-value parameter: by callingtotal(xs)you gave away the list. Afterwardsxscannot be used.count(xs: &List<i32>)and the callcount(&xs)- the function only reads and takes nothing.addOne(&var xs)-&varis written explicitly at the call site: the reader sees at once that the function may change the argument.clone()is expensive (it copies the data), so it never happens implicitly.
Common mistakes#
Use after a move:
fn main() {
let xs = [1, 2, 3]
let ys = xs // the value moved into ys
print(xs.len()) // error: xs no longer owns the list
}use-after-move
The diagnostic use-after-move also points at the place of the move. Fixes: pass &xs, make
xs.clone() or return the passed value from the function.
Modification while reading:
fn main() {
var xs = [1, 2, 3]
let first = &xs
xs.push(4) // cannot mutate while &xs is alive
print(first.len())
}borrow-conflict
borrow-conflict: while first holds a borrow of the list, push is forbidden - otherwise the pointer could
dangle after the memory is reallocated. A borrow lives until its last use, so it is often enough to
swap lines. References cannot be stored in fields of data or passed to other tasks (borrow-escape): for
shared data use freeze(x) - an immutable value that may be shared between tasks (§7.3).
Remember#
- One owner; move by default;
&to read,&varto modify,cloneto copy. Copytypes (numbers,bool,str,bytes) do not fear moves.- Ownership errors are compile errors, not runtime crashes.
Next: effects.