In my project so far, I use many traits to permit mocking/stubbing in unit tests for injected dependencies. However, one detail of what I'm doing so far seems so suspicious that I'm surprised it even compiles. I'm worried that something dangerous is going on that I don't see or understand. It's based on the difference between these two method signatures:
fn confirm<T>(subject: &MyTrait<T>) ...
fn confirm<T>(subject: impl MyTrait<T>) ...
I only just discovered the impl ...
syntax in method arguments, and it seems like the only documented way to do this, but my tests pass using the other way already, which I came to by intuition based on how Go solves the same problem (size of method argument at compile time, when argument can be any implementer of an interface, and references can come to the rescue).
What is the difference between these two? And why are they both allowed? Do they both represent legitimate use cases, or is my reference syntax (&MyTrait<T>
) strictly a worse idea?
The two are different, and serve different purposes. Both are useful, and depending on circumstances one or the other may be the best choice.
The first case, &MyTrait<T>
, is preferably written &dyn MyTrait<T>
in modern Rust. It is a so-called trait object. The reference points to any type implementing MyTrait<T>
, and method calls are dispatched dynamically at runtime. To make this possible, the reference is actually a fat pointer; apart from a pointer to the object it also stores a pointer to the virtual method table of the type of the object, to allow dynamic dispatch. If the actual type of your object only becomes known at runtime, this is the only version you can use, since you need to use dynamic dispatch in that case. The downside of the approach is that there is a runtime cost, and that it only works for traits that are object-safe.
The second case, impl MyTrait<T>
, denotes any type implementing MyTrait<T>
again, but in this case the exact type needs to be known at compile time. The prototype
fn confirm<T>(subject: impl MyTrait<T>);
is equivalent to
fn confirm<M, T>(subject: M)
where
M: MyTrait<T>;
For each type M
that is used in your code, the compiler creates a separate version of confim
in the binary, and method calls are dispatched statically at compile time. This version is preferable if all types are known at compile time, since you don't need to pay the runtime cost of dynamically dispatching to the concrete types.
Another difference between the two prototypes is that the first version accepts subject
by reference, while the second version consumes the argument that is passed in. This isn't a conceptual difference, though – while the first version cannot be written to consume the object, the second version can easily be written to accept subject
by reference:
fn confirm<T>(subject: &impl MyTrait<T>);
Given that you introduced the traits to facilitate testing, it is likely that you should prefer &impl MyTrait<T>
.
It is indeed different. The impl
version is equivalent to the following:
fn confirm<T, M: MyTrait<T>>(subject: M) ...
so unlike the first version, subject
is moved (passed by value) into confirm
, rather than passed by reference. So in the impl
version, confirm
takes ownership of this value.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With