R Rust by Evidence PDF

Chapter 2 — Ownership and Moves

Goal

Predict when using a value transfers ownership and makes its previous binding unusable.

Prediction

fn main() {
    let label = String::from("draft");
    let saved = label;
    println!("{label} -> {saved}");
}

What happens?

A. Both names print because assignment aliases the same string.
B. Both names print because every value is copied.
C. Compilation fails because ownership moved from label to saved.
D. Compilation fails because String cannot be printed.

Reveal: read the evidence

Answer: C.

error[E0382]: borrow of moved value: `label`
  |
2 |     let label = String::from("draft");
  |         ----- move occurs because `label` has type `String`, which does not implement the `Copy` trait
3 |     let saved = label;
  |                 ----- value moved here
4 |     println!("{label} -> {saved}");
  |                ^^^^^ value borrowed here after move

The last line says “borrowed” because formatting temporarily reads label. That read is rejected: label no longer owns a valid String.

Build the model

A String is a small control value containing information such as a pointer, length, and capacity. Its character bytes are held in heap storage. When the owning String goes out of scope, Rust runs its destructor and releases that storage.

If assignment merely copied the control value, two Strings would later try to release the same allocation. Rust instead moves ownership:

let label = String::from("draft");
let saved = label;
// `saved` is now responsible for cleanup.

Working model: a value has one owner. Assigning or passing an owned value transfers it unless the type is cheaply copyable. After a move, stop using the old binding.

Function calls follow the same rule:

fn archive(text: String) {
    println!("archived: {text}");
}

fn main() {
    let note = String::from("ship it");
    archive(note);
    // `note` was moved into `archive`.
}

Ownership is returned when a function returns the value:

fn inspect(text: String) -> String {
    println!("{} bytes", text.len());
    text
}

That works, but passing ownership out and back is noisy when the function only needs to inspect data. Borrowing, introduced next, expresses that intent directly.

Precise model: move behavior applies to types that do not implement Copy. Types such as u32, bool, char, and many tuples of Copy values are copied on assignment:

let first = 7;
let second = first;
println!("{first} {second}");

Both integers remain usable. Copy is a trait with restrictions; it is not chosen dynamically by value size. String owns a resource and implements Drop, so it is not Copy.

A move is a language-level transfer, not necessarily a physical byte-by-byte operation you should try to visualize. Optimizations may remove the copy entirely. The dependable fact is which binding may still be used and which value will be cleaned up.

Simplest fix

Use the new owner:

fn main() {
    let label = String::from("draft");
    let saved = label;
    println!("saved as {saved}");
}

Often the compiler found a real design truth: the old name was unnecessary.

Tradeoff alternative: clone

fn main() {
    let label = String::from("draft");
    let saved = label.clone();
    println!("{label} -> {saved}");
}

clone creates an independent owned String, including another allocation and copied bytes. Use it when both owners genuinely need independent lifetimes or mutation. Do not add clone() automatically to silence E0382; a borrow is usually cheaper when code only reads the value.

Mini challenge

Which line fails?

fn consume(text: String) {
    println!("{text}");
}

fn main() {
    let message = String::from("hello");
    let count = message.len();
    consume(message);
    println!("{count}");
}

A. message.len()
B. consume(message)
C. println!("{count}")
D. No line fails.

Answer

D. len() only reads message. consume then moves it, but message is never used afterward. count is a usize, which is a separate Copy value.