When and why to use AsRef<T> instead of &T
Asked Answered
C

2

63

AsRef documentation writes

Used to do a cheap reference-to-reference conversion.

I understand reference-to-reference part what does it mean by cheap? I hope it has nothing to do with complexity theoretic (big Oh,. etc) "cheapness".

Example:

struct User {
  email: String, 
  age: u8,
}

impl AsRef<User> for User {
  fn as_ref(&self) -> &User {
     &self
  }
}

fn main() {
  let user = User { email: String::from("[email protected]"), age: 25 };
  let user_ref = &user;
  //...
}

What is the reason to implement AsRef for User if I can take a reference by simply &user? What is the rule for implementing AsRef?

PS: I couldn't find anything answering these questions in other forums and docs.

Contraction answered 3/2, 2021 at 11:11 Comment(0)
W
113

As you've noted, impl AsRef<User> for User seems a little pointless since you can just do &user. You could do impl AsRef<String> for User or impl AsRef<u8> for User as alternatives to &user.email and &user.age but those examples are probably misuses of the trait. What does it mean to be able to convert a User to an &String? Is the &String their email, their first name, their last name, their password? It doesn't make a lot of sense, and falls apart the moment a User has more than one String field.

Let's say we're starting to write an app and we only have Users with emails and ages. We'd model that in Rust like this:

struct User {
    email: String,
    age: u8,
}

Let's say some time passes and we write a bunch of functions and our app gets really popular and we decide we need to allow users to become moderators and moderators could have different moderation privileges. We could model that like this:

struct User {
    email: String,
    age: u8,
}

enum Privilege {
    // imagine different moderator privileges here
}

struct Moderator {
    user: User,
    privileges: Vec<Privilege>,
}

Now we could have just added the privileges vector directly into the User struct but since less than 1% of Users will be Moderators it seems like a waste of memory to add a vector to every single User. The addition of the Moderator type causes us to write slightly awkward code though because all of our functions still take Users so we have to pass &moderator.user to them:

#[derive(Default)]
struct User {
    email: String,
    age: u8,
}

enum Privilege {
    // imagine different moderator privileges here
}

#[derive(Default)]
struct Moderator {
    user: User,
    privileges: Vec<Privilege>,
}

fn takes_user(user: &User) {}

fn main() {
    let user = User::default();
    let moderator = Moderator::default();
    
    takes_user(&user);
    takes_user(&moderator.user); // awkward
}

It would be really nice if we could just pass &moderator to any function expecting an &User because moderators are really just users with just a few added privileges. With AsRef we can! Here's how we'd implement that:

#[derive(Default)]
struct User {
    email: String,
    age: u8,
}

// obviously
impl AsRef<User> for User {
    fn as_ref(&self) -> &User {
        self
    }
}

enum Privilege {
    // imagine different moderator privileges here
}

#[derive(Default)]
struct Moderator {
    user: User,
    privileges: Vec<Privilege>,
}

// since moderators are just regular users
impl AsRef<User> for Moderator {
    fn as_ref(&self) -> &User {
        &self.user
    }
}

fn takes_user<U: AsRef<User>>(user: U) {}

fn main() {
    let user = User::default();
    let moderator = Moderator::default();
    
    takes_user(&user);
    takes_user(&moderator); // yay
}

Now we can pass a &Moderator to any function expecting a &User and it only required a small code refactor. Also, this pattern now scales to arbitrarily many user types, we can add Admins and PowerUsers and SubscribedUsers and as long as we implement AsRef<User> for them they will work with all of our functions.

The reason why &Moderator to &User works out of the box without us having to write an explicit impl AsRef<User> for &Moderator is because of this generic blanket implementation in the standard library:

impl<T: ?Sized, U: ?Sized> AsRef<U> for &T
where
    T: AsRef<U>,
{
    fn as_ref(&self) -> &U {
        <T as AsRef<U>>::as_ref(*self)
    }
}

