Hi folks,
Could you please tell me, if there is a difference between the following?
The only difference I can see is that in the second case I won't need to type AsRef<[u8]>
again for another (parse_this2
) function?
Thank you.
pub trait MyParser {
fn parse_this<T>(self, foobar: T) -> String
where
T: AsRef<[u8]>;
}
impl MyParser for String {
fn parse_this<T>(self, foobar: T) -> String
where
T: AsRef<[u8]>,
{
let s = str::from_utf8(foobar.as_ref()).unwrap_or_default();
format!("{self}{s}",)
}
}
pub trait MyParser2<T>
where
T: AsRef<[u8]>,
{
fn parse_this(self, foobar: T) -> String;
}
impl<T> MyParser2<T> for String
where
T: AsRef<[u8]>,
{
fn parse_this(self, foobar: T) -> String {
let s = str::from_utf8(foobar.as_ref()).unwrap_or_default();
format!("{self}{s}",)
}
}
for this particular example, since you use a blanket impl
of MyParser2
, the difference is small.
however, when the generic parameter is on the trait, you can have different implementations of the trait with different type arguments for the same type, e.g.
impl MyParser2<Vec<u8>> for String {
//...
}
impl<'a> MyParser2<&'a str> for String {
//...
}
impl<const N: usize> MyParser2<[u8; N]> for String {
//...
}
this is because a trait with generic parameter is not a single trait, but a family of (infinitely many) traits. this is not the case when the generic is on the method, where there's only one single. as a result, this may also affects type inference, depending on the situation.
another important difference is, when the method has a generic parameter, the trait CANNOT be dyn
-safe. although this is irrelevant for your example, since the method takes self
as receiver, which cannot be dynamically dispatched anyway.
3 Likes
Another difference is that when the generic is on the method, every implementor must handle every type that meets the bounds (in a single implementation). But when the generic is on the trait, implementors can support a subset of types (with a bounded blanket implementation, or separate implementations for different types).
3 Likes
Thank you!
I also realized I can do:
pub trait MyParser3<T> {
fn parse_this2(self, add_me: T) -> String;
}
impl<X, T> MyParser3<T> for X
where
X: Into<String>,
T: AsRef<[u8]>,
{
fn parse_this2(self, add_me: T) -> String {
let first = self.into();
let second = str::from_utf8(add_me.as_ref()).unwrap_or_default();
format!("{first}{second}",)
}
}
Btw. is there a difference between?
impl<X, T> MyParser3<T> for X
where
Self: Into<String>,
T: AsRef<[u8]>,
or
impl<X, T> MyParser3<T> for X
where
// Self: Into<String>,
X: Into<String>,
T: AsRef<[u8]>,
or
pub trait MyParser3<X, T> {
fn parse_this2(self, add_me: T) -> String;
}
// impl<X, T> MyParser3<T> for X
impl<X, T> MyParser3<X, T> for X
where
X: Into<String>,
T: AsRef<[u8]>,
Thank you.
Self
is an alias that represents the implementing type exactly. In impl
blocks, it's just a matter of convenience that allows you to not type the name of the type which is nice since most type names are longer; additionally it allows for easier refactoring if you were to change the name of the type since you wouldn't have to change all the Self
s to the new name.
Self
becomes more than just a convenience when used in trait
definitions though since it allows one to reference the implementing type. This is extremely powerful, and it's something some other languages don't have (e.g., C#). In languages like C#, the only way you could mimic Self
is by explicitly defining a type parameter intended, but impossibly enforced, to represent "the implementing type". The language would not actually prevent you from implementing an interface
where the type passed for TSelf
was not the same as the implementing type though. For example, IEquatable<T>
does not actually enforce what it ideally would enforce since one can always pass a different type to that interface
constructor than the type that is implementing it.
In Rust you can think of all trait
s as having at least one "hidden" type parameter: Self
. By "hiding" this type parameter, you make it impossible for a developer to pass a different type into the trait
constructor as the implementing type preventing the above issue that exists in languages like C#.
So your first two examples are exactly the same. The third example is obviously different since now the trait
constructor has two parameters (3 if you include the "hidden" Self
). You have essentially defined a second Self
parameter except one where X
doesn't actually have to be the implementing type. The fact that you don't reference X
at all in the trait
definition is a red flag that you don't need it. It's not too different than a polymorphic function that never uses the type parameter (e.g., fn foo<T>() {}
) or a "normal" function that never uses a "normal" parameter (e.g., fn foo(x: u32) {}
). You have essentially defined a needlessly verbose and more confusing trait
. Honestly, I'm surprised there aren't compiler or Clippy lints that get fired when that happens.
Seeing how a type can implement a trait
at most once, your third example could be (ab)used to somewhat mimic implementing a trait
multiple times where I just use different types for X
but keep everything else the same. For example:
impl<T: AsRef<[u8]>> MyParser3<u32, T> for u64 {
fn parse_this2(self, _: T) -> String {
"u32 is not the same as u64, so no conflict!".to_owned()
}
}
impl<T: AsRef<[u8]>> MyParser3<u64, T> for bool {
fn parse_this2(self, _: T) -> String {
"u64 is not the same as bool, so no conflict!".to_owned()
}
}