Posted on :: Tags: :: Source Code

It is common to encounter a situation where you may require to return a different type either as a immutable or mutable reference which can be a field of a struct or a completely new type composed out of a given type for some operation

You may also have come across such type expression impl AsRef<str> or where T: Borrow<[u8]> or something like impl Deref<Target = [u8]> these kinds of expression are common to come across which can be tricky sometimes to understand and draw a distinction among it.

It's quite common to come across these traits in standard library AsRef<T> / AsMut<T>, Borrow / BorrowMut and Deref / DerefMut.

Unless you have good background with rust, The first thought which may come to mind after looking at these traits is they are naturally made for producing a subtype or newtype from a given type when called! well this is unfortunately not true. If this was true then standard library won't be having these many traits!

Similarity

But before we start to look over there characteristics and differences, Let's cover there similarities first, keep this in mind following similarities are not strongly same but similar or close to being similar depending upon situation, and in some simple situations could be used interchangibely, let's check them out !

  • Simplification of borrow: All the above traits are used to simplify access to data. They allow for different ways of referring to an object, typically without taking ownership of real one.
    • with AsRef & AsMut
    impl AsRef<Car> for FourWheeler {
        ...
    } 
    
    • with Borrow trait
    impl Borrow<Car> for FourWheeler {
        ...
    }
    
    • with Deref and DerefMut
    impl Deref for FourWheeler {
        type Target = Car; 
        ...
    }
    
  • Conversion support to Non-sized types: Each trait definition allows conversion to non-sized types, as shown in the definition of each trait bellow, you will notice this.
    • With AsRef & AsMut
    pub trait AsRef<T>
     where
         T: ?Sized, // Non-sized type
     {
         // Required method
         fn as_ref(&self) -> &T;
     }
    
    pub trait AsMut<T>
    where
        T: ?Sized, // Non-sized type
    {
        // Required method
        fn as_mut(&mut self) -> &mut T;
    }
    
    • With Borrow & BorrowMut trait
    pub trait Borrow<Borrowed>
     where
         Borrowed: ?Sized, // Non-sized type
     {
         // Required method
         fn borrow(&self) -> &Borrowed;
     }
    
    pub trait BorrowMut<Borrowed>: Borrow<Borrowed>
    where
        Borrowed: ?Sized, // Non-sized type
    {
        // Required method
        fn borrow_mut(&mut self) -> &mut Borrowed;
    }
    
    • With Deref & DerefMut
     pub trait Deref {
         type Target: ?Sized; // Non-sized type
    
         // Required method
         fn deref(&self) -> &Self::Target;
     }
    
    pub trait DerefMut: Deref {
        // Required method
        fn deref_mut(&mut self) -> &mut Self::Target;
    }
    
  • Used in Generic Programming: All three traits are highly useful in generic programming, where you want to write functions or structs that can operate on references to data rather than the data itself.
    • With AsRef & AsMut you may have the impelmentation for any given type such following
    //case 1 
    impl AsRef<str> for T { 
        ...
    } 
    //case 2
    fn compute<T>(s: impl AsRef<T>) where T: ToString ..
    
    //case 3 
    struct Car<I> {
        tires: Box<dyn AsRef<I>>
        ...
    }
    
    • with Borrow & BorrowMut traits you can still acheive same exact thing as above

    • with Deref & DerefMut things get bit special due to associated type in it's signature definition

    //case 1
    impl<Y> Deref for SomeType 
     where Y: std::fmt::Debug {
        type Target = Y;
        fn deref(&self) -> &Self::Target {
            ...
        } 
    }
    //case 2
    fn accept_deref<U>(s: impl Deref<Target=U>) where U: ToString ...
    
    //case 3
    struct Truck<I> {
        tires: Box<dyn Deref<Target = I>>
        ...
    }
    

AsRef vs Borrow vs Deref in Rust

AsRef & AsMut trait

AsRef is a trait for performing cheap reference-to-reference conversions. This includes read and write borrows! It's most commonly used for functions that can accept multiple types of concrete references. It can also be used to borrow non-sized types!

Let's check out a simple example first. In the example below, we are trying to borrow a type as a str, but we are allowed to pass each type as a reference as illustrated below.

Common Use Cases

fn process_string<T: AsRef<str>>(s: T) {
    let s_ref: &str = s.as_ref();
    // Process the string slice
}

// Can be called with different types
let string = String::from("hello");
let str_literal = "world";

process_string(&string);  // works
process_string(str_literal);  // also works

In the above example, we have seen how we can pass around all those types that implement AsRef. These types are now qualified to be passed as owned types, and later, using the as_ref() method, we can call that type to be represented as the intended type as a read-only reference.

Since there exist implementations for &str and String types as shown below:

impl AsRef<str> for String {
    #[inline]
    fn as_ref(&self) -> &str {
        self
    }
}

and for str type

impl AsRef<str> for str {
    #[inline(always)]
    fn as_ref(&self) -> &str {
        self
    }
}

