Oliver Jumpertz
hero image

Rust's AsRef Explained

Share this post to:

Rust has some built-in types and traits that many newbies to the language usually don’t come in touch with explicitly for a long time. Especially some of these traits can make a huge difference in making any Rust developer’s code more:

  1. Maintainable
  2. User-friendly
  3. Extensible
  4. Generic

If you don’t follow The Book closely, it can often take forever to even realize that these types and traits exist.

AsRef is one of the built-in traits that play a huge role in the standard library. Outside of that, AsRef is also one of the major traits that any Rust developer should know and embrace more often. This is why this article tries to explain AsRef to you as best as possible and show you why you should begin to use and understand it right now.


Table of Contents

Open Table of Contents

Borrowing

Rust’s ownership model is a pretty clever way to remove the need for any programmer to think about pointers. A block of code either owns a variable or it borrows it. If a code block ends, all variables that are owned by this block get freed. That’s not the case for borrowed variables, though. These remain in memory for as long as any block of code still owns and holds a reference to them.

It’s best to visualize this by looking at some code examples. The first example below shows a very basic case of ownership:

fn main() {
let a = String::from("Hello reader!");
// more code...
// prints a
println!("{a}");
// a now goes out of scope and can be dropped.
// Its memory is freed and it cannot be used again.
}

Now imagine you want to work with that String in another function. If you don’t know about ownership and borrowing, coming from another language, you will probably accidentally pass the variable and transfer its ownership, like seen below:

fn print_string(string: String) {
println!("I got {string} to print!");
// Does anything happen here?!
}
fn main() {
let a = String::from("Hello reader!");
print_string(a);
// prints a
println!("Hmmm, checking again whether I can still print the variable: {a}");
// Hmm...what might happen here?
}

But this way of doing it leads to the following issue:

Terminal window
error[E0382]: borrow of moved value: `a`
--> src/main.rs:11:72
|
6 | let a = String::from("Hello reader!");
| - move occurs because `a` has type `String`, which does not implement the `Copy` trait
7 |
8 | print_string(a);
| - value moved here
...
11 | println!("Hmmm, checking again whether I can still print the variable: {a}");
| ^ value borrowed here after move
|

This is basically the textbook example of ownership. print_string takes ownership of the String it accepts and drops the variable the moment its execution ends. The code that gave ownership of that variable away is no longer able to use it because the compiler prevents that from happening. It is clear, according to the rules of Rust, that after calling print_string, any variable passed into that function is gone.

This is where borrowing comes in. Instead of giving away ownership, a function can gently ask to borrow a variable. In return, it gives a few promises to keep the ownership model intact.

The same example from above can be rewritten in the following way to make it work. By passing the String by reference, print_string effectively only borrows it and won’t free the memory that variable occupies automatically when it finishes running.

// Notice the ampersand ↓ (&) before the type
fn print_string(string: &String) {
println!("I got {string} to print!");
// Nothing happens here
}
fn main() {
let a = String::from("Hello reader!");
// a is passed as a reference and, thus,
// effectively borrowed
print_string(&a);
// a is still alive!
// prints a
println!("Hmmm, checking again whether I can still print the variable: {a}");
// Now, a is dropped
}

Now everything is fine, or isn’t it? Well, yes and no. Borrowing and references are actually even more powerful than what you have just seen. The standard library, for example, also uses some pretty clever tricks to make its APIs as usable as possible.


AsRef As A Generic Way To Borrow From Any Type

The Basics

The simple examples from above are perfectly fine to explain the basic concept of ownership and borrowing. Sometimes, however, there are even more advanced use cases for borrowing.

Let’s say that you want to create a function that somewhere in its execution has to rename a file. Temporary files for downloads are a great example of this. If you download a pretty large file through your browser, you have probably already noticed that the file has a temporary name as long as your browser still writes to it. Only when the download is finished, the file is renamed.

Rust’s standard library has a helper method for this, called rename. It is located in the fs module of the standard library (std) and accepts two Paths; from, and to. The function itself is actually more intelligent, but let’s assume for a second that it only accepts Path(s) (because in reality…it somehow still does). Following this path (sorry, pun intended) further, you could write the following code to do that:

use std::fs;
use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
fs::rename(Path::new("temporary_file.txt"), Path::new("final_file.txt"))?;
}

That’s some pretty straightforward logic, isn’t it? One line of code and two arguments to rename a file. But is that already it? It normally should because Rust doesn’t allow for function overloading.

Let’s still do a small experiment to find out whether it really does, so take a look at the following code:

use std::fs;
use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = Path::new("temporary_file.txt");
fs::rename(path, "final_file.txt")?;
}

Okay, wait a second. This doesn’t lead to compile-time errors although we have passed a str to the function instead of a Path?! This shouldn’t compile at all…but somehow it still does.

You might be a little confused now, but let’s finally take a look at the actual implementation of fs::rename to find out what sorcery is actually responsible for all this to work:

#[stable(feature = "rust1", since = "1.0.0")]
pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()> {
fs_imp::rename(from.as_ref(), to.as_ref())
}

By looking at this code, you have probably already noticed that rename is a generic function that takes two types for each of its parameters. These generic types are additionally specified as AsRef<Path>, which is what makes the magic actually happen.

Any type you pass into rename that implements AsRef<Path> can be used as an argument! A look at Path’s source code reveals that several types from the standard library already implement AsRef<Path>:

