Using References and Owned Values in Rust

Best Practices on using references and owned values in rust


One of the most important concepts in rust is the concept of ownership of memory and borrowing access to it in different parts of your program. Any data that exists in memory is owned by a variable. To access this data, you typically use the variable that owns it. Alternatively, you can borrow the value using references.

References(&T) are data structures that act as memory addresses, providing information on where to find some owned data stored in memory. They act like pointers but with safety guarantees, allowing access to a value without copying or taking ownership of it. This is critical for optimizing memory usage by preventing unnecessary copying of data in memory.

References, similar to variables are immutable by default and are denoted using the & operator. There are two types of references: immutable reference (&T) , which allow for borrowing without modifying the data and mutable reference (&mut T) which allow the borrowed data to be modified. In order for the compiler to prevent data races and ensure thread safety, only one mutable reference to a particular piece of data is allowed at a time.

In the example below, y is an immutable reference to the value x, while z is a mutable reference to x.

let x = 10;
let y = &x; // y is an immutable reference to x
let z = &mut x; // z is a mutable reference to x
 
println("y equates to {}", y);
println("z equates to {}", z);
 

In larger codebases where references &T may shared and operated on in different parts of the codebase, it's important to distinction between the reference(pointer) and the data it points to. To ensure this distinction, the * operator is used to dereference the reference, which allows access to the data it points to. This operator comes in handy when performing operations on the underlying data or reading the actual value. For example, you can create a copy of the data by using let copy_of_x: i32 = *x_ref;, where x_ref is a reference to an i32 value. In this example our i32 integer is a primitive type, and thus gets stored on the stack.

references-gif

Choosing between owned values and references depends on how much control or access your function needs over the data. An owned value is where the function or struct takes full control over the data i.e. it is responsible for managing it’s lifecycle, freeing up memory when its no longer needed. The best practice is to take as little ownership of the values as necessary.

You would want to use references unless the parameter will be invalid after the function call. An example is when converting types or finalizing a process like saving data to a database.

use std::fs::File;
use std::io::{self, Write};
 
fn save_to_file(file_name: String, data: String) -> io::Result<()> {
    let mut file = File::create(file_name)?;
    file.write_all(data.as_bytes())
}
 
let file_name = String::from("output.txt");
let data = String::from("This is some data to be saved.");
if let Err(e) = save_to_file(file_name, data) {
    println!("Failed to save data: {}", e);
}
 

In the above example, save_to_file takes ownership of both the file name and the data to be saved. Here, the function needs to complete the process of saving the data, after which the original values are no longer required.

For structs/enums, use owned values if the struct produces new data, like a generated report or computed result. Structs that work with data provided by the caller should generally use references. Exceptions include copy types like integers, which can always be owned unless a mutable reference or similar is needed.

Understanding when to use references versus owned values is crucial for efficient and safe memory management in Rust. By using references in functions where parameters are still needed afterward and owned values when data is produced or fully consumed, you can write more robust and clear code. Additionally, recognizing exceptions for copy types ensures you apply these principles appropriately, leading to optimal resource handling in your Rust programs.