Merge pull request #94 from llogiq/ootb-dyn-dispatch
New idiom: Out of the Box Dynamic Dispatchpull/99/head
commit
83b6d0dac4
@ -0,0 +1,83 @@
|
||||
# On-Stack Dynamic Dispatch
|
||||
|
||||
## Description
|
||||
|
||||
We can dynamically dispatch over multiple values, however, to do so, we need
|
||||
to declare multiple variables to bind differently-typed objects. To extend the
|
||||
lifetime as necessary, we can use deferred conditional initialization, as seen
|
||||
below:
|
||||
|
||||
## Example
|
||||
|
||||
```rust
|
||||
// These must live longer than `readable`, and thus are declared first:
|
||||
let (mut stdin_read, mut file_read);
|
||||
|
||||
// We need to ascribe the type to get dynamic dispatch.
|
||||
let readable: &mut dyn io::Read = if arg == '-' {
|
||||
stdin_read = io::stdin();
|
||||
&mut stdin_read
|
||||
} else {
|
||||
file_read = fs::File::open(arg)?;
|
||||
&mut file_read
|
||||
};
|
||||
|
||||
// Read from `readable` here.
|
||||
```
|
||||
|
||||
## Motivation
|
||||
|
||||
Rust monomorphises code by default. This means a copy of the code will be
|
||||
generated for each type it is used with and optimized independently. While this
|
||||
allows for very fast code on the hot path, it also bloats the code in places
|
||||
where performance is not of the essence, thus costing compile time and cache
|
||||
usage.
|
||||
|
||||
Luckily, Rust allows us to use dynamic dispatch, but we have to explicitly ask
|
||||
for it.
|
||||
|
||||
## Advantages
|
||||
|
||||
We do not need to allocate anything on the heap. Neither do we need to
|
||||
initialize something we won't use later, nor do we need to monomorphize the
|
||||
whole code that follows to work with both `File` or `Stdin`, with all the
|
||||
|
||||
## Disadvantages
|
||||
|
||||
The code needs more moving parts than the `Box`-based version:
|
||||
|
||||
```
|
||||
// We still need to ascribe the type for dynamic dispatch.
|
||||
let readable: Box<dyn io::Read> = if arg == "-" {
|
||||
Box::new(io::stdin())
|
||||
} else {
|
||||
Box::new(fs::File::open(arg)?)
|
||||
};
|
||||
// Read from `readable` here.
|
||||
```
|
||||
|
||||
## Discussion
|
||||
|
||||
Rust newcomers will usually learn that Rust requires all variables to be
|
||||
initialized *before use*, so it's easy to overlook the fact that *unused*
|
||||
variables may well be uninitialized. Rust works quite hard to ensure that this
|
||||
works out fine and only the initialized values are dropped at the end of their
|
||||
scope.
|
||||
|
||||
The example meets all the constraints Rust places on us:
|
||||
|
||||
* All variables are initialized before using (in this case borrowing) them
|
||||
* Each variable only holds values of a single type. In our example, `stdin` is
|
||||
of type `Stdin`, `file` is of type `File` and `readable` is of type `&mut dyn
|
||||
Read`
|
||||
* Each borrowed value outlives all the references borrowed from it
|
||||
|
||||
## See also
|
||||
|
||||
* [Finalisation in destructors](idioms/dtor-finally.md) and
|
||||
[RAII guards](patterns/RAII.md) can benefit from tight control over lifetimes.
|
||||
* For conditionally filled `Option<&T>`s of (mutable) references, one can
|
||||
initialize an `Option<T>` directly and use its [`.as_ref()`] method to get an
|
||||
optional reference.
|
||||
|
||||
[`.as_ref()`]: https://doc.rust-lang.org/std/option/enum.Option.html#method.as_ref
|
Loading…
Reference in New Issue