Ownership and borrowing
Read this lesson twice. You already know a binding, a String on the heap, and that passing a String into a function can take it away. These four sections are the rules, in order: one owner, then a borrow, then a slice, then Copy, Clone, and Drop.
One owner
| Rule | What it means |
|---|---|
| One owner | Each value has exactly one owner. |
| Drop | When the owner leaves scope, the value is dropped and the memory is freed. |
| Move | There is only one owner at a time. A move transfers that ownership. |
let a = String::from("hi") is one owner and one heap allocation.
A block ends the owner. Inside { let s = String::from("temp") } the heap is freed at the closing brace. That cleanup is deterministic. There is no manual free and no garbage-collector pause. Drop is the name of that cleanup. The last section of this lesson shows it on files and locks.
A function parameter that takes String takes ownership. After take(s), s is gone. A parameter that takes i32 copies, and the caller still has the number. A function can give ownership back: fn into_upper(s: String) -> String. At every boundary, ask who owns the data. Taking String is a strong contract and a stiff API. Borrowing, next, is what callers usually want.
fn consume(s: String) {
println!("owned: {s}");
}
fn main() {
let a = String::from("hi");
let b = a;
println!("{b}");
let s = String::from("x");
consume(s);
let n = 1;
let m = n;
println!("{n} {m}");
}Borrows
A borrow is temporary access that does not take ownership. &T is a shared read. &mut T is an exclusive write.
| At one moment | Allowed |
|---|---|
Many shared references, &T | Yes |
Exactly one &mut T | Yes |
| Shared and mutable at once, on the same data | No |
| A reference that outlives the data | No. The lifetime syntax is a later lesson. The rule starts now. |
Several & references can look at the same value. Nobody is changing it, so they cannot race.
fn len(s: &String) -> usize { s.len() } borrows. After let n = len(&s) the owner s is still usable. fn push_bang(s: &mut String) needs let mut s and the call push_bang(&mut s). Method calls often follow the reference for you. When you assign through a mutable reference to a Copy value, write the star: let r = &mut x; *r += 1.
Prefer &str over &String on a parameter. &str accepts a literal and an owned string: greet("Ada") and greet(&owned). A String is the owner: pointer, length, and capacity, plus the heap bytes. An &str is two words, a pointer and a length, aimed at some UTF-8. &String can become &str automatically.
| The caller needs | The parameter takes |
|---|---|
| A read | &T or &str |
| A change in place | &mut T |
| To store the value past the call | T, by value |
| To keep a new value | Return an owned T. Return a reference only when it is tied to something the caller already passed in. |
fn greet(name: &str) {
println!("hi {name}");
}
fn add_exclaim(s: &mut String) {
s.push('!');
}
fn main() {
let mut owned = String::from("Ada");
greet(&owned);
add_exclaim(&mut owned);
greet("literal");
let mut x = 5;
let r = &mut x;
*r += 1;
println!("{owned} {x}");
}Slices
A slice is a reference to a contiguous sequence. It does not own that sequence. &[T] is a slice of T. &str is a slice of UTF-8 bytes, and it is always valid UTF-8.
The array or string is the owner. Indexes run from the start of that sequence.
On a string, &s[0..5] and &s[..] are slices. Do not cut in the middle of a multibyte character. That panics. Walk characters, or use a Unicode library, when the text is not ASCII. fn sum(xs: &[i32]) accepts an array, a subrange, and later a growable list, because each of those can lend a slice. &str and &[T] are two words, pointer plus length, so they do not need a terminator the way a C string does.
fn sum(xs: &[i32]) -> i32 {
let mut total = 0;
for n in xs {
total += n;
}
total
}
fn first_word(s: &str) -> &str {
for (i, b) in s.bytes().enumerate() {
if b == b' ' {
return &s[..i];
}
}
s
}
fn main() {
let xs = [10, 20, 30, 40];
println!("{} {}", sum(&xs[1..3]), first_word("hello rust"));
}Copy, Clone, and Drop
| Trait | When it runs | What it costs |
|---|---|---|
Copy | Implicitly, on assign and on pass | A bitwise duplicate. Integers, floats, bool, char, shared references &T, and tuples or arrays made only of those. A type that owns a heap buffer must not be Copy. |
Clone | Only when you write value.clone() | An explicit duplicate. Cloning a String allocates another buffer. derive(Clone) writes it when a bit copy is not enough. |
Drop | When the owner leaves scope, including an early return and a panic unwind | Cleanup. A file, a socket, or a lock closes here. There is no finally block for that. |
Opening a file, a socket, or a lock is tied to a value. That pairing is called RAII.
derive(Debug, Clone, Copy, PartialEq, Eq, Hash) on struct UserId(u64) is legal because the only field is Copy. The attribute still starts with a hash in real source. Derive Copy only when every field is Copy and a bit copy is the right meaning. Prefer a borrow when two places need to look at data. Clone when they each need an owned copy. Shared ownership, several owners of one allocation, is a later lesson. Do not reach for it to dodge a borrow.
Locals drop in reverse order of declaration. You rarely call drop yourself. std::mem::drop(x) moves the value and drops it early. A lock guard should unlock in Drop, so forgetting to unlock is not a path in the code.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct UserId(u64);
#[derive(Clone)]
struct Session {
token: String,
}
fn main() {
let id = UserId(7);
let id2 = id;
let session = Session { token: String::from("abc") };
let copy = session.clone();
println!("{:?} {:?}", id, id2);
println!("{}", copy.token);
}