Borrowing is Rust's mechanism for temporarily accessing data without taking ownership. It's a powerful feature that enables both flexibility and safety, letting you work with data in multiple ways while maintaining memory safety guarantees.
Understanding Borrowing
Borrowing allows you to reference data without transferring ownership. This is essential for writing flexible Rust code that doesn't constantly move values around.
The Basics
References come in two flavors: immutable and mutable.
let s = String::from("hello");
let len = calculate_length(&s);
println!("Length of '{}' is {}", s, len);The &s syntax creates a reference to s, allowing you to use it without taking ownership.
Immutable References
Immutable references let you read data without modifying it.
Multiple Immutable References
You can have multiple immutable references simultaneously:
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);This is safe because multiple readers can't cause data races.
Reference Scope
References are valid only within a certain scope:
let s = String::from("hello");
{
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
} // r1 and r2 go out of scope
let r3 = &s; // OKMutable References
Mutable references allow you to modify data through a reference.
Exclusive Mutable Access
You can have only one mutable reference at a time:
let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(", world");
// let r2 = &mut s; // Error!
println!("{}", r1);This prevents data races by enforcing exclusive write access.
Combining Mutable and Immutable
You can't have mutable and immutable references simultaneously:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
// let r3 = &mut s; // Error!
println!("{} and {}", r1, r2);But this works:
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1);
let r2 = &mut s; // OK, r1 is no longer used
r2.push_str(", world");Reference Lifetime
References have lifetimes that determine how long they're valid.
Implicit Lifetimes
Most of the time, Rust infers lifetimes automatically:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}The compiler ensures the returned reference is valid.
Explicit Lifetime Annotations
Sometimes you need to be explicit:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}The 'a lifetime parameter ensures the output lives as long as the inputs.
Lifetime Rules
Rust has three lifetime rules that the compiler enforces:
- 1Each parameter gets its own lifetime
- 2If there's exactly one input lifetime, it's assigned to all output lifetimes
- 3If there are multiple input lifetimes but one is &self or &mut self, that lifetime is assigned to all output lifetimes
Structs with Lifetimes
Structs can hold references with lifetime annotations:
struct ImportantExcerpt<'a> {
part: &'a str,
}
let novel = String::from("Call me Ishmael...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
part: first_sentence,
};Dangling References
Rust prevents dangling references at compile-time:
fn dangle() -> &String { // Error!
let s = String::from("hello");
&s
}The error: "returns a reference to data owned by the current function."
The Solution
Return the owned value instead:
fn no_dangle() -> String {
let s = String::from("hello");
s
}Or pass in a reference:
fn no_dangle(s: &String) -> &str {
&s[..]
}Borrowing in Practice
Let's explore practical borrowing patterns.
String Slices
String slices are common borrowing patterns:
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];Slices are references to portions of strings.
Array Slices
Works with arrays too:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // [2, 3]Function Parameters
Functions often borrow parameters:
fn print_coordinates(grid: &Vec<Vec<bool>>) {
for row in grid {
for cell in row {
print!("{}", if *cell { "■" } else { "□" });
}
println!();
}
}Borrowing and Iterators
Iterators make heavy use of borrowing:
let v = vec![1, 2, 3];
let iter = v.iter(); // Borrows immutably
for val in iter {
println!("{}", val);
}IntoIterator
Consumes the collection:
let v = vec![1, 2, 3];
for val in v {
println!("{}", val);
}
// v is no longer validIterMut
Mutable iteration:
let mut v = vec![1, 2, 3];
for val in v.iter_mut() {
*val *= 2;
}Smart Pointers and Borrowing
Smart pointers provide specialized borrowing behavior:
Box<T>
Single ownership, can be borrowed:
let b = Box::new(5);
let r = &b;
println!("{}", r);Rc<T>
Multiple ownership, can be borrowed:
use std::rc::Rc;
let a = Rc::new(5);
let b = Rc::clone(&a);
let r = &*a;
println!("{}", r);RefCell<T>
Interior mutability with runtime borrow checking:
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
let borrowed = data.borrow(); // Immutable borrow
let mut_borrowed = data.borrow_mut(); // Panic!Borrowing Rules in Methods
Methods borrow self in various ways:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 { // Borrows immutably
self.width * self.height
}
fn set_width(&mut self, width: u32) { // Borrows mutably
self.width = width;
}
}Common Borrowing Patterns
The AsRef Trait
Convert to references:
fn print<T: AsRef<str>>(s: T) {
println!("{}", s.as_ref());
}
print("hello");
print(String::from("hello"));The Borrow Trait
Convert between borrowed forms:
use std::borrow::Borrow;
fn eq<T: Borrow<str>>(a: T, b: &str) -> bool {
a.borrow() == b
}Cow (Clone on Write)
Borrow or clone as needed:
use std::borrow::Cow;
fn modify_string(s: Cow<str>) -> Cow<str> {
if s.contains("foo") {
let mut owned = s.into_owned();
owned.push_str("bar");
Cow::Owned(owned)
} else {
s
}
}Borrowing and Closures
Closures capture variables by various borrowing strategies:
Immutable Borrow
let list = vec![1, 2, 3];
let iter = list.iter();Mutable Borrow
let mut list = vec![1, 2, 3];
list.push(4);
let mut borrows_mutably = || list.push(5);
borrows_mutably();Take Ownership
let list = vec![1, 2, 3];
let takes_ownership = move || {
println!("{:?}", list);
};
takes_ownership();Borrow Checker Errors
Understanding common errors helps you write better Rust.
Use After Move
let s = String::from("hello");
let s2 = s;
println!("{}", s); // Error!Fix: Use s.clone() or borrow s2.
Mutable While Borrowed
let mut s = String::from("hello");
let r = &s;
s.push_str(" world"); // Error!Fix: Drop the reference before mutation.
Lifetime Mismatch
fn returns_str() -> &'static str {
let s = String::from("hello");
&s // Error!
}Fix: Return owned String or use 'static lifetime.
Advanced Borrowing
Multiple Lifetimes
Functions can have multiple lifetime parameters:
fn annotate<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where
'b: 'a,
{
if x.len() > y.len() {
x
} else {
x
}
}Lifetime Subtyping
Longer lifetimes can substitute for shorter ones:
fn choose_first<'a: 'b, 'b>(first: &'a str, second: &'b str) -> &'b str {
first
}Trait Objects with Lifetimes
trait Foo {}
fn use_foo<'a>(foo: &'a dyn Foo) {
// Use foo
}Borrowing and Concurrency
Borrowing enables safe concurrent programming:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}Move Closures
For concurrency, use move:
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});Debugging Borrowing Issues
The compiler provides helpful messages.
Reading Errors
"borrow of moved value" - use after move "cannot borrow as mutable" - existing immutable borrow "does not live long enough" - lifetime issue
Compiler Suggestions
Rust often suggests fixes:
// Consider borrowing or cloning the data
let s2 = s.clone();Best Practices
- 1Prefer borrowing over cloning
- 2Keep borrows as short as possible
- 3Use references for function parameters
- 4Understand lifetime elision rules
- 5Use Cow for optional cloning
- 6Design APIs to minimize borrowing constraints
Performance Considerations
Borrowing has zero runtime cost:
let s = String::from("hello");
let r = &s; // Just a pointerReferences are just pointers under the hood, with compile-time guarantees.
Conclusion
Borrowing is Rust's secret sauce for flexible, safe code. It lets you work with data without ownership transfers, enabling powerful patterns while maintaining memory safety.
Master borrowing, and you'll write Rust code that's both ergonomic and efficient. The borrow checker is your friend, guiding you toward correct code that avoids entire classes of bugs.
Practice with borrowing until it feels natural. It's the key to unlocking Rust's full potential for building reliable, performant software.