The short version is simple:
- every value has an owner
- there is only one owner at a time
- when the owner goes out of scope, the value is dropped
Everything else is built on top of that.
Ownership in one example#
let s = String::from("hello");Here, s owns the String. When s goes out of scope, Rust automatically frees that heap-allocated memory.
This is how Rust avoids a garbage collector while still keeping memory management safe.
Moves happen by default for owned values#
Some types are moved when assigned:
let a = String::from("hello");
let b = a;
// println!("{a}"); // invalid: a was moved
println!("{b}");After let b = a;, ownership moved from a to b.
This prevents double-free bugs. If both bindings thought they owned the same String, both would try to clean it up.
Copy types behave differently#
Small stack-only types like integers are copied instead of moved:
let x = 10;
let y = x;
println!("{x}");
println!("{y}");That works because i32 implements Copy.
As a beginner rule:
- integers, booleans, and chars are usually cheap copies
String,Vec<T>, and most heap-owning types move unless you explicitly clone them
Borrowing lets you use a value without taking ownership#
Passing ownership everywhere would be awkward, so Rust lets you borrow:
fn print_length(s: &String) {
println!("{}", s.len());
}
let name = String::from("m58");
print_length(&name);
println!("{name}");The function receives a reference, not ownership, so name remains valid afterward.
Mutable borrowing#
If a function should modify a value in place, borrow mutably:
fn append_world(s: &mut String) {
s.push_str(" world");
}
let mut text = String::from("hello");
append_world(&mut text);This is safe because Rust enforces exclusivity for mutable borrows.
Borrowing rules#
The borrow checker enforces a small set of rules:
- you can have any number of immutable references
- or exactly one mutable reference
- references must never outlive the value they point to
That is the core safety story.
For example, this is invalid:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
// let r3 = &mut s; // invalid while r1 and r2 exist
println!("{r1} {r2}");Rust prevents mixing shared reads and exclusive mutation at the same time.
Slices are borrowed views#
Borrowing is not just &String or &Vec<T>. Slices are borrowed views into part of a value:
let text = String::from("hello");
let first = &text[0..2];
println!("{first}");For strings and vectors, slice types come up constantly:
&stris a borrowed string slice&[T]is a borrowed slice of elements
That is why idiomatic Rust APIs often prefer:
fn greet(name: &str) { /* ... */ }
fn sum(values: &[i32]) { /* ... */ }These signatures are more flexible than taking String or Vec<i32> directly.
Cloning is explicit#
If you really need a second owned copy, call clone():
let a = String::from("hello");
let b = a.clone();
println!("{a}");
println!("{b}");Rust makes cloning explicit on purpose. Hidden deep copies make performance harder to reason about.
Why this model is worth the trouble#
Ownership and borrowing buy Rust three things at once:
- memory safety without a garbage collector
- clear data flow at function boundaries
- fewer accidental aliasing and lifetime bugs
That is the real reason the compiler feels strict. It is enforcing rules that many other languages leave to runtime bugs or discipline.
Summary#
- owned values move by default
- borrowed values let you read or mutate without taking ownership
Copytypes are duplicated implicitly, but heap-owning types usually are not- the borrow checker prevents invalid sharing and mutation patterns
If you are learning Rust, treat ownership as the main concept, not an advanced one. Most of the language becomes more coherent once this part is solid.