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:
- Maintainable
- User-friendly
- Extensible
- 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:
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:
But this way of doing it leads to the following issue:
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.
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 Path
s; 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:
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:
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:
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>
:
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:
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:
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:
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
:
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:
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.