Create your own
Lesson illustration

Borrowing Data: References in Rust

Hello! Welcome to the next lesson in your journey to becoming a Solana developer.

In our previous lesson, we demystified Rust's ownership system. You learned that by default, Rust uses move semantics for heap-allocated data, which means ownership is transferred when a value is assigned or passed to a function. This ensures memory safety but can be cumbersome. We ended on a key question: how can we let a function use some data without taking ownership of it?

Today, we will answer that question by exploring references and borrowing. This is the other half of the ownership story. Borrowing allows you to create references that "point" to data without taking ownership, enabling different parts of your program to access data efficiently and safely. Mastering this is essential for writing performant Solana programs where you frequently need to pass account data between functions without expensive copying or ownership transfers.

By the end of this lesson, you will be able to use references and borrowing to access data without taking ownership.

The Problem with Ownership Transfer

Let's quickly recall the issue from last time. Imagine a function that needs to calculate the length of a string. Using our knowledge of ownership, we might write something like this:

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length) // Return the string and its length
}

fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    // s1 is no longer valid here
    println!("The length of '{}' is {}.", s2, len);
}

This works, but it's awkward. We have to pass ownership of the String to the function, and then the function has to return ownership back to us along with the result. This pattern of passing ownership back and forth would get very tedious in a large program.

References: Borrowing Without Owning

Rust provides a much cleaner solution through references. A reference is like a pointer in that it’s an address we can follow to access data owned by another variable. However, unlike pointers in languages like C/C++, a Rust reference is guaranteed by the compiler to point to a valid value for its entire lifetime.

The action of creating a reference is called borrowing.

Let's start with the official Rust documentation, which explains this concept perfectly.

References and Borrowing - The Rust Programming ...

Please read the introduction to 'References and Borrowing' from The Rust Programming Language book. It demonstrates how to rewrite our calculate_length example using references to avoid transferring ownership.

Read the first part of the chapter, up to (but not including) the 'Mutable References' subsection. Pay close attention to the & syntax for creating a reference and for specifying a reference type in a function signature.

As you saw, by changing the function signature to take &String instead of String, and calling it with &s1, we can let the function borrow the string. The main function remains the owner, and s1 is still valid after the call.

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // s goes out of scope, but because it does not own the data, nothing is dropped.

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // We pass a reference to s1
    println!("The length of '{}' is {}.", s1, len); // s1 is still valid!
}

This is the fundamental idea of borrowing: providing read-only access to a value without taking ownership.

The Three Rules of Borrowing

While borrowing solves the ownership transfer problem, it comes with its own set of rules that the compiler (specifically, the "borrow checker") enforces to maintain memory safety. These rules are the key to preventing a whole class of bugs, most notably data races.

A data race occurs when:

  1. Two or more pointers/references access the same data concurrently.
  2. At least one of them is writing to the data.
  3. There's no mechanism to synchronize access.

Rust prevents data races at compile time by enforcing a few simple rules for borrowing. The following video provides an excellent, structured overview of these rules.

Learn these 3 RULES to master Borrowing! | Rust Fundamentals Part 6

Watch this video from Green Tea Coding. It breaks down the borrowing system into three easy-to-remember rules that form the core of the concept.

Watch from 01:32 to 12:43. This covers the three fundamental rules of borrowing: immutable borrows, mutable borrows, and reference validity. Pay close attention to the code examples and the compiler errors they trigger.

Let's recap and solidify those rules.

Rule 1: You can have any number of immutable references.

As long as no one is changing the data, it's perfectly safe for multiple parts of your code to have read-only access.

let s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem

println!("r1: {}, r2: {}", r1, r2); // This is fine.

Rule 2: You can have only ONE mutable reference OR any number of immutable references, but not both at the same time.

This is the rule that prevents data races. To modify a borrowed value, you need a mutable reference, created with &mut.

let mut s = String::from("hello"); // `s` must be mutable

let r1 = &mut s;
r1.push_str(", world");

println!("{}", r1);

The compiler enforces that if a mutable reference exists, no other references (mutable or immutable) to that data can exist in the same scope.

This code will fail because we can't have two mutable borrows at once:

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s; // ERROR! Cannot borrow `s` as mutable more than once

//println!("{}, {}", r1, r2);

This code will also fail because you can't mix immutable and mutable borrows. An immutable reference holder doesn't expect the data to suddenly change.

let mut s = String::from("hello");

let r1 = &s; // immutable borrow
let r2 = &mut s; // ERROR! Cannot borrow `s` as mutable because it is also borrowed as immutable

//println!("{}, {}", r1, r2);

The Rust book provides further excellent examples and explains the concept of non-lexical lifetimes, where a reference's scope ends after its last use, which can allow for code like this to compile:

