Introduction to Borrowing and References
Borrowing is one of Rust's most distinctive and powerful features. It allows you to access data without taking ownership, enabling multiple parts of your code to work with the same data safely. The borrowing system, enforced by the compiler, prevents data races and dangling references at compile time.
Key Concepts
- References: Pointers that borrow values without owning them
- Borrowing: The act of creating references to data
- Lifetimes: How long references are valid
- Borrowing Rules: At any time, you can have either:
- One mutable reference
- Any number of immutable references
- No Dangling References: The compiler ensures references always point to valid data
1. Basic References
Immutable References
fn main() {
let s = String::from("hello");
// Create an immutable reference (borrow)
let r1 = &s;
let r2 = &s; // Multiple immutable references are allowed
println!("r1: {}, r2: {}", r1, r2);
// References are dereferenced automatically in many contexts
let len = r1.len(); // No need for (*r1).len()
println!("Length: {}", len);
// Function that takes a reference
print_string(&s);
// s is still usable here
println!("Original: {}", s);
}
fn print_string(s: &String) {
println!("Borrowed: {}", s);
}
Mutable References
fn main() {
let mut s = String::from("hello");
// Create a mutable reference
let r = &mut s;
// Modify through the mutable reference
r.push_str(", world");
println!("r: {}", r);
// Can't use s while r is active
// println!("{}", s); // Error: cannot borrow as immutable
// After r goes out of scope, s can be used again
println!("s: {}", s); // Now OK
// Multiple mutable references not allowed
let mut t = String::from("test");
let r1 = &mut t;
// let r2 = &mut t; // Error: cannot borrow as mutable more than once
// But we can create new mutable reference after previous is done
let r1 = &mut t;
r1.push_str("1");
// r1 goes out of scope here
let r2 = &mut t; // This is fine
r2.push_str("2");
println!("t: {}", t);
}
Reference Rules Demonstration
fn main() {
let mut data = String::from("data");
// Rule 1: Many immutable references allowed
let immut1 = &data;
let immut2 = &data;
println!("immut1: {}, immut2: {}", immut1, immut2);
// immut1 and immut2 go out of scope here
// Now we can have a mutable reference
let mut_ref = &mut data;
mut_ref.push_str(" modified");
println!("mut_ref: {}", mut_ref);
// mut_ref goes out of scope
// Can't mix mutable and immutable
let immut = &data;
// let mut_ref = &mut data; // Error: cannot borrow as mutable
println!("immut: {}", immut);
// Scope matters: references are valid until last use
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("r1: {}, r2: {}", r1, r2);
// r1 and r2 no longer used after this line
let r3 = &mut s; // This is allowed
r3.push_str(" world");
println!("r3: {}", r3);
}
2. Borrowing in Functions
Function Parameters with References
// Function that borrows immutably
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but since it was borrowed, nothing happens
// Function that borrows mutably
fn modify_string(s: &mut String) {
s.push_str(" - modified");
}
// Function that borrows multiple parameters
fn compare_strings(s1: &String, s2: &String) -> bool {
s1 == s2
}
// Function returning a reference (requires lifetime annotation)
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s1 = String::from("hello");
let s2 = String::from("world");
// Immutable borrow
let len = calculate_length(&s1);
println!("Length of '{}': {}", s1, len);
// Mutable borrow
modify_string(&mut s1);
println!("Modified: {}", s1);
// Multiple borrows
let are_equal = compare_strings(&s1, &s2);
println!("Equal: {}", are_equal);
// Returning reference
let sentence = String::from("hello world");
let word = first_word(&sentence);
println!("First word: {}", word);
}
Borrowing and Ownership Transfer
fn take_ownership(s: String) {
println!("Took ownership of: {}", s);
} // s dropped here
fn borrow(s: &String) {
println!("Borrowed: {}", s);
}
fn borrow_and_modify(s: &mut String) {
s.push_str("!");
}
fn give_ownership() -> String {
let s = String::from("new string");
s // Ownership transferred out
}
fn main() {
let s1 = String::from("hello");
// Borrowing doesn't transfer ownership
borrow(&s1);
println!("Still have s1: {}", s1);
// After taking ownership, can't use
take_ownership(s1);
// println!("{}", s1); // Error: s1 moved
// Get ownership back
let s2 = give_ownership();
println!("Got: {}", s2);
// Mutable borrow
let mut s3 = String::from("rust");
borrow_and_modify(&mut s3);
println!("After modify: {}", s3);
}
3. Dereferencing
Explicit Dereferencing
fn main() {
let x = 5;
let y = &x;
// Dereference to get the value
assert_eq!(5, x);
assert_eq!(5, *y);
// With mutable references
let mut z = 10;
let w = &mut z;
*w += 5; // Modify through dereference
println!("z after modification: {}", z);
// Dereferencing complex types
let s = String::from("hello");
let r = &s;
// These are equivalent:
println!("Length: {}", (*r).len());
println!("Length: {}", r.len()); // Auto-dereferencing
// Dereferencing in patterns
let maybe_ref = Some(&5);
match maybe_ref {
Some(&value) => println!("Got value: {}", value),
None => (),
}
// Equivalent using deref in pattern
match maybe_ref {
Some(value) => println!("Got ref to: {}", value),
None => (),
}
}
Auto-dereferencing Rules
struct Point {
x: i32,
y: i32,
}
impl Point {
fn distance_from_origin(&self) -> f64 {
((self.x.pow(2) + self.y.pow(2)) as f64).sqrt()
}
}
fn main() {
let p = Point { x: 3, y: 4 };
let r = &p;
let rr = &&p;
let rrr = &&&p;
// All work due to auto-dereferencing
println!("r distance: {}", r.distance_from_origin());
println!("rr distance: {}", rr.distance_from_origin());
println!("rrr distance: {}", rrr.distance_from_origin());
// With mutable references
let mut v = vec![1, 2, 3];
let mut_ref = &mut v;
// push automatically dereferences
mut_ref.push(4);
println!("mut_ref: {:?}", mut_ref);
// Indexing also auto-derefs
println!("First element: {}", mut_ref[0]);
// Method calls auto-deref to find the right implementation
let s = String::from("hello");
let r1 = &s;
let r2 = &r1;
let r3 = &r2;
// All work - Rust automatically dereferences as needed
println!("r1 len: {}", r1.len());
println!("r2 len: {}", r2.len());
println!("r3 len: {}", r3.len());
}
4. Reference Types and Coercions
String References and Slices
fn main() {
// &String vs &str
let s = String::from("hello");
let string_ref: &String = &s;
let str_slice: &str = &s[..]; // Explicit conversion
// &String coerces to &str automatically (deref coercion)
fn takes_str(s: &str) {
println!("Got: {}", s);
}
takes_str(&s); // &String coerced to &str
takes_str("literal"); // Works directly with &str
// Array references and slices
let arr = [1, 2, 3, 4, 5];
let array_ref: &[i32; 5] = &arr;
let slice: &[i32] = &arr[1..4];
println!("Array ref: {:?}", array_ref);
println!("Slice: {:?}", slice);
// Vector references
let v = vec![1, 2, 3];
let vec_ref: &Vec<i32> = &v;
let vec_slice: &[i32] = &v[..];
println!("Vec ref: {:?}", vec_ref);
println!("Vec slice: {:?}", vec_slice);
}
Reference to Trait Objects
trait Drawable {
fn draw(&self);
}
struct Circle {
radius: f64,
}
impl Drawable for Circle {
fn draw(&self) {
println!("Drawing circle with radius {}", self.radius);
}
}
struct Square {
side: f64,
}
impl Drawable for Square {
fn draw(&self) {
println!("Drawing square with side {}", self.side);
}
}
fn draw_shape(shape: &dyn Drawable) {
shape.draw();
}
fn main() {
let circle = Circle { radius: 5.0 };
let square = Square { side: 3.0 };
// References to trait objects
let drawables: [&dyn Drawable; 2] = [&circle, &square];
for drawable in drawables {
drawable.draw();
}
// Function taking trait object reference
draw_shape(&circle);
draw_shape(&square);
// Boxed trait objects
let boxed: Box<dyn Drawable> = Box::new(Circle { radius: 10.0 });
boxed.draw();
}
5. Reference Patterns
Reference Patterns in Match
fn main() {
// Matching on references
let maybe_value: Option<i32> = Some(5);
let ref_to_option = &maybe_value;
match ref_to_option {
Some(value) => println!("Got value: {}", value),
None => println!("Got none"),
}
// Using ref keyword in patterns
let x = 5;
let y = Some(x);
match y {
Some(ref value) => println!("Got ref to: {}", value), // value is &i32
None => (),
}
// ref mut for mutable references
let mut z = Some(10);
match z {
Some(ref mut value) => {
*value += 5;
println!("Modified: {}", value);
}
None => (),
}
println!("z after: {:?}", z);
// Matching on slices
let arr = [1, 2, 3];
let slice = &arr[..];
match slice {
[first, second, third] => println!("Three elements: {}, {}, {}", first, second, third),
[first, ..] => println!("First element: {}", first),
[] => println!("Empty slice"),
}
// Matching on strings
let s = String::from("hello");
let s_ref = &s;
match s_ref {
s if s.starts_with('h') => println!("Starts with h: {}", s),
_ => println!("Other"),
}
}
Reference Patterns in Destructuring
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 10, y: 20 };
let point_ref = &point;
// Destructuring a reference
let Point { x, y } = point_ref;
println!("x: {}, y: {}", x, y); // x and y are &i32
// Using & in pattern to match references
let &Point { x: x_val, y: y_val } = point_ref;
println!("x_val: {}, y_val: {}", x_val, y_val); // x_val and y_val are i32
// With nested references
let nested = (1, &point);
match nested {
(num, Point { x, y }) => {
println!("num: {}, x: {}, y: {}", num, x, y);
}
}
// Mutable references in destructuring
let mut point = Point { x: 5, y: 10 };
let point_mut_ref = &mut point;
let Point { x, y } = point_mut_ref;
*x += 1;
*y += 2;
println!("point after: ({}, {})", point.x, point.y);
}
6. Borrowing and Lifetimes
Basic Lifetime Annotations
// Lifetime annotations specify relationship between references
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
// Multiple lifetimes
fn complex<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
println!("y: {}", y);
x // Return x with lifetime 'a
}
// Lifetime in struct
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part // Lifetime elided - same as self
}
}
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(&string1, &string2);
println!("Longest: {}", result); // Valid here
} // string2 dropped
// println!("Longest: {}", result); // Would be invalid if used
let excerpt;
{
let novel = String::from("Call me Ishmael. Some years ago...");
excerpt = ImportantExcerpt {
part: &novel[..20],
};
println!("Excerpt: {}", excerpt.part);
} // novel dropped, excerpt invalid after this
// println!("{:?}", excerpt.part); // Error: borrowed value doesn't live long enough
}
Lifetime Elision Rules
// No explicit lifetimes needed - elision rules apply
// Rule 1: Each parameter that is a reference gets its own lifetime parameter
fn first_word(s: &str) -> &str { // Elided: <'a>(s: &'a str) -> &'a str
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
// Rule 2: If there's exactly one input lifetime, it's assigned to all output lifetimes
fn identity(x: &i32) -> &i32 { // Elided: <'a>(x: &'a i32) -> &'a i32
x
}
// Rule 3: If there are multiple input lifetimes, but one is &self or &mut self,
// the lifetime of self is assigned to all output lifetimes
struct Container {
data: String,
}
impl Container {
fn get_data(&self) -> &str { // Elided: <'a>(&'a self) -> &'a str
&self.data
}
fn process(&self, other: &str) -> &str { // Elided: <'a>(&'a self, other: &str) -> &'a str
if other.len() > self.data.len() {
other // But wait, this doesn't work with elision!
} else {
&self.data
}
}
// The above doesn't actually work because the elided lifetime would
// force both return values to have lifetime 'a, but 'other' has a different lifetime
// Correct version:
fn process_fixed<'a>(&'a self, other: &str) -> &'a str {
if other.len() > self.data.len() {
&self.data // Return with lifetime 'a
} else {
&self.data
}
}
}
7. Advanced Borrowing Patterns
Borrowing in Structs
// Struct containing references
struct RefHolder<'a> {
reference: &'a i32,
}
// Struct with multiple references
struct Pair<'a, 'b> {
first: &'a str,
second: &'b str,
}
impl<'a, 'b> Pair<'a, 'b> {
fn new(first: &'a str, second: &'b str) -> Self {
Pair { first, second }
}
fn longest(&self) -> &str
where
'a: 'b, // 'a outlives 'b
{
if self.first.len() > self.second.len() {
self.first
} else {
self.second
}
}
}
// Struct with mutable reference
struct MutHolder<'a> {
value: &'a mut i32,
}
impl<'a> MutHolder<'a> {
fn increment(&mut self) {
*self.value += 1;
}
}
fn main() {
let x = 42;
let holder = RefHolder { reference: &x };
println!("Value: {}", holder.reference);
let s1 = String::from("hello");
let s2 = String::from("world");
let pair = Pair::new(&s1, &s2);
println!("Longest: {}", pair.longest());
let mut num = 10;
let mut holder = MutHolder { value: &mut num };
holder.increment();
holder.increment();
println!("num after: {}", num);
}
Borrowing in Enums
// Enum with references
enum JsonValue<'a> {
Null,
Boolean(bool),
Number(f64),
String(&'a str),
Array(Vec<JsonValue<'a>>),
Object(Vec<(&'a str, JsonValue<'a>)>),
}
// Enum with different reference types
enum Either<'a, 'b> {
Left(&'a str),
Right(&'b str),
}
impl<'a, 'b> Either<'a, 'b> {
fn as_str(&self) -> &str {
match self {
Either::Left(s) => s,
Either::Right(s) => s,
}
}
}
fn main() {
let s = "hello";
let json = JsonValue::String(s);
match json {
JsonValue::String(s) => println!("String: {}", s),
_ => println!("Not a string"),
}
let left = Either::Left("left");
let right = Either::Right("right");
println!("Left: {}", left.as_str());
println!("Right: {}", right.as_str());
}
8. Interior Mutability
RefCell for Interior Mutability
use std::cell::RefCell;
struct Messenger {
messages: RefCell<Vec<String>>,
}
impl Messenger {
fn new() -> Self {
Messenger {
messages: RefCell::new(Vec::new()),
}
}
fn send(&self, message: &str) {
// Borrow mutably even though self is immutable
self.messages.borrow_mut().push(message.to_string());
}
fn get_messages(&self) -> Vec<String> {
self.messages.borrow().clone()
}
fn count(&self) -> usize {
self.messages.borrow().len()
}
}
// Runtime borrow checking
fn main() {
let messenger = Messenger::new();
messenger.send("Hello");
messenger.send("World");
println!("Message count: {}", messenger.count());
// Multiple borrows
{
let msgs = messenger.messages.borrow();
println!("Messages: {:?}", *msgs);
// This would panic at runtime
// let mut msgs_mut = messenger.messages.borrow_mut(); // panic!
}
messenger.send("Another message");
// Iterate through messages
for msg in messenger.get_messages() {
println!("Message: {}", msg);
}
}
RefCell with Rc for Multiple Owners
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
impl Node {
fn new(value: i32) -> Rc<Self> {
Rc::new(Node {
value,
children: RefCell::new(vec![]),
})
}
fn add_child(node: &Rc<Node>, child: Rc<Node>) {
node.children.borrow_mut().push(child);
}
fn print_tree(node: &Rc<Node>, depth: usize) {
println!("{}{}", " ".repeat(depth), node.value);
for child in node.children.borrow().iter() {
Node::print_tree(child, depth + 1);
}
}
}
fn main() {
let root = Node::new(1);
let child1 = Node::new(2);
let child2 = Node::new(3);
let grandchild = Node::new(4);
Node::add_child(&root, Rc::clone(&child1));
Node::add_child(&root, Rc::clone(&child2));
Node::add_child(&child1, grandchild);
Node::print_tree(&root, 0);
// Multiple owners possible with Rc
let shared = Node::new(42);
let owner1 = Rc::clone(&shared);
let owner2 = Rc::clone(&shared);
Node::add_child(&owner1, Node::new(100));
println!("Owner1 count: {}", owner1.children.borrow().len());
println!("Owner2 count: {}", owner2.children.borrow().len());
}
9. Borrowing and Iterators
Iterator References
fn main() {
let v = vec![1, 2, 3, 4, 5];
// Iterate over references
for item in &v {
println!("Item: {}", item); // item is &i32
}
// Iterate over mutable references
let mut v2 = vec![1, 2, 3, 4, 5];
for item in &mut v2 {
*item *= 2;
}
println!("Doubled: {:?}", v2);
// Iterator adapters with references
let v3 = vec![1, 2, 3, 4, 5];
let sum: i32 = v3.iter().sum(); // iter() gives references
println!("Sum: {}", sum);
let doubled: Vec<i32> = v3.iter().map(|&x| x * 2).collect();
println!("Doubled: {:?}", doubled);
// Filter with references
let evens: Vec<&i32> = v3.iter().filter(|&&x| x % 2 == 0).collect();
println!("Evens: {:?}", evens);
// Chaining operations
let result: Vec<i32> = v3
.iter()
.filter(|&&x| x > 2)
.map(|&x| x * x)
.collect();
println!("Squares of numbers > 2: {:?}", result);
}
Custom Iterator with References
struct Window<'a, T> {
slice: &'a [T],
size: usize,
pos: usize,
}
impl<'a, T> Window<'a, T> {
fn new(slice: &'a [T], size: usize) -> Self {
Window {
slice,
size,
pos: 0,
}
}
}
impl<'a, T> Iterator for Window<'a, T> {
type Item = &'a [T];
fn next(&mut self) -> Option<Self::Item> {
if self.pos + self.size <= self.slice.len() {
let window = &self.slice[self.pos..self.pos + self.size];
self.pos += 1;
Some(window)
} else {
None
}
}
}
fn main() {
let data = vec![1, 2, 3, 4, 5, 6];
let windows = Window::new(&data, 3);
for window in windows {
println!("Window: {:?}", window);
}
// Using with other iterator methods
let sums: Vec<i32> = Window::new(&data, 2)
.map(|w| w.iter().sum())
.collect();
println!("Sums of pairs: {:?}", sums);
}
10. Borrowing and Closures
Closure Capture Modes
fn main() {
let s = String::from("hello");
// Closure captures by reference (Fn)
let print = || println!("s: {}", s);
print();
println!("Still have s: {}", s); // s still accessible
let mut counter = 0;
// Closure captures by mutable reference (FnMut)
let mut increment = || {
counter += 1;
println!("counter: {}", counter);
};
increment();
increment();
// println!("counter: {}", counter); // Can't borrow here
// Closure captures by value (FnOnce)
let consume = || {
let s = s; // s moved here
println!("Consumed: {}", s);
};
consume();
// consume(); // Can't call twice
// println!("s: {}", s); // s moved
// Mixing capture modes
let a = String::from("a");
let mut b = String::from("b");
let c = String::from("c");
let mixed = move || {
println!("a: {}", a); // by value (due to move)
b.push_str(" modified"); // by mutable reference
println!("b: {}", b);
drop(c); // by value
};
// a, b, c moved into closure
mixed();
// mixed(); // Can't call twice (FnOnce)
}
Borrowing in Returned Closures
// Returning closure that borrows
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
// x is copied/moved into closure
move |y| x + y
}
// Returning closure that borrows from environment (doesn't work)
// fn make_printer(s: &str) -> impl Fn() + '_ {
// || println!("{}", s) // Need to specify lifetime
// }
// Correct version with lifetime
fn make_printer<'a>(s: &'a str) -> impl Fn() + 'a {
move || println!("{}", s)
}
// Closure that borrows mutably
fn make_counter() -> impl FnMut() -> i32 {
let mut count = 0;
move || {
count += 1;
count
}
}
fn main() {
let add_five = make_adder(5);
println!("5 + 3 = {}", add_five(3));
let s = String::from("hello");
let printer = make_printer(&s);
printer();
let mut counter = make_counter();
println!("Count: {}", counter());
println!("Count: {}", counter());
println!("Count: {}", counter());
}
11. Borrowing and Error Handling
References in Results
#[derive(Debug)]
struct Data {
value: i32,
}
impl Data {
fn new(value: i32) -> Self {
Data { value }
}
fn get_value(&self) -> i32 {
self.value
}
}
fn find_data(data: &[Data], target: i32) -> Option<&Data> {
data.iter().find(|&item| item.value == target)
}
fn modify_data(data: &mut [Data], target: i32) -> Result<&mut Data, String> {
if let Some(item) = data.iter_mut().find(|item| item.value == target) {
Ok(item)
} else {
Err(format!("Data with value {} not found", target))
}
}
fn process_data(data: &[Data]) -> Result<Vec<i32>, Box<dyn std::error::Error>> {
let mut results = Vec::new();
for item in data {
if item.value < 0 {
return Err(format!("Negative value: {}", item.value).into());
}
results.push(item.value * 2);
}
Ok(results)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let data = vec![
Data::new(1),
Data::new(2),
Data::new(3),
Data::new(4),
];
if let Some(item) = find_data(&data, 3) {
println!("Found: {}", item.value);
}
let mut mutable_data = vec![
Data::new(10),
Data::new(20),
Data::new(30),
];
match modify_data(&mut mutable_data, 20) {
Ok(item) => {
item.value += 5;
println!("Modified: {}", item.value);
}
Err(e) => println!("Error: {}", e),
}
let processed = process_data(&data)?;
println!("Processed: {:?}", processed);
Ok(())
}
12. Best Practices and Common Patterns
Borrowing Guidelines
// 1. Use references for read-only access
fn read_only(data: &[i32]) -> i32 {
data.iter().sum()
}
// 2. Use mutable references when you need to modify
fn modify_in_place(data: &mut [i32]) {
for item in data {
*item *= 2;
}
}
// 3. Return owned data when you need to give ownership
fn create_data() -> Vec<i32> {
vec![1, 2, 3, 4, 5]
}
// 4. Use slices for more flexible borrowing
fn process_slice(data: &[i32]) -> i32 {
data.iter().sum()
}
// 5. Avoid long-lived borrows
fn process_with_temp() -> i32 {
let data = vec![1, 2, 3, 4, 5];
// Borrow only as long as needed
let sum = {
let slice = &data[..];
slice.iter().sum()
}; // Borrow ends here
// Can modify data here
sum
}
// 6. Use interior mutability sparingly
use std::cell::RefCell;
struct Cache {
data: RefCell<Vec<i32>>,
}
impl Cache {
fn new() -> Self {
Cache {
data: RefCell::new(Vec::new()),
}
}
fn get(&self, index: usize) -> Option<i32> {
self.data.borrow().get(index).copied()
}
fn set(&self, value: i32) {
self.data.borrow_mut().push(value);
}
}
fn main() {
// 1. Read-only
let data = vec![1, 2, 3];
let sum = read_only(&data);
println!("Sum: {}", sum);
// 2. Modify in place
let mut data = vec![1, 2, 3];
modify_in_place(&mut data);
println!("Modified: {:?}", data);
// 3. Create owned data
let new_data = create_data();
println!("New data: {:?}", new_data);
// 4. Work with slices
let slice = &data[1..3];
println!("Slice sum: {}", process_slice(slice));
// 5. Temporary borrow
let result = process_with_temp();
println!("Result: {}", result);
// 6. Interior mutability
let cache = Cache::new();
cache.set(42);
cache.set(100);
println!("Cache[0]: {:?}", cache.get(0));
println!("Cache[1]: {:?}", cache.get(1));
}
Common Patterns
// Builder pattern with references
struct QueryBuilder<'a> {
table: &'a str,
condition: Option<&'a str>,
limit: Option<usize>,
}
impl<'a> QueryBuilder<'a> {
fn new(table: &'a str) -> Self {
QueryBuilder {
table,
condition: None,
limit: None,
}
}
fn condition(mut self, condition: &'a str) -> Self {
self.condition = Some(condition);
self
}
fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
fn build(&self) -> String {
let mut query = format!("SELECT * FROM {}", self.table);
if let Some(cond) = self.condition {
query.push_str(&format!(" WHERE {}", cond));
}
if let Some(limit) = self.limit {
query.push_str(&format!(" LIMIT {}", limit));
}
query
}
}
// Visitor pattern with references
trait Visitor<'a> {
fn visit_str(&mut self, s: &'a str);
fn visit_int(&mut self, i: i32);
}
struct Printer {
output: String,
}
impl<'a> Visitor<'a> for Printer {
fn visit_str(&mut self, s: &'a str) {
self.output.push_str(&format!("String: {}\n", s));
}
fn visit_int(&mut self, i: i32) {
self.output.push_str(&format!("Int: {}\n", i));
}
}
fn visit_all<'a, V: Visitor<'a>>(visitor: &mut V, data: &[&'a str]) {
for item in data {
if let Ok(num) = item.parse::<i32>() {
visitor.visit_int(num);
} else {
visitor.visit_str(item);
}
}
}
fn main() {
// Builder pattern
let query = QueryBuilder::new("users")
.condition("age > 18")
.limit(10)
.build();
println!("Query: {}", query);
// Visitor pattern
let data = vec!["hello", "42", "world", "123"];
let mut printer = Printer {
output: String::new(),
};
visit_all(&mut printer, &data);
println!("Visitor output:\n{}", printer.output);
}
Conclusion
Rust's borrowing and reference system is a cornerstone of its memory safety guarantees:
Key Takeaways
- Borrowing Rules: At any time, you can have either:
- One mutable reference
- Any number of immutable references
- References:
&T: Immutable reference (shared borrow)&mut T: Mutable reference (exclusive borrow)
- Lifetimes: Ensure references never outlive the data they point to
- Dereferencing: Automatic and explicit dereferencing with
* - Interior Mutability:
RefCell<T>for runtime borrow checking
Best Practices
- Prefer immutable references by default
- Use mutable references only when mutation is needed
- Keep borrow scopes small to minimize conflicts
- Use slices (
&[T]) for flexible borrowing - Consider interior mutability only when necessary
- Understand lifetimes for complex reference relationships
Common Patterns
| Pattern | Use Case |
|---|---|
&self | Read-only methods |
&mut self | Modifying methods |
&[T] | Function parameters for sequences |
&str | String parameters |
RefCell<T> | Interior mutability |
Rc<RefCell<T>> | Shared mutable data |
Rust's borrowing system provides compile-time guarantees that prevent entire classes of bugs, making it one of the language's most powerful features while requiring some adjustment for developers coming from other languages.