Rust's most unique feature, understand the rules that guarantee memory safety without garbage collection.
Ownership is Rust's most distinctive feature and is crucial to understanding how Rust ensures memory safety without garbage collection. It's also the source of most beginners' struggles with Rust. But once you understand it, you'll appreciate its power.
Ownership is a set of rules that governs how a Rust program manages memory. Every value in Rust has an owner, and when the owner goes out of scope, the value is dropped (memory is freed). This ensures memory is always properly cleaned up.
To understand ownership, it helps to understand how the stack and heap work:
fn main() {
let x = 5; // Small integer on stack
let y = "hello"; // String literal (stack)
let s = String::new(); // String on heap (pointer on stack)
}
Rust has three fundamental rules of ownership:
These three rules are the foundation of Rust's memory safety. The compiler enforces them at compile-time, so there is zero runtime cost for memory management.
When you assign a value from one variable to another, ownership is transferred. This is called a "move":
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1's ownership moves to s2
// This would cause a compile error:
// println!("{}", s1); // Error: s1 has been moved
println!("{}", s2); // This works fine
}
For simple types like integers, Rust copies the value instead of moving. This is because they're stored on the stack and are cheap to copy:
fn main() {
let x = 5;
let y = x; // x is copied (integers implement Copy)
println!("x = {}, y = {}", x, y); // Both are valid
}
When you want to make a copy of heap data, explicitly call clone():
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Explicit deep copy
println!("s1 = {}, s2 = {}", s1, s2); // Both are valid
}
Types that implement the Copy trait are automatically copied instead of moved. Simple types like integers, floats, booleans, and characters implement Copy:
fn main() {
let x = 42; // i32 implements Copy
let y = x; // x is copied automatically
let z: bool = true; // bool implements Copy
let w = z; // z is copied automatically
println!("x={}, y={}, z={}, w={}", x, y, z, w);
}
When you pass a value to a function, ownership is transferred (or copied):
fn take_ownership(s: String) {
println!("I own: {}", s);
} // s is dropped here
fn copy_integer(x: i32) {
println!("I have: {}", x);
} // x is not dropped because i32 implements Copy
fn main() {
let s = String::from("hello");
take_ownership(s); // s's ownership moves into function
// This would error: println!("{}", s); // s no longer valid
let num = 5;
copy_integer(num); // num is copied
println!("num = {}", num); // num is still valid
}
Functions can also return ownership:
fn give_ownership() -> String {
String::from("hello")
fn take_and_give_back(s: String) -> String {
s // Return ownership
fn main() {
let s1 = give_ownership();
let s2 = String::from("world");
let s3 = take_and_give_back(s2); // s2 moves in, s3 receives ownership
println!("s1 = {}, s3 = {}", s1, s3);
}
Instead of transferring ownership, you can lend a reference to a value. A reference allows you to use a value without taking ownership:
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but it doesn't own s, so nothing happens
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Borrow s1
println!("'{}' has length {}", s1, len); // s1 is still valid
}
You can modify a value through a mutable reference:
fn change_string(s: &mut String) {
s.push_str(" world");
fn main() {
let mut s = String::from("hello");
change_string(&mut s); // Mutable borrow
println!("{}", s); // Prints: hello world
}
At any given time, you can have EITHER one mutable reference OR any number of immutable references. You cannot have both.
fn main() {
let mut s = String::from("hello");
let r1 = &s; // Immutable reference
let r2 = &s; // Another immutable reference (OK)
println!("{}, {}", r1, r2);
// This would error (can't mix mutable and immutable):
// let r3 = &mut s;
let r3 = &mut s; // Mutable reference (now OK after r1, r2 are gone)
r3.push_str(" world");
println!("{}", r3);
}
A slice is a reference to a contiguous sequence of elements within a collection. Slices don't take ownership:
fn main() {
let s = String::from("Hello World");
let hello = &s[0..5]; // "Hello"
let world = &s[6..11]; // "World"
println!("{} {}", hello, world);
}
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..4]; // [2, 3, 4]
for &item in slice {
println!("Item: {}", item);
}
}
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..] // Return the whole string
fn main() {
let s = String::from("hello world");
let word = first_word(&s);
println!("First word: {}", word); // Prints: First word: hello
}
Rust prevents dangling references, references to data that no longer exists. The compiler catches these at compile-time:
fn dangle() -> &String { // This won't compile!
let s = String::from("hello");
&s // s is dropped at end of function, so reference is invalid
} // ERROR: `s` does not live long enough
fn no_dangle() -> String { // This works!
let s = String::from("hello");
s // Ownership is transferred, not a reference
fn main() {
let s = no_dangle();
println!("{}", s);
}
Rust's borrow checker catches dangling references at compile-time. In C or C++, these would be runtime bugs that are notoriously difficult to track down.
Ownership is powerful once you understand it. Here's a practical example that brings everything together:
fn main() {
let mut text = String::from("Ownership");
// Immutable borrow - we can read
let len = get_length(&text);
println!("Length: {}", len);
// Mutable borrow - we can modify
add_exclamation(&mut text);
println!("Text: {}", text);
// Transfer ownership
let owned = text;
println!("Owned: {}", owned);
// text is no longer valid here
fn get_length(s: &String) -> usize {
s.len()
fn add_exclamation(s: &mut String) {
s.push_str("!!!");
}
let s2 = s1;)let s2 = s1.clone();)&s or &mut s)&s[0..5])