#[stable(feature = "rust1", since = "1.0.0")]
impl AsRef<Path> for Path {
#[inline]
fn as_ref(&self) -> &Path {
self
}
}
#[stable(feature = "rust1", since = "1.0.0")]
impl AsRef<Path> for OsStr {
#[inline]
fn as_ref(&self) -> &Path {
Path::new(self)
}
}
#[stable(feature = "cow_os_str_as_ref_path", since = "1.8.0")]
impl AsRef<Path> for Cow<'_, OsStr> {
#[inline]
fn as_ref(&self) -> &Path {
Path::new(self)
}
}
#[stable(feature = "rust1", since = "1.0.0")]
impl AsRef<Path> for OsString {
#[inline]
fn as_ref(&self) -> &Path {
Path::new(self)
}
}
#[stable(feature = "rust1", since = "1.0.0")]
impl AsRef<Path> for str {
#[inline]
fn as_ref(&self) -> &Path {
Path::new(self)
}
}
#[stable(feature = "rust1", since = "1.0.0")]
impl AsRef<Path> for String {
#[inline]
fn as_ref(&self) -> &Path {
Path::new(self)
}
}
#[stable(feature = "rust1", since = "1.0.0")]
impl AsRef<Path> for PathBuf {
#[inline]
fn as_ref(&self) -> &Path {
self
}
}

All these types can be passed into fs::rename and they will automatically work. That’s awesome, isn’t it? But wait, aren’t there Into and From for cases like this one? Well, usually yes, but there is one advantage that AsRef has over From and Into, which makes it absolutely valuable.

Making Sense Of AsRef

Let’s open another scenario that makes it pretty clear why AsRef has its right to live and can also be used for seemingly obvious cases of type conversion. This time, we will model a blog system with blog posts that are separated into posts and guides.

A Post is the basic model for any post on a blog. It has an author, content, title, etc. An excerpt from this logic can look like this:

use chrono::{DateTime, Utc};
struct Post {
title: String,
author: String,
content: String,
published_at: Option<DateTime<Utc>>,
}

Additionally, there are advanced versions of a Post, like a VideoGuide, for example, that additionally contains videos that are stored and referenced separately. It makes not much sense to also put these into a Post, but Rust has no inheritance, and thus, according to the Newtype principle, you create a new struct and nest the original Post and all additional properties there, like seen below:

struct VideoGuide {
post: Post,
videos: Vec<Video>,
}

With these basics established, let’s think about what you could do if you have a function that wants to accept a Post. Let’s assume a notify function for this scenario that notifies all your readers that a new post has been published. An obvious way would be to simply borrow a Post, fetch all your subscribers, and then use everything to write a mail and send that to a lot of people. Such a function could look and be used like this:

fn notify(post: &Post) {
// some logic...
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let post = fetch_post_with_id("1337");
notify(&post);
let video_guide = fetch_video_guide_with_id("1337");
notify(&video_guide.post);
}

That’s perfectly fine, but you can simplify it a lot more, and additionally still benefit from one important aspect of AsRef: It’s cheap.

Into and From definitely have a right to exist, but they cost way more computing power than AsRef (it’s still not much but compared to AsRef, Into and From cost the whole world). AsRef only returns a reference (as its name implies), and it does it in a way that is cheap (because passing a reference to a nested property costs nearly nothing). This allows you to not worry too much about the cost of conversion and focus on the logic you want to implement.

With this settled, let’s look at how you can implement the scenario from above by leveraging AsRef:

use chrono::{DateTime, Utc};
struct Post {
title: String,
author: String,
content: String,
published_at: Option<DateTime<Utc>>,
}
struct VideoGuide {
post: Post,
videos: Vec<Video>
}
// Implementing AsRef is usually pretty simple
impl AsRef<Post> for VideoGuide {
fn as_ref(&self) -> &Post {
// This costs nearly nothing
&self.post
}
}
fn notify<P: AsRef<Post>>(post: P) {
// Getting the actual reference happens here
let post = post.as_ref();
// logic...
}
fn main() {
let post = fetch_post_with_id("1337");
notify(&post);
let video_guide = fetch_video_guide_with_id("1337");
// No need to access the post property directly
// anymore. The call to as_ref is used within the function called,
// so you can focus on just passing a reference here.
notify(&video_guide);
}

That’s the advantage of AsRef. You can simply pass in a reference, and the function itself can then make use of the super cheap conversion and borrowing of the nested value.

If you have a little more knowledge about Rust, there is one last thing you might ask yourself now: Why does &VideoGuide to &Post work automatically if you only implemented AsRef<Post> for VideoGuide and not &VideoGuide? Well, this is thanks to the following generic implementation in the standard library, which makes the implicit conversion possible:

// As lifts over &
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_const_unstable(feature = "const_convert", issue = "88674")]
impl<T: ?Sized, U: ?Sized> const AsRef<U> for &T
where
T: ~const AsRef<U>,
{
#[inline]
fn as_ref(&self) -> &U {
<T as AsRef<U>>::as_ref(*self)
}
}

This implicit magic saves you a lot of time. It negates the need for you to always implement AsRef for all Types and &Types that you work with.


Summary

AsRef is Rust’s way to allow a programmer to pass references of different types to functions that can then use the cheap conversion and borrowing AsRef provides, given that they implement the corresponding AsRef<T>. This additionally allows you to open your APIs for more use cases and types.

AsRef<T> is the way of conversion if you want to borrow, while Into<T> is the way if you want to take ownership of the value and are additionally okay with a little more overhead for the conversion.

The standard library itself makes excessive use of AsRef where applicable. Many functions in the standard library accept AsRef<str>, for example, which automatically makes these functions usable for both Strings, and strs, as well as any variant of them.


Share this post to: