Rust is a language that allows programs to run safely and efficiently without requiring manual memory management. At the core of Rust's memory safety model are the concepts of Ownership and Borrowing. This article provides a detailed explanation of ownership and borrowing, including practical code examples, to help beginners and intermediate users understand these fundamental concepts.
1. What is Ownership?
Rust's ownership system is based on three fundamental rules:
- Each value in Rust has a single owner.
- When the owner goes out of scope, the value is automatically dropped.
- Ownership can be transferred (moved), but a value can only have one owner at a time.
These rules ensure memory safety without requiring a garbage collector.
1.1 Moving Ownership
Ownership is transferred when a variable is assigned to another variable. In the following example, the ownership of a String
moves from s1
to s2
:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // Compilation error
}
Since s1
's ownership has moved to s2
, s1
is no longer valid. This restriction prevents double-free errors.
1.2 Cloning to Copy Data
To copy a value instead of transferring ownership, we use the clone
method:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}", s1); // No issue
}
Using clone()
, a new memory allocation is made, allowing both variables to remain valid.
1.3 Copying Stack-Based Types
Primitive types, such as i32
, are stored on the stack and implement the Copy
trait. This means they are copied rather than moved when assigned:
fn main() {
let x = 5;
let y = x; // `x` remains valid
println!("{}", x);
}
1.4 Ownership and Functions
Ownership also transfers when passing values to functions:
fn takes_ownership(s: String) {
println!("{}", s);
} // `s` is dropped here
fn main() {
let s = String::from("hello");
takes_ownership(s);
println!("{}", s); // Error: ownership moved
}
When s
is passed to takes_ownership
, its ownership moves into the function, making it invalid in main
.
2. What is Borrowing?
Borrowing allows references to a value without transferring ownership. Rust has two types of borrowing:
- Immutable Borrowing
- Mutable Borrowing
2.1 Immutable Borrowing
An immutable borrow allows reading a value without modifying it.
fn print_length(s: &String) {
println!("Length: {}", s.len());
}
fn main() {
let s = String::from("hello");
print_length(&s);
println!("{}", s); // No issue
}
Multiple immutable borrows are allowed at the same time.
2.2 Mutable Borrowing
A mutable borrow allows modifying a value, but only one mutable reference is permitted at a time:
fn change(s: &mut String) {
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // hello world
}
This rule prevents data races and ensures safe concurrent access.
3. Lifetimes and Borrowing
Lifetimes define how long a reference remains valid. By specifying lifetimes, the Rust compiler ensures that references remain valid throughout their usage.
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
fn main() {
let s1 = "long string";
let s2 = "short";
let result = longest(s1, s2);
println!("{}", result);
}
By specifying 'a
as the lifetime parameter, Rust ensures that both s1
and s2
outlive the returned reference.
4. Summary of Ownership and Borrowing
- Each value has only one owner, and ownership transfers on assignment or passing to a function.
- Moving ownership invalidates the original variable.
- Cloning creates a new copy, allowing both values to remain valid.
- Immutable borrowing allows multiple simultaneous references.
- Mutable borrowing allows modification but only one reference at a time.
- Lifetimes ensure that references remain valid for a defined scope.