Interestingly, if you observe carefully here, you will find that AsRef doesn't write the following type signature for the generic position AsRef<&str> instead, it writes AsRef<str>. If you are wondering why this is, check out these two posts respectively: Rust nomicon reference and Niko Matsakis, to understand the difference. Now, if you look at the trait function fn as_ref(&self) -> &str, it returns a borrowed version. Frankly, the explanation behind how str is borrowed into &str deserves its own post, but to give a short answer: inside the method, self is already of type &str because we're implementing a method that takes &self, so we can just return it directly since it matches the return type.

Now let's move our attention towards AsMut, which lets us borrow a type mutably. If you visit the standard library, you will find there exist implementations for AsMut for str and String types. These implementations are as follows:

For the string type:

impl AsMut<str> for String {
    #[inline]
    fn as_mut(&mut self) -> &mut str {
        self
    }
}

for str type

impl AsMut<str> for str {
    #[inline(always)]
    fn as_mut(&mut self) -> &mut str {
        self
    }
}

Interestingly, this time we borrow both of our types as mutable references &mut str.

By now, I am hoping this has brought some clarity regarding how the AsRef and AsMut traits can be used. The real power of these traits lies in their flexibility regarding how they can be used.

Pay attention to how AsRef and AsMut are totally independent of each other, meaning they don't have any associated type dependency or super trait requirement, unlike what we previously saw for the Borrow, BorrowMut, Deref, and DerefMut traits. Hence, we are given complete flexibility in how one concrete type can be cheaply converted into another type. Let's understand this explanation through an example.

Let's think of a situation where you wish to convert a book type Book into some primitive representation such as &str as an immutable borrow only, and also as Vec<String> as a mutable reference.

struct Book {
    title: String,
    pages: Vec<String>,
}

writing a simple AsRef conversion is possible, given if it makes sense to use that type temporarly

impl AsRef<str> for Book {
    fn as_ref(&self) -> &str {
        &self.title  // We can reference the title as a &str
    }
}

in a simple manner it is possible to write a AsMut<Vec<String>> conversion as well

// Implement AsMut<Vec<String>> for Book to modify pages
impl AsMut<Vec<String>> for Book {
    fn as_mut(&mut self) -> &mut Vec<String> {
        &mut self.pages  // We can get a mutable reference to pages
    }
}

following from previous, it is possible to write a AsMut<[str]> conversion as well

impl AsMut<[String]> for Book {
    fn as_mut(&mut self) -> &mut [String] {
        &mut self.pages  // We can get a mutable reference to pages
    }
}

Points to note while using AsRef & AsMut

  • Usage of these traits is best suited when you have to derive a type that is mostly a field of a struct, and the conversion doesn't involve any expensive computation.
  • These traits give you flexibility in usage, meaning you may have a single AsRef for some type, whereas you may have several AsMut for distinct types, and they don't depend on each other in any way!
  • It's better to avoid writing these traits for Non-Sized types such as &dyn .. or &mut dyn...

Borrow & BorrowMut

The Borrow trait is similar to the AsRef trait in that it supports borrowing of type A into type B, but the catch is that it lets you borrow rather than convert something. Hence, the point of comparison shall be between how to distinguish between borrowing and conversion.

Conversion of any type to another type is straightforward since it doesn't enforce us to carry any further semantic information, while borrowing shall satisfy such norms.

From the above information, Borrow is similar to AsRef but with an additional contract: the borrowed form must hash, compare, and order the same as the owned form.

Let's check out its definition first.

pub trait Borrow<Borrowed: ?Sized> {
    fn borrow(&self) -> &Borrowed;
}

In above definition we have declared out borrow to be a generic just like AsRef which can be Non-sized type and borrows immutably, but we would also need to borrow mutably, let's checkout the definition for BorrowMut trait in detail

pub trait BorrowMut<Borrowed>: Borrow<Borrowed>
where
    Borrowed: ?Sized,
{
    // Required method
    fn borrow_mut(&mut self) -> &mut Borrowed;
}

For the BorrowMut definition, as we can clearly observe, it depends on the Borrow<Borrowed> implementation, which is generic over Borrowed. Therefore, types that are going to implement BorrowMut<Borrowed> must first implement the immutable version, i.e., Borrow<Borrowed>. This enforces a type to be borrowable in both forms, i.e., &T and &mut T, unlike the AsRef and AsMut traits, which can be totally independent of each other and have no correlation between them.

Let's check out the std::collections::HashMap definition regarding the Borrow trait.

Use Cases

// struct definition
pub struct HashMap<K, V, S = RandomState>

// method get definition 
pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V>
where
    K: Borrow<Q>, // Observe how Borrow trait is used 
    Q: Hash + Eq,
{
    ...
}

The type definition of HashMap<K, V> is generic over three parameters, but we will only consider K and V, which are for key and value respectively, for simplicity. Now, if we look at the method get, it declares another generic Q which can be borrowed from k, and Q must implement Hash + Eq traits as bounds, since it requires that keys can be distinguishable.

This requirement is present for multiple methods of collection types within the standard library.

Note: Borrow & BorrowMut themselves do not enforce the semantic requirements through their definitions. It is up to the defining method or struct to declare how semantics are going to be enforced via them.

Borrow traits can also be useful for types where we want to retrieve an internal type rather than a composed one, such as for Vec<T>, which can be borrowed as a mutable slice &mut [T] and modified directly rather than converting it first.

Points to note

  1. Stricter semantic requirements, though not required by default.
  2. Essentially used for collection types such as HashMap and BtreeMap.
  3. Useful when you need to work on a subtype of some composed type which might require modification.
  4. There may exist several definitions for a given type to be borrowed in different types.

Deref & DerefMut

The trait Deref is one of my personal favorites and quite a powerful one among all. Deref is used for dereferencing operations using the * operator and for automatic dereferencing to a defined type. Unlike traits Borrow & AsRef, Deref carries an associated type as part of its definition and can only be applied once. Let's check out its definition.

pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

In the above definition it returns a associated type as immutable reference, now let's take a look for DerefMut

pub trait DerefMut: Deref {
    // Required method
    fn deref_mut(&mut self) -> &mut Self::Target;
}

As you can clearly see, just like BorrowMut, DerefMut also depends on Deref to be pre-implemented for a type. Interestingly, the return type belongs to the super trait Deref, which can be borrowed mutably. This has two major advantages:

  • The output type for both borrows can't be changed since it is based on the associated type.
  • It prevents the implementation from existing for more than one type.

To find out more about how associated types are different from generics, I recommend you read this blog here.

Once a type implements deref traits, it can now automatically call the methods of dereferenced types using the dot . operator.

Let's look at a simple example here.

Common Use Cases

use std::ops::Deref;

struct MyBox(Vec<u8>);

impl MyBox {
    pub fn my_box_len(&self) -> usize {
        self.0.len()
    }
}

impl Deref for MyBox {
    type Target = Vec<u8>;
    
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

let mut x = MyBox(vec![4]);

// valid
let len_one = x.my_box_len(); 

// also valid, we are directly Vec::len() method since it only takes &self
let len = x.len() 

Incase we require some mutable borrows

impl DerefMut for MyBox {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

now we can call any mutable method over Vec<u8>

let _val = x.remove(0); // x -> .remove()

Those types which implement Deref trait are automatically qualified to use * operator for explicit dereferencing,

let _val = (*x).remove(0);  // x -> Vec<u8> -> .remove()

Now you might wonder why both expressions are making valid calls to remove. Once a type implements the Deref trait, it will attempt to call the respective method through implicit dereferencing if required and will make multiple attempts if the subtype also implements Deref.

Here is a straightforward example:

let _val: Option<&mut u8> = x.last_mut(); // x -> Vec<u8> -> &mut [u8]

If you visit Vec<u8>, you will find that it implements Deref<Target=[u8]>, hence all the methods on &mut [u8] are directly accessible to us.

This is very useful, but if not used cautiously, it could be the cause of bugs sometimes, since you may call methods on some other type other than the desired type! Luckily, in such cases, only methods that are first observed through implicit dereference are called.

Deref Coercion

One of the most powerful features of Deref is onsite coercion if a type implements deref traits. This could be helpful where we wish to avoid explicit casting/conversions:

fn takes_str(s: &str) {
    println!("{}", s);
}

let string = String::from("hello");
takes_str(&string);  // Works because String: Deref<Target=str>
takes_str("string2");  // also works because it is a concrete type

But hold on a second, why would we need such an onsite coercion feature if we already have AsRef and Borrow traits to take care of the job? Well, I'm glad you asked!

If you observe the above definition carefully, you will notice it is a concrete type, which is &str. However, for AsRef and Borrow traits, you will need to have a type that must implement them anyhow, therefore limiting the application and optimizations available.

Deref coercion is a powerful language feature if used correctly, you can read more about it here

Now that we have built some understanding around it, let's quickly compare them through a simple table.

Comparison Table

TraitPrimary PurposeKey Characteristics
AsRefCheap reference conversions- No guarantees about equality or hashing
- Useful for generic functions accepting references
BorrowBorrowed data equivalence- Guarantees about equality and hashing
- Essential for collections types like HashMap/BtreeMap
DerefSmart pointer functionality- Enables * operator
- Enables deref coercion
- Affects method resolution

Common Gotchas

  1. Deref Coercion Costs
// Multiple deref coercions can have performance implications
let s = Box::new(String::from("hello"));
takes_str(&s);  // Box -> String -> str, two dereferences
  1. Borrow vs AsRef Choice
// Wrong choice for HashMap key type
fn bad_design<T: AsRef<str>>(map: &HashMap<T, u32>) {} // Won't work well

// Correct choice
fn good_design<T: Borrow<str>>(map: &HashMap<T, u32>) {}
  1. Deref for Non-Pointer Types
// Generally avoid implementing Deref for non-pointer-like types
// Could be confusing for other developers
struct Seconds(u64);
// Avoid this:
impl Deref for Seconds {
    type Target = u64;
    fn deref(&self) -> &u64 { &self.0 }
}

Summary

This overview covers the main differences and use cases for these three important Rust traits. The key is understanding their specific purposes and contracts to use them effectively in your code.