References & Borrowing
References and borrowing allow you to use values without taking ownership of them. This is fundamental to writing efficient Rust code, as it enables you to access data without the expensive operations of moving or cloning values.
What are References?
A reference is like a pointer to data owned by another variable. Unlike pointers in other languages, Rust references are guaranteed to be valid and safe.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Pass a reference
println!("The length of '{}' is {}.", s1, len);
// s1 is still valid here because we didn't move it
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but because it's a reference, nothing is dropped
The &
symbol creates a reference, and the process of creating a reference is called borrowing.
Immutable References
By default, references are immutable:
fn main() {
let s = String::from("hello");
let r1 = &s; // Immutable reference
let r2 = &s; // Another immutable reference
println!("{} and {}", r1, r2);
// Multiple immutable references are allowed
// This would not compile:
// *r1 = String::from("world"); // Error: cannot assign
}
Multiple Immutable References
You can have multiple immutable references to the same data:
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{}, {}, and {}", r1, r2, r3);
// This is perfectly fine
}
Mutable References
To modify data through a reference, you need a mutable reference:
fn main() {
let mut s = String::from("hello"); // s must be mutable
change(&mut s); // Pass a mutable reference
println!("{}", s); // "hello, world"
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Mutable Reference Restrictions
You can have only one mutable reference to a particular piece of data at a time:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // Error: cannot borrow `s` as mutable more than once
println!("{}", r1);
}
This restriction prevents data races at compile time.
The Borrowing Rules
Rust enforces these borrowing rules at compile time:
- At any given time, you can have either:
- One mutable reference, OR
- Any number of immutable references
- References must always be valid
Rule 1: Exclusive Mutable Access
fn main() {
let mut s = String::from("hello");
// This works - one mutable reference
{
let r1 = &mut s;
r1.push_str(", world");
} // r1 goes out of scope here
// This works - new scope, new reference
let r2 = &mut s;
println!("{}", r2);
}
Rule 1: No Mixing Mutable and Immutable
fn main() {
let mut s = String::from("hello");
let r1 = &s; // Immutable reference
let r2 = &s; // Another immutable reference
// let r3 = &mut s; // Error: cannot borrow as mutable
println!("{} and {}", r1, r2);
// After this point, r1 and r2 are no longer used
let r3 = &mut s; // This is OK - immutable references are no longer active
println!("{}", r3);
}
Reference Scope and Non-Lexical Lifetimes
References are valid from where they're created until their last use:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // Immutable reference starts
let r2 = &s; // Another immutable reference starts
println!("{} and {}", r1, r2);
// r1 and r2 end here (last use)
let r3 = &mut s; // Mutable reference starts after immutable ones end
println!("{}", r3);
}
Dereferencing
Use the *
operator to access the value a reference points to:
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y); // Dereference y to get the value
// This would not compile:
// assert_eq!(5, y); // Error: can't compare integer with reference
}
Automatic Dereferencing
Rust automatically dereferences when calling methods:
fn main() {
let s = String::from("hello world");
let s_ref = &s;
// These are equivalent:
let len1 = s.len();
let len2 = s_ref.len(); // Automatic dereferencing
let len3 = (*s_ref).len(); // Manual dereferencing
println!("Lengths: {}, {}, {}", len1, len2, len3);
}
Borrowing in Function Parameters
Taking References as Parameters
fn print_string(s: &String) {
println!("{}", s);
} // s goes out of scope, but the String it points to is not dropped
fn modify_string(s: &mut String) {
s.push_str(" modified");
}
fn main() {
let mut original = String::from("hello");
print_string(&original);
modify_string(&mut original);
println!("{}", original); // "hello modified"
}
Returning References
Functions can return references, but they must live long enough:
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[..]
}
fn main() {
let my_string = String::from("hello world");
let word = first_word(&my_string);
println!("First word: {}", word);
}
Slices: A Special Kind of Reference
Slices are references to a contiguous sequence of elements:
String Slices
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
let hello_alt = &s[..5]; // Same as &s[0..5]
let world_alt = &s[6..]; // Same as &s[6..11]
let whole = &s[..]; // Same as &s[0..11]
println!("{} {}", hello, world);
}
Array Slices
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // [2, 3]
assert_eq!(slice, &[2, 3]);
for element in slice {
println!("{}", element);
}
}
Common Borrowing Patterns
Borrowing for Reading
fn get_length(s: &String) -> usize {
s.len()
}
fn contains_char(s: &String, c: char) -> bool {
s.contains(c)
}
fn main() {
let text = String::from("hello world");
println!("Length: {}", get_length(&text));
println!("Contains 'o': {}", contains_char(&text, 'o'));
// text is still available
println!("Original: {}", text);
}
Borrowing for Modification
fn append_exclamation(s: &mut String) {
s.push('!');
}
fn make_uppercase(s: &mut String) {
*s = s.to_uppercase();
}
fn main() {
let mut greeting = String::from("hello");
append_exclamation(&mut greeting);
println!("{}", greeting); // "hello!"
make_uppercase(&mut greeting);
println!("{}", greeting); // "HELLO!"
}
Borrowing in Loops
fn main() {
let words = vec!["hello", "world", "rust"];
// Borrowing each element
for word in &words {
println!("{}", word);
}
// words is still available
println!("Original vector: {:?}", words);
let mut numbers = vec![1, 2, 3, 4, 5];
// Borrowing mutable references
for num in &mut numbers {
*num *= 2;
}
println!("Doubled: {:?}", numbers);
}
Advanced Borrowing Concepts
Partial Borrows
You can borrow different parts of a struct simultaneously:
struct Point {
x: i32,
y: i32,
}
fn main() {
let mut point = Point { x: 5, y: 10 };
let x_ref = &mut point.x;
let y_ref = &point.y; // OK: borrowing different fields
*x_ref += 1;
println!("x: {}, y: {}", x_ref, y_ref);
}
Method Borrowing
impl Point {
fn distance_from_origin(&self) -> f64 {
((self.x.pow(2) + self.y.pow(2)) as f64).sqrt()
}
fn translate(&mut self, dx: i32, dy: i32) {
self.x += dx;
self.y += dy;
}
}
fn main() {
let mut p = Point { x: 3, y: 4 };
println!("Distance: {}", p.distance_from_origin()); // Immutable borrow
p.translate(1, 1); // Mutable borrow
println!("New position: ({}, {})", p.x, p.y);
}
Common Borrowing Mistakes and Solutions
Mistake 1: Trying to Modify Through Immutable Reference
fn main() {
let mut s = String::from("hello");
let r = &s; // Immutable reference
// s.push_str(", world"); // Error: cannot borrow as mutable
// *r = String::from("world"); // Error: cannot assign
println!("{}", r);
// Solution: Ensure reference is no longer needed
// After r is last used, we can mutate s again
s.push_str(", world");
println!("{}", s);
}
Mistake 2: Multiple Mutable References
fn main() {
let mut s = String::from("hello");
// let r1 = &mut s;
// let r2 = &mut s; // Error: multiple mutable borrows
// Solution: Use scopes to separate borrows
{
let r1 = &mut s;
r1.push_str(", world");
}
{
let r2 = &mut s;
r2.push('!');
}
println!("{}", s);
}
Mistake 3: Dangling References
// This would not compile:
// fn dangle() -> &String {
// let s = String::from("hello");
// &s // Error: returns a reference to data owned by function
// } // s goes out of scope and is dropped
// Solution: Return owned data instead
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership is moved out
}
Performance Benefits of Borrowing
Avoiding Unnecessary Clones
fn print_twice_bad(s: String) {
println!("{}", s);
println!("{}", s);
// s is moved into this function, can't be used by caller
}
fn print_twice_good(s: &String) {
println!("{}", s);
println!("{}", s);
// s is borrowed, caller retains ownership
}
fn main() {
let my_string = String::from("hello");
print_twice_good(&my_string);
// my_string is still available
println!("Still have: {}", my_string);
}
Efficient Data Processing
fn process_large_data(data: &[i32]) -> i32 {
data.iter().sum() // Process without taking ownership
}
fn main() {
let large_dataset = vec![1; 1_000_000];
let sum = process_large_data(&large_dataset); // No expensive move/copy
println!("Sum: {}", sum);
// large_dataset is still available for other operations
}
Best Practices
1. Prefer Borrowing Over Ownership
// Good: Takes a reference
fn analyze_text(text: &str) -> (usize, usize) {
(text.len(), text.chars().count())
}
// Less ideal: Takes ownership
// fn analyze_text(text: String) -> (usize, usize) {
// (text.len(), text.chars().count())
// }
2. Use String Slices for Parameters
// Good: Accepts both &String and &str
fn greet(name: &str) {
println!("Hello, {}!", name);
}
// Less flexible: Only accepts &String
// fn greet(name: &String) {
// println!("Hello, {}!", name);
// }
fn main() {
let owned_string = String::from("Alice");
let string_literal = "Bob";
greet(&owned_string); // Works
greet(string_literal); // Also works
}
3. Return Owned Data When Appropriate
fn create_greeting(name: &str) -> String {
format!("Hello, {}!", name) // Return owned String
}
fn main() {
let name = "World";
let greeting = create_greeting(name);
println!("{}", greeting);
}
References and borrowing are fundamental to Rust's memory safety guarantees. They allow you to write efficient code without sacrificing safety, enabling multiple readers or a single writer pattern that prevents data races at compile time.