In the previous article, we learned the most important idea in Rust:
Ownership.
We learned that a value can have one owner, ownership can move, and values are automatically cleaned up when their owner goes out of scope.
But ownership alone would make Rust unnecessarily difficult to use.
Imagine you have a String and want to let a function read it without taking ownership.
You don't want this:
fn print_name(name: String) {
println!("{name}");
}
because passing the String moves ownership.
Instead, Rust gives us borrowing.
fn print_name(name: &String) {
println!("{name}");
}
Now the function can use the value without owning it.
This article is where the borrow checker starts to become understandable.
We'll learn:
&&mut*String vs &strBorrowing means:
Using a value without taking ownership of it.
Suppose:
fn main() {
let name = String::from("Rust");
print_name(&name);
println!("{name}");
}
fn print_name(name: &String) {
println!("{name}");
}
The important part is:
print_name(&name);
The & creates a reference.
The function receives a reference instead of receiving ownership of the String.
Conceptually:
main
│
│ owns
▼
String
│
│ borrowed
▼
print_name()
After print_name() finishes, the original owner still owns the String.
A reference is a way to refer to a value without owning it.
Example:
let name = String::from("Rust");
let reference = &name;
Now:
name
│
▼
String "Rust"
reference
│
└──────→ borrows name
The reference doesn't own the String.
& OperatorThe & symbol creates a reference.
let number = 100;
let reference = &number;
You can think of:
&number
as:
"Give me a reference to
number."
You can generally use a reference just like the value it refers to.
fn main() {
let name = String::from("Rust");
let reference = &name;
println!("{reference}");
}
Rust automatically handles the appropriate dereferencing in many normal expressions.
This is called deref coercion/autoderef behavior, which we'll explore more later.
Compare the two approaches.
fn consume(name: String) {
println!("{name}");
}
Calling:
consume(name);
moves ownership.
fn read(name: &String) {
println!("{name}");
}
Calling:
read(&name);
borrows the value.
The difference:
Ownership:
main ─────→ function
ownership moves
Borrowing:
main ─────→ value
↑
│
function borrows
The simplest reference is an immutable reference.
let name = String::from("Rust");
let reference = &name;
You can read through it:
println!("{reference}");
But you cannot modify the original value through an immutable reference.
Rust allows multiple immutable references at the same time.
fn main() {
let name = String::from("Rust");
let first = &name;
let second = &name;
let third = &name;
println!("{first}");
println!("{second}");
println!("{third}");
}
This is perfectly valid.
Why?
Because nobody is modifying the value.
Conceptually:
┌──→ first
│
String ←─────┼──→ second
│
└──→ third
Everyone is reading.
Nobody is changing.
This is safe.
What if you want to modify a value through a reference?
Use:
&mut
Example:
fn add_text(text: &mut String) {
text.push_str(" Programming");
}
fn main() {
let mut message = String::from("Rust");
add_text(&mut message);
println!("{message}");
}
Output:
Rust Programming
There are two important keywords here:
let mut message
and:
&mut message
Both matter.
mut?This won't work:
let message = String::from("Rust");
add_text(&mut message);
because message itself isn't mutable.
You need:
let mut message = String::from("Rust");
Then:
add_text(&mut message);
is allowed.
Think:
mut variable
↓
can be modified
&mut variable
↓
create mutable reference
Rust has an important rule:
At a given time, you can have either one mutable reference or any number of immutable references, but not both.
In simplified form:
Many readers
OR
One writer
Not:
Many readers + writer
This prevents data races.
This isn't allowed:
let mut value = 10;
let first = &mut value;
let second = &mut value;
Why?
There would be two active mutable references to the same value.
Rust prevents this situation.
Imagine two pieces of code simultaneously modifying the same memory:
Thread A ──→ value
↑
Thread B ─────┘
Both could attempt to modify it at the same time.
The result could become unpredictable.
Rust's borrowing rules prevent many such problems at compile time.
This is also problematic while the references are simultaneously active:
let mut value = 10;
let read = &value;
let write = &mut value;
Rust rejects this if the immutable borrow is still in use.
The problem is:
read → reading
write → modifying
You don't want someone changing a value while another active reference relies on it remaining unchanged.
Remember this:
┌── Multiple immutable references
│
Borrowing ──────────┤
│
└── OR one mutable reference
Never:
Multiple immutable references
+
Mutable reference
when their active lifetimes overlap.
Consider:
fn main() {
let mut value = 10;
{
let reference = &mut value;
*reference += 10;
}
println!("{value}");
}
The mutable reference exists only inside the inner block.
When the block ends:
reference disappears
↓
value available again
Output:
20
Modern Rust has a very useful feature called Non-Lexical Lifetimes, commonly abbreviated NLL.
Consider:
fn main() {
let mut value = 10;
let reference = &value;
println!("{reference}");
let mutable_reference = &mut value;
*mutable_reference += 10;
println!("{value}");
}
This can work because the immutable reference is no longer used after:
println!("{reference}");
The compiler can determine that its borrow has ended before the mutable borrow begins.
Conceptually:
reference created
↓
reference used
↓
reference no longer needed
↓
mutable borrow allowed
This makes Rust's borrowing system more flexible than a simplistic "block ends, borrow ends" model.
Now let's look at:
*
The * operator can be used to dereference a reference.
Example:
fn main() {
let value = 10;
let reference = &value;
println!("{}", *reference);
}
Here:
reference
is a reference to the integer.
And:
*reference
accesses the value being referenced.
Think of:
value = 10
reference
│
▼
value
10
Then:
*reference
means:
Follow the reference and access the underlying value.
This becomes particularly useful when modifying values.
fn increase(value: &mut i32) {
*value += 1;
}
fn main() {
let mut number = 10;
increase(&mut number);
println!("{number}");
}
Output:
11
The expression:
*value += 1;
means:
Access the value being referenced and modify it.
*?You might wonder why this works:
fn print_name(name: &String) {
println!("{name}");
}
instead of:
println!("{}", *name);
Rust provides automatic dereferencing in many contexts.
For example, method calls such as:
name.len()
can work even though name is a reference.
This is one of the conveniences provided by Rust's type system.
This is extremely important.
Consider:
let name = String::from("Rust");
let reference = &name;
reference does not own the String.
Therefore, when the original owner goes out of scope:
name
↓
owns String
reference
↓
borrows String
the reference cannot keep the String alive by itself.
This prevents references from becoming hidden owners.
A dangling reference would point to memory that is no longer valid.
Imagine:
reference
↓
freed memory
That would be dangerous.
Rust's compiler prevents safe Rust code from creating ordinary dangling references.
For example, this doesn't compile:
fn create_reference() -> &String {
let value = String::from("Rust");
&value
}
Why?
Because value will be destroyed when the function ends.
Returning a reference to it would leave a reference pointing to invalid memory.
Rust catches this at compile time.
Languages with manual memory management can allow dangerous situations if the programmer isn't careful.
Rust instead asks:
Does this reference remain valid for as long as it is used?
If the answer is no, compilation fails.
This is one of the biggest advantages of Rust's ownership and borrowing model.
StringLet's create a reusable function:
fn print_length(text: &String) {
println!("Length: {}", text.len());
}
fn main() {
let text = String::from("Rust Programming");
print_length(&text);
println!("Still available: {text}");
}
Output:
Length: 16
Still available: Rust Programming
The function borrowed the string.
&str for Read-Only String ParametersIn idiomatic Rust, you will often see:
fn print_length(text: &str) {
println!("Length: {}", text.len());
}
instead of:
fn print_length(text: &String) {
}
Why?
Because &str is more flexible.
You can pass:
print_length("Hello");
and:
let message = String::from("Hello");
print_length(&message);
The function doesn't need to care whether the caller has a String or a string literal.
String vs &strThis distinction is extremely important.
StringAn owned, growable string.
let message = String::from("Hello");
&strA borrowed string slice.
let message = "Hello";
The second value is a string literal with type &'static str.
Conceptually:
String
↓
owns string data
&str
↓
borrows string data
We'll go deeper into this when we study slices and lifetimes.
A slice is a reference to part of a collection.
Example:
fn main() {
let text = String::from("Hello Rust");
let part = &text[0..5];
println!("{part}");
}
Output:
Hello
Here:
&text[0..5]
creates a string slice.
Suppose:
Hello Rust
0123456789
Then:
&text[0..5]
represents:
Hello
It doesn't create a completely independent String.
It references part of the existing string.
Suppose you need only part of a large string.
Instead of copying:
Entire String
↓
copy "some part"
you can borrow a slice:
Entire String
↑
│
&str
This can be efficient because the slice doesn't need to own another copy of the text.
Rust strings are UTF-8 encoded.
Therefore:
let text = String::from("Hello");
let part = &text[0..5];
works because these indices fall on valid UTF-8 character boundaries.
But arbitrary byte ranges can cause a runtime panic if they split a UTF-8 character.
For example, don't assume one character always equals one byte.
This is an important difference between Rust strings and arrays of simple bytes.
Slices aren't limited to strings.
Consider:
fn main() {
let numbers = [10, 20, 30, 40, 50];
let slice = &numbers[1..4];
println!("{slice:?}");
}
Output:
[20, 30, 40]
The slice borrows part of the array.
For an array:
let numbers = [10, 20, 30];
a slice looks like:
&numbers[..]
Its type is:
&[i32]
You can think of:
&[T]
as:
A borrowed view into a sequence of
Tvalues.
Instead of requiring an array of one exact size:
fn calculate(numbers: &[i32]) {
// ...
}
the function can accept slices of different lengths.
Example:
fn print_numbers(numbers: &[i32]) {
for number in numbers {
println!("{number}");
}
}
fn main() {
let numbers = [10, 20, 30, 40];
print_numbers(&numbers);
}
This is a very common Rust pattern.
&[T] Is Extremely UsefulYou can use:
fn sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for number in numbers {
total += number;
}
total
}
Then:
fn main() {
let numbers = [10, 20, 30, 40];
println!("{}", sum(&numbers));
}
The function doesn't need to own the array.
You can also borrow a slice mutably.
fn double(numbers: &mut [i32]) {
for number in numbers {
*number *= 2;
}
}
fn main() {
let mut numbers = [1, 2, 3, 4];
double(&mut numbers);
println!("{numbers:?}");
}
Output:
[2, 4, 6, 8]
This combines:
forA good function often takes the least ownership it needs.
Suppose a function only needs to read data.
Instead of:
fn calculate(data: Vec<i32>) {
}
consider:
fn calculate(data: &[i32]) {
}
The second function doesn't need ownership of the vector.
This makes the API more flexible.
VecSuppose:
let numbers = vec![10, 20, 30];
You can pass:
calculate(&numbers);
Rust can use the vector as a slice where appropriate.
For example:
fn sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
fn main() {
let numbers = vec![10, 20, 30];
let result = sum(&numbers);
println!("{result}");
}
Output:
60
Now let's talk about the famous Rust borrow checker.
The borrow checker is part of the Rust compiler that checks whether references follow Rust's ownership and borrowing rules.
It asks questions such as:
If your program violates the rules, compilation fails.
Imagine:
Your Rust Code
↓
Compiler
↓
Ownership check
↓
Borrow check
↓
Type check
↓
Machine code
The compiler prevents many memory bugs before your program runs.
This is why Rust can be strict.
The compiler is essentially saying:
"Prove to me that this memory usage is safe."
Consider:
let mut value = 10;
let first = &value;
let second = &mut value;
println!("{first}");
This is problematic because the immutable reference is used while a mutable borrow exists.
Rust rejects it.
One solution is to finish using the immutable reference first:
let mut value = 10;
let first = &value;
println!("{first}");
let second = &mut value;
*second += 1;
Now the immutable borrow is no longer needed before the mutable borrow begins.
This is valid:
let mut value = 10;
{
let reference = &mut value;
*reference += 5;
}
println!("{value}");
The mutable reference's scope ends before value is used again.
Keep this nearby while learning Rust.
You can have:
Any number of immutable references
at the same time.
You can have:
One mutable reference
at a time.
You cannot have active immutable and mutable references to the same value simultaneously.
References must never outlive the value they refer to.
These rules are the foundation of Rust's borrowing system.
Think of a value like a document.
Multiple people can read it:
Document
↑ ↑ ↑
A B C
One person gets editing access:
Document
↑
Editor
You don't want:
Document
↑ ↑
Reader Editor
at the same time if the reader relies on the document staying unchanged.
Rust enforces this concept at compile time.
Let's use borrowing in a realistic example.
struct Account {
balance: f64,
}
fn show_balance(account: &Account) {
println!("Balance: ₹{}", account.balance);
}
fn deposit(account: &mut Account, amount: f64) {
account.balance += amount;
}
fn main() {
let mut account = Account {
balance: 1000.0,
};
show_balance(&account);
deposit(&mut account, 500.0);
show_balance(&account);
}
Output:
Balance: ₹1000
Balance: ₹1500
Notice the design:
show_balance()
↓
immutable borrow
deposit()
↓
mutable borrow
Each function gets exactly the access it needs.
fn contains(numbers: &[i32], target: i32) -> bool {
for number in numbers {
if *number == target {
return true;
}
}
false
}
fn main() {
let numbers = [10, 20, 30, 40];
println!("{}", contains(&numbers, 30));
}
Output:
true
The function doesn't take ownership of the array.
It simply borrows a slice.
fn increase_all(numbers: &mut [i32]) {
for number in numbers {
*number += 10;
}
}
fn main() {
let mut numbers = [1, 2, 3];
increase_all(&mut numbers);
println!("{numbers:?}");
}
Output:
[11, 12, 13]
Suppose you have a large vector:
let numbers = vec![/* millions of values */];
You don't want every function call to copy the entire vector.
Instead:
fn process(numbers: &[i32]) {
}
allows the function to operate on the existing data.
Conceptually:
Large collection
↓
Borrowed slice
↓
Function
No unnecessary ownership transfer or data duplication.
At this point, you can see how Rust combines:
Ownership
+
Borrowing
+
References
+
Compile-time checking
to achieve memory safety without requiring a garbage collector.
This is one of the reasons Rust is widely used for:
&Suppose:
fn print_name(name: &String) {
println!("{name}");
}
You need:
print_name(&name);
not:
print_name(name);
because the latter attempts to pass ownership.
mutThis:
let value = 10;
cannot be mutably borrowed.
Use:
let mut value = 10;
then:
let reference = &mut value;
Avoid:
let read = &value;
let write = &mut value;
when both references are simultaneously active.
This is invalid:
fn create() -> &String {
let value = String::from("Rust");
&value
}
The local value disappears when the function returns.
String When &str Is BetterFor read-only string parameters, prefer:
fn process(text: &str) {
}
when appropriate.
Create:
fn print_number(number: &i32)
and call it without transferring ownership.
Create:
fn increment(number: &mut i32)
that increases a number by 1.
Create:
fn add_exclamation(text: &mut String)
that turns:
Hello
into:
Hello!
Given:
let numbers = [10, 20, 30, 40, 50];
create a slice containing:
20, 30, 40
Write:
fn sum(numbers: &[i32]) -> i32
that returns the sum.
Write:
fn maximum(numbers: &[i32]) -> i32
that returns the largest value.
Experiment with:
let mut value = 10;
let a = &value;
let b = &value;
println!("{a}");
println!("{b}");
Then try:
let mut value = 10;
let a = &value;
let b = &mut value;
println!("{a}");
Observe what the compiler tells you.
You've now reached an important point in the Rust learning path.
The progression looks like this:
Variables
↓
Data Types
↓
Control Flow
↓
Functions
↓
Ownership
↓
Borrowing
↓
References
↓
Lifetimes
And lifetimes are the next major piece.
Pixels to Perfection Design that Impresses