In this lesson, you will learn how to write a single piece of logic that works seamlessly with both shared (&T) and exclusive (&mut T) references.
The Motivation for Abstraction
In Rust, we often find ourselves writing two versions of the same code: one for reading data and one for modifying it. This duplication happens because &T and &mut T are technically different types. While they share the same underlying data, the compiler enforces strict rules about aliasing that prevent us from treating them as the same thing.
To abstract over mutability, we use traits. By defining a trait that captures the "get data" operation, we can implement it for both reference types, allowing our functions to remain generic.
Implementing the Pattern
The key is to leverage the fact that &mut T can coerce into &T. However, when we need to be generic, we can use a trait to unify the access patterns. Consider this simple example where we want to access an internal value regardless of how we hold the container:
trait DataAccessor {
type Output;
fn get_data(self) -> Self::Output;
}
impl<'a, T> DataAccessor for &'a T {
type Output = &'a T;
fn get_data(self) -> Self::Output { self }
}
impl<'a, T> DataAccessor for &'a mut T {
type Output = &'a mut T;
fn get_data(self) -> Self::Output { self }
}
fn process_data<D: DataAccessor>(container: D) {
let _data = container.get_data();
// Logic here works for both shared and mutable access
}
Why This Matters
By abstracting over mutability, you prevent code bloat. Instead of writing process_shared and process_mutable, you write one process_data function. The compiler uses the trait implementation to resolve the correct access level at compile time, ensuring that the safety guarantees of Rust—specifically the rules against having multiple mutable references—are still strictly upheld.
You are effectively telling the compiler: "I don't care if this is a read-only view or a write-enabled view, as long as the type provides the data, my logic remains the same."
Key Takeaway: By using traits to normalize access to data, you can write generic code that respects Rust’s ownership and mutability rules without duplicating your business logic.