When to use AsRef<T> vs &T

Heya,

Just started learning Rust, and writing some basic programs with it. When (or how often) do you use AsRef<T> over &T?

For example, today I've been writing a trait for a struct that appends strings to itself (in some internal buffer), but I wasn't sure which one to use:

pub trait SomeTrait {
    fn append_0(&mut self, name: &str);
    fn append_1<TString>(&mut self, name: TString) where TString: AsRef<str>;
}

The first option means any std::string::Strings would need to be passed in as &name, and after doing in all call sites (and for functions with multiple parameters), the callers were mixed with & and non-&s. It looked unergonomic with & symbols literred everywhere. On the other hand, using the second option cleaned up how it looked, but then I wasn't sure if that was idiomatic Rust, and it's kind of pervasive where every function would need to have AsRef (if we're just using a parameter solely within the lifetime of that function).

So I want to ask, what do you do, and what's idiomatic? When should we be using AsRef, but also, when shouldn't we be using AsRef?

On a related note, what about Into?

I've read the entire Rust book, Nomicon and Rust by Example, but none of those give any guidance or explanation about situations these should be used.

Thanks so much.

4 Likes

I'm also new to Rust, but my impression is that &str is the usual choice here: it's explicit and it's what I see in the standard library functions that accept strings.

Requiring call sites to explicitly lend their strings to the method is not bad in my opinion. Can you give and example of a call with mixed parameters that looks strange?

1 Like

Ohh, I see. I understand and value the explicitness, and that's what I first thought when I came here, but after looking at parts of the standard library, and a bunch of well-known crates, I wasn't sure about the whole thing. I know I can use it, but I don't feel confident knowing when it's bad form. I feel tempted to use it a lot, and any time I want a str reference.

For example, std::path has a bunch of functions that take an AsRef<Path> instead of taking a &T where T: Path. This is one such example:

impl Path {
    fn starts_with<P: AsRef<Path>>(&self, base: P)
}

My understanding is that it makes calling those functions more ergonomic, and saves you from needing to construct a Path from a str, OsStr, PathBuf, or whatever else. Instead of converting them to a string with as_str(), relying on deref coercions, or some other method, you can just pass them as a normal parameter.

There are other examples in the standard library where CStr, OsStr, String can be converted into an AsRef<str>, and what I asked about doesn't seem too much different. The same when I look at popular crates on crates.io (hyper for urls, etc). Strings are one example, but I think it applies equally to other types that aren't strings.

So I wanted to get some guidance - how often should you use it? Is it bad form to blanket use AsRef on everything, especially strings?


On the question of an example, this one isn't nearly as bad as what I had earlier (can't find it), but it might look something like this. It only deals with String -> &str, so we don't need to construct Paths or anything like that:

let client       = Client::new(&cdn_url, application_token, &security_token, session_token);
let mut response = client.send(&service_name, "data.json")?;
let package      = serializer.deserialize::<Package>(codec, response.body, &response.mime_type, &application_token)?;

// ...

Really sorry if I'm asking too much.

I'm not even close to an expert rust programmer and i've never actually used what i'm about to say in practice so take with a large pillar of salt but my understanding of Rust gives these two things subtly different meanings that give some interesting insight into the details of Rust compile time type checking.

&str means "this is a reference to a string". As in this is a pointer to an object (on the heap?) that is definately a string. It can't be an int. It can't be a btree... it's definately a string. Providing a reference to something that isn't precisely a str is a compile time error.

AsRef<str> means "this is something that implements the trait for being converted into a string". So if you have a struct Foo and then impl AsRef<str> for Foo then you could pass a struct Foo to the function that is AsRef<str> but not to one that is &str (Because it matches the first type but not the second).

So now your thinking "but hang on... thats not how str and String work in the standard libraries" and you'd be right but thats because another trait gets involved here. If you implement Deref (as in impl Deref for String { type Target = str; ... }) then you're telling rust that it can implicitly convert this to a str if necessary. Which would allow you to use a String when the function asks for a str.

I'm not even really a Rust initiate so i've probably gotten this horribly mangled. I only comment because i recently listened to the new rustaceans podcast on this (at show_notes::e018 - Rust) which also linked to a rust doco page (at Borrow and AsRef) both of which were really interesting reading/listening. Chris Krycho tries to answer the question of when you would use each of them.

6 Likes

Thanks for the comment! Just listened to the podcast too, but it wasn't able to add any new information or answer my question ):

I definitely grasp the concepts, and all the coercions involved. What I am unsure about is when we should take a parameter that is an AsRef<T>, that lets anyone pass a type that can be converted to a reference of T (with an impl AsRef<T> of course), or require them to pass a &T directly (and forcing them to convert to a &T at all call sites, either with deref coercions or forcing them to call a method like as_t() or construct a new object).

From the sounds of that podcast, it sounds useful to take blanket AsRefs to every type (and especially so for &str, since you can then use String, CString, OsStr, etc...), but obviously that cannot be true.

Maybe there's better guidance, but I settled on my own approach based on the semantics of &T vs AsRef<T> I've noticed.

I tend to favour a &T over AsRef<T> except for builder methods because it means that borrow can be required to outlive the function that uses it, it saves a generic parameter and is a bit easier to follow in docs. Then I might switch to a more general function signature later for ergonomic reasons, like when there are a couple of common input types, like the case of AsRef<Path> in the file APIs.

If you've got multiple parameters, AsRef might end up being a bit too liberal. Take the following pseudo-example:

// Takes 2 parameters; AsRef<A> and AsRef<B>
fn do_stuff<AA: AsRef<A>, AB: AsRef<B>>(a: AA, b: AB) {
    let (a, b) = (a.as_ref(), b.as_ref());
}

do_stuff("a" , "b");

Now if we swap the parameters for do_stuff around, but not the call we've got a problem:

// Takes 2 parameters; AsRef<B> and AsRef<A>
fn do_stuff<AB: AsRef<B>, AA: AsRef<A>>(b: AB, a: AA) {
    let (b, a) = (b.as_ref(), a.as_ref());
}

// Now "a" is `B`!
do_stuff("a" , "b");

If you took explicit references that wouldn't be the case, you'd get a compile error.

Tradeoffs, tradeoffs everywhere.

The semantics are different so you have to decide what approach is best for your API. But it's easier to go from &T to AsRef<T>, so I start with &T and need to be convinced to generalise it.

That's my current approach, I'm happy to take other opinions on board :slight_smile:

For filesystem paths - always use AsRef<Path>, because they can be Paths, regular UTF-8 strings or system-specific strings.

Use AsRef when ownership doesn't matter. Function with AsRef argument will take both foo(obj) and foo(&obj) without complaining. It's usually useful when you also use traits — you can use references without having to type out lifetime annotations.

The downside is that it makes type inference a bit more ambiguous, so sometimes compiler won't be sure if you wanted &Vec or convert Vec to &[]. You almost always want the latter, so for vectors use &[T] rather than AsRef.

12 Likes