struct TheBase;
impl Base for TheBase {
}
trait Derived<'a>: Base {
fn e(&self) {
println!("e from derived");
}
fn get_base(self) -> &'a dyn Base;
}
fn call_derived(d: &dyn Derived) {
d.e();
d.a();
call_base(d.get_base());
}
fn call_base(b: &dyn Base) {
b.a();
}
fn call_base2(b: &dyn Derived) {
b.a();
}
struct S {
base: TheBase,
}
impl Base for S {
}
impl<'a> Derived<'a> for S {
fn get_base(self) -> &'a dyn Base {
&self.base
}
}
struct B;
impl Base for B {}
I am trying to deal with the fact that I cannot up cast a trait to another trait.
So, I created the method get_base to access the base trait. I am perplexed. I don't know how I can deal with the fact that I cannot up cast like all the other languages I know.
How do you get around this problem? Isn't this limiting what you can do in Rust?
Trait upcasting is coming but the whole thing looks suspiciously like an attempt to emulate OOP. That's possible, but 99% of time bad idea. Mistake 6.a: Don't look for tricks to simulate inheritance. Rust isn't designed this way and it doesn't help to try to make it OOP. It will most notably lead you to ownership problems as OOP designs usually require keeping many references to various parts of the data.
In some cases you have to emulate OOP and, sometimes, it's even the right thing to do (e.g. if you have to support externally-imposed API that is built around OOP), but most of the time that's really bad idea which just would force you to find a ways to break all guarantees that Rust provides.
The end result is code that is hard to read, hard to support and hard to answer the question: why the heck you have bothered to use Rust if some other, OOP-based language would have worked much better?
I have an interest in Rust because it produces fast native code. I also like Kotlin, but it takes a lot of memory, just like Java. Otherwise, there is Swift, but it is more an Apple thing.
So, how should I think? Because I am used to OOP and I thought that instead of using inheritance, in Rust we would use protocols with traits, delegates, and composition. No?
Yes and no. Yes, in Rust you are using traits and composition, but you rarely build large, complex, hierarchies of complex, optionally-related traits.
Usually you have a type that there are dozens of traits implemented for it, but you don't need or want to go from one trair for another!
Instead of designing your program from isolated pieces that can be flexibly connected in bazillion different fashions (which then leads to endless bugfixing sessions since nothing prevents you from connecting them incorrectly) you only have concrete types and interfaces (traits) that they are used through. Most of the time not even via dyn Trait but via impl Trait.
And when your interfaces are not working… you fix them. And compiler guides you in changing the code to follow “new vision”.
It's stark contrast to usual OOP-thinking where code is often looked upon as “extendable but not modifiable”. And where you add layers upon layers of patches on top of design mistakes that you have made years ago.
That's why trait upcasting can still be an experimental feature which is almost but not yet fully ready to be stabilized almost 10 years after Rust 1.0 release, rather than something you use every day: you don't need trait upcasting if you don't need to upcast to or from!
If you really, desperately need arbitrary trait object casting, you can implement it the same way COM does—using a query method that, given a marker value representing a target trait, returns the "object" cast to an appropriate dynamic trait object.
I'd post code for it, but I can't remember where I used this last, but I remember macros and unsafe code. Better to just avoid needing it, if possible.
Rust traits rarely follow OOP-inheritance-like hierarchies. Traits are not used to create a taxonomy of types; ideally, a trait describes a single operation you can do with a particular type of value. Those things are often abstractly independent from each other even when they're used together. So, often, there is nothing to want to upcast to.
When you design traits, think first about “what does this trait allow me to do with its implementing types”, and avoid adding any constraints that aren't about that operation.
Traits are much more often used as bounds on generics rather than as trait objects. In generic code, there is no restriction against “upcasting” — because the analogous thing is just not using a bound, which is of course fine.
Trait objects as currently implemented are fairly restrictive, but most Rust code doesn't need trait objects. Your example code will compile fine if you replace dyn with generics and discard get_base entirely:
Your real situation may of course be more complex than this, but in general: try using generics before you try using trait objects. (With practice, you’ll be able to predict when you do need to use trait objects, and design within that constraint when necessary.)
All that said, yes, the lack of trait object upcasting can be a problem, which is why it's being worked on.
I'm confused by this claim. If I comment out the impl, I get:
error[E0277]: the trait bound `(): Subtrait` is not satisfied
--> src/main.rs:16:30
|
16 | let quz: &dyn Subtrait = &();
| ^^^ the trait `Subtrait` is not implemented for `()`
|
help: this trait has no implementations, consider adding one
...
And if I comment out both lines, I get:
error[E0405]: cannot find trait `Subtrait` in this scope
--> src/main.rs:16:19
|
16 | let quz: &dyn Subtrait = &();
| ^^^^^^^^ not found in this scope
Are you sure you included the fn main() part of the example, where everything is actually used?
In the example, we had Subtrait: Foo + Bar and wanted to upcast from dyn SubTrait to dyn Foo or dyn Bar. In your case, we have Derived: Base and want to upcast from dyn Derived to dyn Base. The only difference is that in the example there were two supertraits, whereas in your case there is one supertrait.
The trait hierarchy in my playground looks like so:
Derive <: Base <: AsDynBase
And this is the difference from your playground that works to my playgound:
-trait Base {
+trait Base: AsDynBase {
fn a(&self) {
println!("a from base");
}
}
-trait Derived: AsDynBase + Base {
+trait Derived: Base {
fn e(&self);
}
trait AsDynBase { fn as_dyn_base(&self) -> &dyn Base; }
-impl<T: Derived> AsDynBase for T {
+impl<T: Base> AsDynBase for T {
fn as_dyn_base(&self) -> &dyn Base { self }
}
So your trait hierarchy is like so:
Derive <: Base
Derive <: AsDynBase
But the way it basically works is the same in both cases:
You supply the implementation of AsDynBase for all T: Sized + ImmediateSubTrait, whether the ImmediateSubTrait is Derive (your playground) or Base (mine)
The compiler supplies the implementation of AsDynBase for dyn AsDynBase and for dyn SubTrait for every subtrait, transitively
In your playground, dyn Derive
In mine, dyn Base and dyn Derive
And in case it wasn't clear, the reason this works in your playground:
impl<T: Derived> AsDynBase for T {
fn as_dyn_base(&self) -> &dyn Base { self }
}
Is because T: Derived implies T: Base due to the trait Derived: Base supertrait bound.