Rust · Intermediate

Ownership, moves and borrowing

3 min read

Why it matters

Rust appeared in almost half of the Blockchain Developer job descriptions we read. Rust prevents whole classes of bugs (use after free, data races) at compile time through ownership rules. Learning to read what the compiler says about them is the first real step.

The concept

  • Every value has exactly one owner. When the owner goes out of scope, the value is freed.

  • Assigning or passing a value such as a String moves it: the old variable can no longer be used. Small Copy types (integers, booleans) are copied instead.

  • To use a value without taking ownership, borrow it: &value is a shared, read-only reference, and &mut value is an exclusive, mutable one.

  • The rule: at any time you can have many shared references or one mutable reference, not both. This is what rules out data races.

  • .clone() makes an explicit copy when you really need two owners.

Worked example: a moved value

moved.rs
fn main() {
    let name = String::from("asha");
    let owner = name; // ownership moves to `owner`
    println!("{name} {owner}");
}
shellsession
$ rustc --edition 2021 moved.rs
error[E0382]: borrow of moved value: `name`
 --> moved.rs:4:16
  |
2 |     let name = String::from("asha");
  |         ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
3 |     let owner = name; // ownership moves to `owner`
  |                 ---- value moved here
4 |     println!("{name} {owner}");
  |                ^^^^ value borrowed here after move

The compiler points at all three places: where the value was created, where it moved, and where it was used afterwards. It also suggests cloning. Usually the better fix is to borrow.

Worked example: borrowing

borrow.rs
fn total_length(names: &[String]) -> usize {
    names.iter().map(|n| n.len()).sum()
}

fn add_name(names: &mut Vec<String>, name: &str) {
    names.push(name.to_string());
}

fn main() {
    let mut names = vec![String::from("asha"), String::from("ravi")];
    println!("total before: {}", total_length(&names)); // shared borrow
    add_name(&mut names, "meera"); // exclusive (mutable) borrow
    println!("total after: {}", total_length(&names));
    let owner = names.clone(); // an explicit copy if you need two owners
    println!("{} names, clone has {}", names.len(), owner.len());
}
shellsession
$ rustc --edition 2021 borrow.rs && ./borrow
total before: 8
total after: 13
3 names, clone has 3

total_length only reads, so it takes &[String]; add_name changes the list, so it takes &mut Vec<String>. main keeps ownership throughout.

Common mistakes

  • Cloning everything to silence the compiler. It works but hides design problems and costs performance.

  • Taking ownership in function parameters (Vec<String>) when a borrow (&[String]) is enough.

  • Fighting lifetimes early. Most code needs no explicit lifetime annotations; restructure first.

Interview angle

"What is ownership in Rust?" "What is the difference between &T and &mut T?" "Why can't you have a mutable and a shared reference at the same time?" (it prevents data races and iterator invalidation). "When is clone appropriate?"

Practice task

  1. Write longest(names: &[String]) -> Option<&String> and explain why the returned reference cannot outlive the list.

  2. Rewrite a small Python script of yours in Rust and note every compiler error you hit and what it taught you.

Further reading