Which basically just says if we have some impl AsRef<U> for T we also automatically get impl AsRef<U> for &T for all T for free.

Wondrous answered 3/2, 2021 at 14:3 Comment(13)
Perhaps worth noting that AsRef<U> is blanket implemented for any &T where T: AsRef<U>, which is why you can call takes_user(&user) and don't have to pass user by value. This is a big part of what makes it useful as a general purpose (reference) conversion trait.Gujranwala
But I still don't understand, what it gives me. I see &moderator is better than &moderator.user but I could also send &moderator and take user as user = moderator.user inside take_user instead of writing ugly <U: AsRef<User>>. Clear and straightforward.Contraction
@Contraction How would you pass a &Moderator to takes_user(user: &User)? You can't, it's a compile error. You have to do takes_user<U: AsRef<User>>(user: U). Also, it's okay to not like the trait and to not use it in your API designs. If there's no use-case for AsRef in your application then no one is forcing you to use it. The Rust standard library gets decent mileage out of the trait by providing impls for AsRef<Path> and AsRef<OsStr> for the many various string types.Wondrous
@Wondrous take_user(&Moderator m) {...}Contraction
@Contraction you changed the function signature so it no longer works for regular users, now passing a &User is a compile-time error.Wondrous
@Wondrous Why do I need to pass User? Just pass Moderator. You can access the user if you have moderator.Contraction
@Contraction all Moderators are Users but not all Users are Moderators. In my answer I gave the example that less than 1% of Users would be Moderators. If you change the function signature to only accept Moderators you would be excluding 99% of your Users.Wondrous
@Wondrous Ok, you mean you want to make the function generic, e.g., take_user must be able to accept anything that implements AsRef<User>.Contraction
@Contraction Yes, that's the primary use-case of the trait, for writing generic functions. If you write any Rust programs that work with the filesystem you'll notice that many standard library functions and methods are generic and take <P: AsRef<Path>> as an argument.Wondrous
Seems like an adapter pattern to me.Joannajoanne
@尤慕李 that is because it is. Rust's type system allows for automatic adaptors via the AsRef trait. It is a form of operator overloading that happens also perform the conversion at the same time.Ohara
Great answer @pretzelhammer! A question: for the blanket implementation in the std library which you had provided in your answer, why is the deferencing of &T in the fully qualified call to as_ref neccessary when the function signature of as_ref takes a &T? I.e. why do <T as AsRef<U>::as_ref(*T) and not ...::as_ref(T)?Ulema
@jim &self is a shorthand for self: &Self. In this case, the blanket implementation is for a reference type &T, i.e. the type of self variable is &&T. By definition, <T as AsRef<U>>::as_ref() is only expecting &T, therefore self variable in this &T implementation has to be de-referenced as *self.Chape
D
23

I hope it has nothing to do with complexity theoretic (big Oh,. etc) "cheapness".

It absolutely does. AsRef is intended to cost essentially nothing.

What is reason to implement AsRef for User if I can take a reference by simply &user?

There probably isn't one. AsRef is useful for generic code, especially (though not exclusively) for ergonomics.

For instance if std::fs::rename took an &Path you'd have to write:

fs::rename(Path::new("a.txt"), Path::new("b.txt"))?;

which is verbose and annoying.

However since it does take AsRef<Path> instead it works out of the box with strings, meaning you can just call:

fs::rename("a.txt", "b.txt")?;

which is perfectly straightforward and much more readable.

Dg answered 3/2, 2021 at 11:24 Comment(3)
The rename would look even (slightly) worse, as you'd have to reference the paths: fs::rename(&Path::new("a.txt"), &Path::new("b.txt"))?Preceptive
@user4815162352 nope it’s a bit unusual but Path::new actually returns an &Path. It could not do otherwise as Path is unsized, a bare Path is essentially the same thing as a bare str.Dg
For the curious, Path::new's implementationOhara

© 2022 - 2024 — McMap. All rights reserved.