I was missing that part, even though I cited it in my OP. So the documentation does state that AsRef
isn't reflexive (not in the overview in std::convert
though, and it does so in the context of Borrow
only).
Maybe it could be said more explicitly? Something like:
(example of how it could be phrased)
Trait std::convert::AsRef
Reflexivity
Ideally,
AsRef
would be reflexive, that is there is animpl<T: ?Sized> AsRef<T> for T
, withas_ref
simply returning&self
. Such a blanket implementation is currently not provided due to technical restrictions of Rust's type system (it would be overlapping with another existing blanket implementation for&T
whereT: AsRef<U>
which allowsAsRef
to auto-dereference, see "Generic Implementations" above).A trivial implementation of
AsRef<T> for T
must be added explicitly for a particular typeT
where needed or desired. Note, however, that not all types fromstd
contain such an implementation, and those cannot be added by crates due to orphan rules.Therefore, writing
.as_ref()
cannot be used generally to go from a type to its reference, e.g. the following code does not compile:let i: i32 = 0; let r: &i32 = i.as_ref(); // let r: &i32 = &i; must be used instead
Maybe that's a bit easier to understand for newcomers. Particularly, it would explain that AsRef
isn't lacking reflexivity for semantic but only for technical reasons (P.S.: and for backwards-compatibility, as I try to show further below).
Explaining this might help to reduce confusion on this subject for non-newcomers as well.
There are several methods in std
which take a P: AsRef<Path>
(which might or might not be considered to be over-generic). I would say it makes it easy to invoke them, but not sure if that has been a good design decision.
Getting back to some of the breaking examples in my OP, it should be said that circumventing these problems in practice isn't so difficult. We can do:
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fs::File;
fn main() {
let _: Result<File, _> = File::open("nonexistent");
let _: Result<File, _> = File::open(&Cow::Borrowed(OsStr::new("nonexistent")));
// This fails to compile:
// let _: Result<File, _> = File::open(&Cow::Borrowed("nonexistent"));
// If we have a reference to a `Cow`, we must use `&**`:
let _: Result<File, _> = File::open(&**(&Cow::Borrowed("nonexistent")));
}
Just like you said, using &**
(or &*
if the Cow
is owned and may be consumed). Not nice, but doable.
But these practical issues aside, I wonder if the use of .as_ref()
in the following is (or rather: should be) considered idiomatic:
use std::borrow::Cow;
use std::fs::File;
fn main() {
let cow: Cow<_> = Cow::Borrowed("nonexistent");
let ref_to_cow: &Cow<_> = &cow;
let _: Result<File, _> = File::open(&**ref_to_cow);
let _: Result<File, _> = File::open(ref_to_cow.as_ref());
}
This works because of:
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized + ToOwned> AsRef<T> for Cow<'_, T> {
fn as_ref(&self) -> &T {
self
}
}
(source)
I would like to state the hypothesis that:
- while this implementation exists (and cannot be removed without breaking code), it should not have been added in the first place,
- using
ref_to_cow.as_ref()
in the above Playground should be avoided in idiomatic code (hypothesis 4).
Moreover, I believe that existing code which uses .as_ref()
in that way makes it impossible to solve the outlined problems by solely adding two blanket implementations (AsRef<T> for T
and AsRef<U> for T where T::Target: AsRef<U>
, e.g. with negative bounds) in the future.
This can be demonstrated by using my_as_ref
as defined in my above post:
fn takes_path(_: impl MyAsRef<Path>) {}
fn main() {
let cow: Cow<_> = Cow::Borrowed("nonexistent");
let ref_to_cow: &Cow<_> = &cow;
let _ = takes_path(&**ref_to_cow);
let _ = takes_path(ref_to_cow.my_as_ref());
}
(Note that the first (reflexive) blanket implementation cannot provided for technical reasons in this demonstration but only a concrete implementation for str
.)
Here type inference will fail due to multiple implementations of MyAsRef<_> for str
:
Compiling playground v0.0.1 (/playground)
error[E0282]: type annotations needed
--> src/main.rs:78:35
|
78 | let _ = takes_path(ref_to_cow.my_as_ref());
| -----------^^^^^^^^^--
| | |
| | cannot infer type for type parameter `T` declared on the trait `MyAsRef`
| this method call resolves to `&T`
error[E0283]: type annotations needed
--> src/main.rs:78:35
|
78 | let _ = takes_path(ref_to_cow.my_as_ref());
| -----------^^^^^^^^^--
| | |
| | cannot infer type for type parameter `U`
| this method call resolves to `&T`
|
note: multiple `impl`s satisfying `str: MyAsRef<_>` found
--> src/main.rs:51:1
|
51 | impl MyAsRef<str> for str {
| ^^^^^^^^^^^^^^^^^^^^^^^^^
...
66 | impl MyAsRef<Path> for str {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
note: required because of the requirements on the impl of `MyAsRef<_>` for `Cow<'_, str>`
--> src/main.rs:38:12
|
38 | impl<T, U> MyAsRef<U> for T
| ^^^^^^^^^^ ^
help: use the fully qualified path for the potential candidates
|
78 | let _ = takes_path(<str as MyAsRef<Path>>::my_as_ref(&ref_to_cow));
| +++++++++++++++++++++++++++++++++++ ~
78 | let _ = takes_path(<str as MyAsRef<str>>::my_as_ref(&ref_to_cow));
| ++++++++++++++++++++++++++++++++++ ~
Some errors have detailed explanations: E0282, E0283.
For more information about an error, try `rustc --explain E0282`.
error: could not compile `playground` due to 2 previous errors
Ironically just writing takes_path(ref_to_cow)
would work! (Playground)
While these considerations are all very hypothetical and ignore that std
doesn't do all these things, it might still be worth noting, because
- being able to provide two blanket implementations for
AsRef
in the future would not automatically solve all the outlined problems but providing such a second blanket implementation forAsRef
might instead induce more problems with existing code, - third party crates may go a different way than
std
.
Regarding the first point, I believe that the problem really cannot be solved without starting over with an entirely new AsRef
(likely not possible at this point, but I still hope that somehow the Edition mechanism could be used in future to overcome bad design decisions of the past).
The latter point brings me back to the question:

So what to do in external crates?
- Use
std
's practice regarding smart-pointers?- Be consistent with
std
's implementation ofAsRef
for references?Maybe this question can only be answered individually for each use case? Or perhaps there is no satisfactory answer at all.
Moreover, should .as_ref()
be avoided in teaching material in cases where it's used to dereference (edit: generic(?)) smart-pointers?