On this page
References and borrowing: using without owning
References: using without owning
In the previous lesson we saw that passing a String to a function transfers its ownership. This is correct but verbose: you have to return the value to continue using it. References are the solution.
A reference allows you to refer to a value without taking its ownership. It is created with the & operator and is called "borrowing" — you are borrowing the value without owning it.
fn print_length(s: &String) -> usize {
// s is a reference to String
// We can read s, but we don't own it
println!("The text is: {s}");
s.len()
}
fn main() {
let my_string = String::from("Rust is great");
// We pass a reference — we don't move ownership
let length = print_length(&my_string);
// my_string is still valid here
println!("'{my_string}' has {length} characters");
}The &String parameter in the function indicates it receives a reference. When s goes out of scope at the end of the function, drop() is not called because the function does not have ownership.
Immutable references: &T
An immutable reference (&T) allows reading the value but not modifying it:
fn main() {
let number = 42;
let reference = &number;
// Access through the reference (automatic dereferencing)
println!("Number: {number}");
println!("Through reference: {reference}");
println!("Dereferenced: {}", *reference); // explicit *
// Multiple immutable references at the same time: OK
let s = String::from("text");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{r1}, {r2}, {r3}"); // All valid simultaneously
// Comparison: references are automatically dereferenced
let a = String::from("hello");
let b = String::from("hello");
let ra = &a;
let rb = &b;
println!("Are they equal? {}", ra == rb); // true — compares content
}Mutable references: &mut T
A mutable reference allows modifying the borrowed value:
fn capitalize_first(s: &mut String) {
if let Some(first) = s.get_mut(0..1) {
first.make_ascii_uppercase();
}
}
fn add_punctuation(s: &mut String, punct: char) {
s.push(punct);
}
fn main() {
let mut message = String::from("hello rust");
capitalize_first(&mut message);
println!("{message}"); // "Hello rust"
add_punctuation(&mut message, '!');
println!("{message}"); // "Hello rust!"
}The fundamental rule of the borrow checker
The borrow checker applies a central rule that prevents data races:
At any given time, you can have either:
- One mutable reference (
&mut T), OR - Any number of immutable references (
&T)
But never both simultaneously.
fn main() {
let mut s = String::from("hello");
// Case 1: Multiple immutable references — OK
let r1 = &s;
let r2 = &s;
println!("{r1}, {r2}"); // r1 and r2 used here — their lives end here
// Case 2: One mutable reference — OK (r1 and r2 no longer in use)
let rm = &mut s;
rm.push_str(" world");
println!("{rm}");
// Case 3 (error): Mutable and immutable simultaneously
// let r3 = &s;
// let rm2 = &mut s; // Error: cannot borrow `s` as mutable
// // because it is also borrowed as immutable
// println!("{r3} {rm2}");
}NLL: Non-Lexical Lifetimes
The compiler is smart: references have a "lifetime" that ends at the last point where they are used, not necessarily at the end of the block:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}"); // r1 and r2 end here (NLL)
// Now we can create a mutable reference — r1 and r2 no longer exist
let rm = &mut s;
rm.push_str(" world");
println!("{rm}");
}This works thanks to Non-Lexical Lifetimes (NLL), introduced in Rust 2018. References don't live until the closing brace of the block, but until their last use.
Dangling references: the borrow checker saves you
In C/C++, it is possible to have a pointer pointing to memory that was already freed — a "dangling pointer." Rust makes this impossible at compile time:
// This code does NOT compile
fn create_dangling_reference() -> &String {
let s = String::from("hello");
&s // Error: s is dropped when this function exits
} // s is freed here — the reference would point to invalid memoryThe compiler rejects this code:
error[E0106]: missing lifetime specifierThe solution is to return the String directly (transferring ownership) rather than a reference:
fn create_string() -> String {
let s = String::from("hello");
s // Transfer ownership — not dangling
}Slices: references to parts of collections
Slices are a special type of reference that point to a contiguous sequence of elements:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[0..i]; // Slice of the first word
}
}
&s[..] // If no space, the whole string is one word
}
fn main() {
let sentence = String::from("hello world rust");
// String slices (&str)
let first = first_word(&sentence);
println!("First word: {first}");
// Array slices
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let half: &[i32] = &numbers[0..5];
let rest: &[i32] = &numbers[5..];
println!("Half: {:?}", half);
println!("Rest: {:?}", rest);
// sum(), min(), max() work on slices
let sum: i32 = half.iter().sum();
println!("Sum of first half: {sum}");
}Summary of borrowing rules
| Reference type | Allows | How many at once |
|---|---|---|
&T |
Read | Unlimited |
&mut T |
Read and write | Exactly 1 (and no &T) |
These rules guarantee that:
- There can be no data race conditions
- There can be no dangling pointers
- There can be no iterator invalidation
All verified at compile time, with no cost at runtime.
Now that you understand references and borrowing, the next lesson dives into lifetimes — the annotations the compiler uses to track how long references live.
fn calculate_length(s: &String) -> usize {
s.len() // Access without taking ownership
}
fn append_world(s: &mut String) {
s.push_str(", world");
}
fn main() {
let s1 = String::from("hello");
// & creates a reference: we borrow s1 without moving it
let length = calculate_length(&s1);
println!("'{s1}' has {length} characters"); // s1 still valid
let mut s2 = String::from("hello");
// &mut: mutable reference — only ONE at a time
append_world(&mut s2);
println!("{s2}"); // "hello, world"
// Multiple immutable references: OK
let r1 = &s1;
let r2 = &s1;
println!("{r1} and {r2}"); // Both valid
// Cannot have &mut while &T references exist
// let r3 = &s2;
// let r4 = &mut s2; // Error!
}
Sign in to track your progress