let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); 
// r1 and r2 are no longer used after this point, so their scopes end.

let r3 = &mut s; // This is now allowed.
println!("{}", r3);

Rule 3: References must always be valid.

Rust's compiler ensures you can never have a dangling reference—a reference that points to memory that has been deallocated. This is a common and serious bug in languages with manual memory management.

Consider this function, which attempts to return a reference to a String created inside it:

/* This code does not compile!
fn dangle() -> &String {
    let s = String::from("hello");
    &s // We return a reference to `s`
} // `s` goes out of scope and is dropped. Its memory is freed.
*/

The compiler will reject this code. The variable s is deallocated at the end of the dangle function, so a reference to it would be pointing to invalid memory. The only valid solution is to transfer ownership of the value itself:

fn no_dangle() -> String {
    let s = String::from("hello");
    s // Ownership of `s` is moved out of the function
}

Borrowing in Practice: Function Parameters

The most common use of borrowing is with function parameters. The same rules apply.

Learn these 3 RULES to master Borrowing! | Rust Fundamentals Part 6

Let's return to the Green Tea Coding video to see how these rules apply when passing references to functions.

Watch from 14:45 to 18:49. This demonstrates how to define functions that take immutable (&T) and mutable (&mut T) references as parameters.

In summary:

  • A function that takes &String borrows the value immutably.
  • A function that takes &mut String borrows the value mutably and can change it.

Dereferencing

To access the value that a reference points to directly, you can use the dereference operator *. While Rust's dot operator (.) performs automatic dereferencing for method calls (like s.len()), sometimes you need to be explicit, especially when modifying a value through a mutable reference to a simple type.

let mut x = 10;
let r = &mut x;

// *r = 20; is how you would assign a new value to what r points to.
*r += 1; // We dereference r to add 1 to the value of x

println!("x is now {}", x); // Prints "x is now 11"

You don't need to master all the nuances of dereferencing right now, but it's important to know the * operator exists.

Test your understanding!

The following function add_and_report is intended to add a suffix to a string and also report its new length. However, the code doesn't compile. Can you identify the error based on the borrowing rules and fix it?

fn main() {
    let mut data = String::from("solana");
    add_and_report(&mut data);
}

fn add_and_report(s: &mut String) {
    let old_ref = &s; // Create an immutable reference

    s.push_str("-program"); // Mutate the string

    // This line causes the error
    println!("The string '{}' now has length {}.", old_ref, s.len());
}
Show answer

The code fails because it violates Rule #2: you cannot have a mutable borrow while an immutable borrow is active.

  1. let old_ref = &s; creates an immutable borrow of s.
  2. s.push_str("-program"); attempts to create a mutable borrow of s to modify it. This is not allowed because old_ref is still in scope.
  3. The compiler error occurs because old_ref is used after the mutation attempt (in the println!).

How to fix it: The goal seems to be to print the original string. The easiest way to do this is to get the data you need before the mutation happens.

fn main() {
    let mut data = String::from("solana");
    add_and_report(&mut data);
}

fn add_and_report(s: &mut String) {
    // Clone the original data before mutation if you need it later.
    let original_data = s.clone(); 
    
    s.push_str("-program"); // Mutate the string

    // Now we can use the original and the mutated versions.
    println!("The original string '{}' became '{}' and now has length {}.", original_data, s, s.len());
}

Another, more efficient solution if you only need to show the original string and don't need to hold onto it, is to print it before the mutation:

fn add_and_report(s: &mut String) {
    println!("Original string: {}", s);
    s.push_str("-program"); 
    println!("New string: '{}', New length: {}", s, s.len());
}

This works because the immutable borrow created by println! ends immediately after the macro call, before the mutable borrow for push_str is needed.

Conclusion

Great work! You have now completed the puzzle of Rust's ownership and borrowing system. This is arguably the steepest part of Rust's learning curve, and understanding it puts you in a strong position to write safe and efficient code.

Here are the key takeaways from today's lesson:

  • References (&) let you borrow data without taking ownership.
  • By default, references are immutable. You can create mutable references with &mut.
  • The borrow checker enforces two crucial rules:
    1. You can have either one mutable reference or any number of immutable references within a given scope.
    2. References must always be valid and cannot outlive the data they point to (no dangling references).
  • These rules allow Rust to prevent data races at compile time, a massive benefit for systems programming like you'll be doing on Solana.

Now that you have a solid grasp of how to manage variables, data, and memory, you're ready to start creating your own custom data structures.

In our next lesson, we will cover structs, which allow you to group related data together into your own custom types, and impl blocks, which let you define methods (behaviors) for those types. This will be your first step towards defining the custom data accounts that are central to Solana programs.

Can't find a good explanation? Sign up and we'll make it for you

Sign up