I think that the main reason that people don't use the convert traits very often is because they're annoying to use, forcing the conversion to the caller (who shouldn't really care).
bar has a better API than foo but it's more annoying to write.
We could have some syntax that desugars to the traits like this
fn foo<T>(arg: do convert &T) // For AsRef
fn foo<T>(arg: do convert &mut T) // For AsMut
fn foo<T>(arg: do convert T) // For Into
fn foo<T>(arg: do convert Result<T, _>) // For TryInto (I'm not sure how useful this will be though)
This has the benefit of being easier to write, easy to refactor, not require a generic / impl and not require the shadowing line.
i see them used a lot in "high level" apis like reqwest, and i call into a fair bit in my own code.
I would say if anything needs syntactic sugar, it would be implementing single-function traits, which (at least in the case of traits that mirror other traits, like Borrow, Deref, and AsRef), function delegation will help with
bar's signature has significant disadvantages over foo’s:
bar is generic, so it has to be monomorphized for each concrete type R it is used with, for each calling crate. foo is not generic, so it only needs to be compiled once (unless inlined). Therefore, the program using bar likely takes longer to compile, and may be larger.
foo 's parameter is a coercion site that can cause deref coercion for types like &std::cell::Ref<'_, str>, which eventually dereference to str but do not implement AsRef<str>, but bar does not allow this.
fn example(cell: &RefCell<String>) {
foo(&cell.borrow()); // compiles
bar(&cell.borrow()); // does not compile
}
Deref coercion is the bread and butter of Rust pointer-like-type usage, and should be allowed to work whenever possible.
Also, while str containers frequently implement AsRef<str>, it is much less common for AsRef to be implemented for non-dynamically-sized types, or for generic containers containing some T.
We should not encourage authors to write functions generic over AsRef unless these disadvantages are mitigated in some way. The place where AsRef is worth using today is for elements of containers; for example,
fn bar<R: AsRef<str>>(arg: &[R]) {
is flexible in a way that is important beyond ergonomics, because it can accept a [&str], [String], or even third-party types like [ArcStr], whereas &[&str] requires that exactly [&str] exist somewhere.
(as I'm sure you know...) It's a standard pattern to have the generic version call an inner non-generic version, which addresses at least the code generation point. I have no idea what to search for, but I believe there was some noise about making that automatic?
There's also cases where Deref doesn't suffice, like all the std::fs functions that take AsRef<Path> because there are so many things that may look like a &Path without having a Deref path. Here's an overview (for simplicity omitting path::{Component{,s},Iter} nodes and some AsRef edges, in particular those for transitive closures):
&String --> &str
|
v
&OsString --> &OsStr horizontal is Deref
^ vertical is AsRef
|
v
&PathBuf --> &Path
Honestly, the situation with Deref/AsRef/Borrow is far from ideal, a bit of a nest, and improving it doesn't seem all that likely. As far as I've intuited:
Deref
Implement when &Self functionally is-a &Target. But note that this isn't "OOP is-a" (inherits API, may override behavior), it's "thin container is-a" (contains and manages the target, has nearly negligible identity beyond that).
Weakly implies that Self should impl AsRef<Target> and Borrow<Target>.
Don't take arguments generic over Deref, allow deref coercions to apply instead.
Don't take arguments of an impl Deref type when &Target would suffice.
AsRef<T>
Implement for explicit conversion from &Self to &T for any "relevant" (typically unsized) T where the conversion is "cheap" (typically nothing more than logical place projection) and &Self is a fair substitute for &T.
Implement the reflective AsRef<Self> for any unsized types, as it's the most relevant for such.
Take arguments generic over AsRef<T> in convenience API that would otherwise take &T for some unsized T in the lower level guts.
Keep the function small before dispatch to the &T functionality for polymorphization purposes.
Honestly, should've been "take &(impl AsRef<T> + ?Sized)", but it wasn't and isn't, so functions that are incapable of utilizing it can take ownership, but stick to existing convention.
Should be kept as transitive as reasonably possible for concrete T.
Borrow<T>
Implement when &Self should substitutable for &T in a way stronger than just Deref — namely that any other trait methods behave observably the same for the two types and can't be used to distinguish between self and self.borrow().
Weakly implies that &Self should coerce to &T (when concrete, through some other mechanism).
Be generic over Borrow to generalize over owned or borrowed T, i.e. for Cow (copy-on-write).
Honestly, kind of got abused for its use in hashmap key lookups, which kind of wants for a more direct key equivalence trait instead of just borrow equivalence.
This understanding builds from a position that it is "wrong" for an API to take ownership if it would always be satisfied without ownership, because this leads to suggestions to use f(s.clone()) where f(&s) would suffice. A different experience might hold a different position (e.g. for async spawn, you need to pass in owned values, so taking ownership even if you don't require it can be a boon in some cases), but this is what I've built up.
No, it was. std was changed from that form to the (worse, IMNSHO) ownership-taking form pre-1.0 on primarily aesthetic grounds (!). As a result and as you alluded to, the compiler will suggest you pathbuf.clone() instead of &pathbuf when you accidentally give up ownership.
If you eschew convention and go with &(impl AsRef<T> + ?Sized), you avoid that particular downside.