I'm of that camp as well; looking at a PR's diff or some grep
result to see Self
in signatures is just so annoying.
-
But I do reckon that Self
as an actual alias, such as in constructors, is useful, and may actually avoid type inference errors should the author of the function make the mistake to leave some generic non-lifetime parameters unconstrained.
/// Doing `MyHashSet::with_default_hasher()` will lead to inference errors with `S`.
impl<K, S> MyHashSet<K, S> {
fn with_default_hasher() -> MyHashSet<K, RandomState>
But the moment I'm reading code, or the documentation of a crate, and the signature becomes a bit more complex (e.g., scoped threads API), I waste so much time performing that Self
alias substitution in my head just to be able to fully unveil a function signature. It's so silly that the current situation forces the reader to perform that mechanical operation just to keep some aliases around.
- Incidentally, it's a similar issue with type aliases: sometimes they improve the readability by conveying some extra information, and in other cases, they just make the signature less self-contained and require that the reader click on a bunch of aliases and create a mental map of the substitutions just to read the signature.
That's why, one thing I'd love, independently of the "written code style" w.r.t. Self
discussed herein, is for there to be to a togglable option, in rustdoc, to replace Self
with what it refers to.
One reason I dislike Self
, for instance, is the common beginner antipattern of:
impl MyThing<'a> {
fn get(&'a self) -> &'a Field
which, when get
is far from the impl
, is easy/ier to miss.
Regarding function bodies, the Self
-as-a-shortcut can also lead to strange error messages.
Take the following snippet, for instance:
enum MyLengthyCowName<'str> {
Owned(String),
Ref(&'str str),
}
impl MyLengthyCowName<'_> {
fn reborrow<'r>(&'r self) -> MyLengthyCowName<'r> {
match *self {
| Self::Ref(it) => Self::Ref(it),
| Self::Owned(ref it) => Self::Ref(&**it),
}
}
}
yields:
error[E0495]: cannot infer an appropriate lifetime for pattern due to conflicting requirements
--> src/lib.rs:10:27
|
10 | | Self::Owned(ref it) => Self::Ref(&**it),
| ^^^^^^
|
note: first, the lifetime cannot outlive the lifetime `'r` as defined here...
--> src/lib.rs:7:17
|
7 | fn reborrow<'r>(&'r self) -> MyLengthyCowName<'r> {
| ^^
note: ...so that reference does not outlive borrowed content
--> src/lib.rs:10:27
|
10 | | Self::Owned(ref it) => Self::Ref(&**it),
| ^^^^^^
note: but, the lifetime must be valid for the lifetime `'_` as defined here...
--> src/lib.rs:6:23
|
6 | impl MyLengthyCowName<'_> {
| ^^
note: ...so that the types are compatible
--> src/lib.rs:10:38
|
10 | | Self::Owned(ref it) => Self::Ref(&**it),
| ^^^^^^^^^
= note: expected `MyLengthyCowName<'_>`
found `MyLengthyCowName<'_>`
For more information about this error, try `rustc --explain E0495`.
where doing:
- | Self::Owned(ref it) => Self::Ref(&**it),
+ | Self::Owned(ref it) => MyLengthyCowName::Ref(&**it),
fixes the issue.
And this kind of illustrates the issue I have with Self
aliases: in the mind of some people, or even anyone when a bit unattentive, it's easy to picture that Self
stands for MyLengthyCowName
, for instance, or, in general, for the path of the type / for the type constructor. That is, Self
in a impl<T> Vec<T>
may be seen, loosely, as representing Vec
, when it actually represents Vec<T, GlobalAlloc>
, and similarly, Self
was standing for MyLengthyCowName<'lt>
, with 'lt
being the '_
-anonymous lifetime introduced at the impl
level.
In these situations, Self
can end up being an overly terse way to hide / forget of generic parameters, and even more so as the impl
line that introduced its ever changing definition is far.
And while for type parameters this doesn't happen that often, I think it's a pretty common mistake the moment lifetime parameters are involved.
So I guess my personal heuristic is, besides the very specific new() -> Self
case, not to use Self
the moment it's hiding a lifetime parameter