DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 963,274 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Sven Kanoldt
Sven Kanoldt

Posted on • Updated on • Originally published at d34dl0ck.me

What is the matter with `AsRef`?

the AsRef trait is everywhere in the std lib and very handy. However, the benefit using it is maybe not too obvious. But read on, and you will see some useful examples and where to apply it.

This is the 2nd post on the series "practical rust bites" that shows very tiny pieces of rust taken out of practical real projects. It aims to illustrate only one rust idiom or concept at a time with very practical examples.

Background

AsRef can be found in the std::convert module and is used
for cheap reference-to-reference conversion.

It's a very essential trait widely used in the std library and helps with seamless reference conversions from one type to another.
Often it is at play, and you do not even realize it is there.

Example: String or &str

Did you ever have had a function where you wanted to accept a string as a parameter?
You might then also have asked yourself should you accept a string reference (as in &str) or an owned string (as in String)?

So you would have something like this in mind:

fn take_a_str(some: &str) {}
Enter fullscreen mode Exit fullscreen mode

and the other one:

fn take_a_string(some: String) {}
Enter fullscreen mode Exit fullscreen mode

So why not have both?!

At this very point AsRef<str> comes in very handy,
because both types str and String implement this trait.

fn take_a_str(some: impl AsRef<str>) {
    let some = some.as_ref();
    println!("{some}");
}

fn main() {
    take_a_str("str");
    take_a_str("String".to_string());

    // also `&String` is supported:
    let string_ref = "StringRef".to_string();
    take_a_str(&string_ref);
}
Enter fullscreen mode Exit fullscreen mode

check out the code on the playground

As you can see this version of take_a_str accepts a type that just implements AsRef<str>.

By doing so some: impl AsRef<str> does not make any concrete assumptions on the type that is provided.
Instead, it just insists that any given type only implements this trait.

Example: wrapper type

In this example we want to focus on how can you implement AsRef yourself for any arbitrary struct.

Let's look at this tiny program:

pub struct Envelope {
    letter: String
}

fn main() {
    let a_letter = Envelope { 
        letter: "a poem".to_string() 
    };

    println!("this is a letter: {}", &a_letter);
}
Enter fullscreen mode Exit fullscreen mode

But of course you'd say now, well, it does not implement Display and so does the rust compiler agree with you:

error[E0277]: `Envelope` doesn't implement `std::fmt::Display`
  --> src/main.rs:10:38
   |
10 |     println!("this is a letter: {}", &a_letter);
   |                                      ^^^^^^^^^ `Envelope` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Envelope`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
Enter fullscreen mode Exit fullscreen mode

But we ignore Display for now and focus on another use case. That is we want to access the inner value of Envelope.

Solving this problem with implementing AsRef<str> we would do the following:

impl AsRef<str> for Envelope {
    fn as_ref(&self) -> &str {
        // here we up-call to the `AsRef<str>` implementation for String
        self.letter.as_ref()
    }
}
Enter fullscreen mode Exit fullscreen mode

and with this we need just one little adjustment:

fn main() {
    let a_letter = Envelope {
        letter: "a poem".to_string()
    };

    println!("this is a letter: {}", a_letter.as_ref());
}
Enter fullscreen mode Exit fullscreen mode

Now the rust compiler does not complain anymore.

Again, of course it would be appropriate to implement Display for this very println! case, but the point is here that we would want to access the inner data as reference.

check out the full example on the playground

Example: a composed type

I have to admit, take this example with a grain of salt. It's very likely that this kind of usage
would lead to problems with bigger structs. So as a rule of thumb if a struct has more than 2 fields, better not go down this path.

let's look at a simple struct that represents a weight:

struct Weight {
    weight: f32,
    unit: String
}

impl Weight {
    /// Weight in Tons that is 157.47 stones 
    pub fn from_tons(weight: f32) -> Self {
        Self { weight, unit: "t".to_string() }
    }

    /// Weight in Stones
    pub fn from_stones(weight: f32) -> Self {
        Self { weight, unit: "st".to_string() }
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see we have not given pub fields and also no getter accessor functions.

So how we can actually get our hand on the data inside?

You've guessed it, AsRef is our friend here as well.

impl AsRef<str> for Weight {
    fn as_ref(&self) -> &str {
        &self.unit
    }
}

impl AsRef<f32> for Weight {
    fn as_ref(&self) -> &f32 {
        &self.weight
    }
}
Enter fullscreen mode Exit fullscreen mode

So here we use the AsRef trait for the types f32 and str to get access to the weight and unit inside the struct.

fn main() {
    let a_ton = Weight::from_tons(1.3);

    let tons: &f32 = a_ton.as_ref();
    let unit: &str = a_ton.as_ref();

    println!("a weight of {tons} {unit}");
}
Enter fullscreen mode Exit fullscreen mode

you could also skip the variable bindings and make it less verbose:

fn main() {
    let a_ton = Weight::from_tons(1.3);

    println!(
        "a weight of {} {}",
        a_ton.as_ref() as &f32,
        a_ton.as_ref() as &str
    );
}
Enter fullscreen mode Exit fullscreen mode

checkout the full example on the playground

Wrap up

AsRef is a very handy tool and if you go through the standard library, you will find it implemented for a lot of types.
By this a lot of reference types become interchangeable and this increases the overall ergonomics a lot.

As you also have seen how AsRef can be useful to get access to inner data of structs without having to provide accessory methods or public fields.

A very related function to get the inner value of a struct, but as value (not as reference) is the .into_inner().
As you can see it is used a lot in the std lib, but it is not bound to a trait. So this is more of a convention: Whenever you need to consume a struct and unfold the inner wrapped type, then .into_inner() is the common schema to go for.

Versions used for this post:

$ cargo --version && rustc --version
cargo 1.61.0 (a028ae42f 2022-04-29)
rustc 1.61.0 (fe5b13d68 2022-05-18)
Enter fullscreen mode Exit fullscreen mode

To receive updates about new blog post and other rust related news you can follow me on twitter.

Top comments (3)

Collapse
 
sloan profile image
Sloan

Hi there, we encourage authors to share their entire posts here on DEV, rather than mostly pointing to an external link. Doing so helps ensure that readers don’t have to jump around to too many different pages, and it helps focus the conversation right here in the comments section.

If you choose to do so, you also have the option to add a canonical URL directly to your post.

Collapse
 
5422m4n profile image
Sven Kanoldt Author

Thanks for sharing that. Sounds like a good idea.

Collapse
 
kgrech profile image
Konstantin Grechishchev • Edited on

Thank you for your article!

🌚 Life is too short to browse without dark mode