Rust Basics - Module 3: Borrowing and References
Part 3 of 5 in Rust BasicsIn the previous blog post, we have discussed about the "Ownership" and its three rules of ownership and how it enforces only one owner for the given data at the given time, so that our code is free from memory bugs. In this post, we'll cover references, borrowing rules, the borrow checker in action, and slices.
If ownership is the rule, borrowing is the exception that makes the rule practical.
Without borrowing, every function call would consume its arguments, and that's genuinely painful to work with.
Part 1: The Core Idea - References
A Reference is a pointer to a value that you do not own. We are borrowing it, like lending a book. The lender still owns it. When the borrow ends, the book goes back.
The syntax is &T for an immutable reference, &mut T for a mutable one.
let s = String::from("hello");
let r = &s // r borrows s - s still owns the heap data
println!("{}", r); // Valid, prints "hello"
println!("{}", s); // Valid, prints "hello"The & in &s creates a reference. The value r on the stack is just a pointer to the address of s . It does not own the heap data. When r goes out of scope, nothing is freed. When s goes out of scope, the heap is freed - because s is still the owner.
Stack
----------------------
r(&String) ptr --> s
----------------------
s(owner)
ptr -> heap
len: 5
cap: 5 -------------> Heap | Heap data 'h' 'e' 'l' 'l' 'o' |
---------------------- Part 2: Immutable vs Mutable References
Rust has exactly two references types, and the rules around them are asymmetric by design.
Immutable Reference
&TMutable Reference
&mut T
An immutable reference &T lets you read the value. You can have as many of these as you want simultaneously. No one can modify the value while any immutable reference exists.
A mutable reference &mut T lets you both read and write the value. You can have exactly one at a time. No immutable references can exist simultaneously with a mutable one.
let mut s = String::from("hello");
let r1 = &s; // fine, read-only
let r2 = &s; // fine, read-only // multiple immutable refs allowed at a same time
println!("{} {}", r1, r2);
let r3 = &mut s; // fine, r1 & r2 are no longer used after above println
r3.push_str(" world"); // r3 is a mutable one, so it can make changes to s's heap
println!("{}", r3);
// Not allowed, error: cannot borrow `s` as mutable because it is also borrowed as immutable
let r4 = &s;
let r5 = &mut s;
println!("{} {}", r4, r5); The rule stated precisely: at any given point in the code, you may have either one mutable reference OR any number of immutable references - never both at the same time.
Part 3: The Borrow Checker in action
Case 1: Simultaneous Mutable and Immutable References
let mut s = String::from("hello");
let r1 = &s; // immutable borrow begins
let r2 = &mut s; // cannot borrw s as mutable because r1 is still alive
println!("{}", r1);If this were allowed, r2 could modify s 's heap buffer, while r1 still holds the old pointer - classic use-after-free case.
Case 2: Two mutable references
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // cannot borrow s as mutable "more than once"
r1.push_str(" world");If both r1 and r2 could mutate s simultaneously, we have a data race - even on a single thread, the mutation order is ambiguous and the internal state (ptr, len, cap) in the stack could become inconsistent.
Case 3: Dangling Reference
fn dangle() -> &String { // returning a reference to local data
let s = String::from("hello");
&s // 's' will be dropped when function returns
}The compiler rejects this: s will be freed/deallocated when dangle returns, so the reference would point to freed memory when we tried to access it. In C this compiles fine and produces a dangling pointer. Rust refuses to compile it.
The solution is to return the String itself by giving up the ownership to the caller
fn no_dangle() -> String {
let s = String::from("hello");
s
}This works fine. Ownership is moved, and nothing is deallocated.
Part 4: &str VS String - a crucial distinction
Now that we understand references, &str should make complete sense.
String is an owned, heap-allocated, growable string. &str is a borrowed reference to a string slice, it is just a pointer and a length. It can point into a String 's heap buffer, or into a string literal in the binary's read-only segment.
let s: String = String::from("hello world");
let slice: &str = &s[0..5]; // borrows 5 bytes of s's heap buffer
let literal: &str = "hello"; // points into read-only binary segmentThe idiomatic guideline: if your function only needs to read a string, take &str - not &String . This is more flexible because both String and &str can be passed as &str :
fn print_it(s: &str) {
println!("{}", s);
}
let owned = String::from("hello");
print_it(&owned); // &String auto-coerces to &str
print_it("world"); // &str literal works directlyThis auto-coercion is called de-ref coercion - &String automatically becomes &str when the context demands it. We will discuss more about this in the traits module.
Before we wrap up, there's one more borrowed type worth understanding - slices.
Part 5: Slices - references to a contiguous sequence
The same idea generalises beyond strings. A slice &[T] is a reference to a contiguous portion of an array or Vec.
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..4]; // borrows elements 2,3,4
println!("{:?}", slice); // [2,3,4]A slice is always a fat pointer - two words on the stack: a pointer to the first element, and the length. No ownership, no allocation.
Conclusion
Borrowing is Rust's answer to a hard problem: how do you share data without copying it, and without the chaos of unchecked aliasing?
The rules are simple to state but deep in consequence:
References let you read or modify data without owning it
Immutable references can coexist freely; mutable references demand exclusivity
The borrow checker enforces these rules at compile time — no runtime cost, no garbage collector needed
Once these rules click, &str, slices, and the broader Rust type system start to feel less like restrictions and more like a contract that makes your code provably safe. Ownership tells you who is responsible. Borrowing tells you who is allowed to look or touch and for how long.
In the next post, we'll move into Structs, Enums and Pattern Matching.
Enjoyed this post?
2 reactions