“ dyn”在一个类型中是什么意思?

我最近看到了使用 dyn关键字的代码:

fn foo(arg: &dyn Display) {}


fn bar() -> Box<dyn Display> {}

这种语法是什么意思?

16541 次浏览

TL;DR: It's a syntax for specifying the type of a trait object and must be specified for clarity reasons.


Since Rust 1.0, traits have led a double life. Once a trait has been declared, it can be used either as a trait or as a type:

// As a trait
impl MyTrait for SomeType {}


// As a type!
impl MyTrait {}
impl AnotherTrait for MyTrait {}

As you can imagine, this double meaning can cause some confusion. Additionally, since the MyTrait type is an unsized / dynamically-sized type, this can expose people to very complex error messages.

To ameliorate this problem, RFC 2113 introduced the dyn syntax. This syntax is available starting in Rust 1.27:

use std::{fmt::Display, sync::Arc};


fn main() {
let display_ref: &dyn Display = &42;
let display_box: Box<dyn Display> = Box::new(42);
let display_arc: Arc<dyn Display> = Arc::new(42);
}

This new keyword parallels the impl Trait syntax and strives to make the type of a trait object more obviously distinct from the "bare" trait syntax.

dyn is short for "dynamic" and refers to the fact that trait objects perform dynamic dispatch. This means that the decision of exactly which function is called will occur at program run time. Contrast this to static dispatch which uses the impl Trait syntax.

The syntax without dyn is now deprecated and it's likely that in a subsequent edition of Rust it will be removed.

The dyn keyword is used to indicate that a type is a trait object. According to the Rust docs:

A trait object is an opaque value of another type that implements a set of traits.

In other words, we do not know the specific type of the object at compile time, we just know that the object implements the trait.

Because the size of a trait object is unknown at compile time they must be placed behind a pointer. For example, if Trait is your trait name then you can use your trait objects in the following manner:

  • Box<dyn Trait>
  • &dyn Trait
  • and other pointer types

The variables/parameters which hold the trait objects are fat pointers which consists of the following components:

  • pointer to the object in memory
  • pointer to that object’s vtable, a vtable is a table with pointers which point to the actual method(s) implementation(s).

See my answer on What makes something a “trait object”? for further details.

TLDR: "dyn" allows you to store in a Box a mix of Apples and Oranges, because they all implement the same trait of Fruit, which is what your Box is using as a type constraint, instead of just a generic type. This is because Generic allows any ONE of Apple OR Orange, but not both:

Vec<Box<T>> --> Vector can hold boxes of either Apples OR Oranges structs
Vec<Box<dyn Fruit>> --> Vector can now hold a mix of boxes of Apples AND Oranges Structs

If you want to store multiple types to the same instance of a data-structure, you have to use a trait wrapping a generic type and tag it as a "dyn", which will then cause that generic type to be resolved each time it's called, during runtime.

Sometimes, rather than using a type (String, &str, i32, etc...) or generic (T, Vec, etc...), we are using a trait as the type constraint (i.e. TryFrom). This is to allow us to store multiple types (all implementing the required trait), in the same data-structure instance (you will probably need to Box<> it too).

"dyn" basically tells the compiler that we don't know what the type is going to be at compile-time in place of the trait, and that it will be determined at run-time. This allows the final type to actually be a mixture of types that all implement the trait.

For generics, the compiler will hard-code the type in place of our generic type at the first use of the call to our data-structure consuming the generics. Every other call to store data in that same data-structure is expected to be using the same type as in the first call.

WARNING As with all things, there is a performance penalty for implementing added flexibility, and this case definitely has a performance penalty.

I found this blog post to explain this feature really clearly: https://medium.com/digitalfrontiers/rust-dynamic-dispatching-deep-dive-236a5896e49b

Relevant excerpt:

struct Service<T:Backend>{
backend: Vec<T>  // Either Vec<TypeA> or Vec<TypeB>, not both
}
...
let mut backends = Vec::new();
backends.push(TypeA);
backends.push(TypeB);  // <---- Type error here

vs

struct Service{
backends: Vec<Box<dyn Backend>>
}
...
let mut backends = Vec::new();
backends.push( Box::new(PositiveBackend{}) as Box<dyn Backend>);
backends.push( Box::new(NegativeBackend{}) as Box<dyn Backend>);