Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”. This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.


Let’s look at the following code:

trait Animal {
   fn talk(&self);
}
struct Cat {}
struct Dog {}

impl Animal for Cat {
    fn talk(&self) {
            println!(" meow "); }}impl Animal for Dog {
    fn talk(&self) {
            println!(" bark "); }}// dyn method
fn animal_talk(a: &dyn Animal) {
    a.talk();
}

fn main() {
    let d = Dog {};
    let c = Cat {};
    animal_talk(&d);
    animal_talk(&c);
}
Copy the code

You can see that animal_talk() is called for both cat and dog. ** Dyn ** tells the compiler to settle for a reference to a type that implements an animal trait instead of determining the exact type. This is called dynamic dispatch, where the type is determined at run time, so there is a run time overhead.

But why was there an & before Dyn? “Pointer to a property?” That’s right! Because the type (and the size of the specific type) is not known at compile time; It is illegal to simply use:

fn animal_talk(a: dyn Animal) {
   a.talk();
}
Copy the code

The compiler reports an error: unknown size. Rust calls it a trait object (& Dyn Animal). It represents a pointer to a concrete type and a vtable pointer to a function pointer. Box

and Rc

are also trait objects. They also contain a pointer to the concrete type allocated on the heap that satisfies the given trait. Now, let’s look at the following code:

fn animal_talk(a: impl Animal) {
    a.talk();
}

fn main() {
    let c = Cat{};
    let d = Dog{};

    animal_talk(c);
    animal_talk(d);
}
Copy the code

The code above compiles to OK, and there is no ampersand. Impl lets the compiler determine the type at compile time, which means the compiler will do some function name processing and there will be variations of two functions: one is dog and the other is MAT. This is called singomorphism and has no run-time overhead, but causes code bloat (the same code being implemented multiple times). Consider the following code, will this compile?

fn animal () - >impl Animal {
    if (is_dog_available()) {
        return Dog {};
    }
    Cat {}
}
Copy the code

Compilation failed! This is because the type is determined at compile time (statically distributed). This error is self-evident:

error[E0308]: mismatched types
   |
   | fn animal() - >impl Animal {
   |                                   ----------- expected because this return type. |if (is_dog_available()) {
   |         returnDog {}; | -- -- -- -- -- -... is found to be `Dog` here | } | ... | Cat {} | ^^^^^^ expectedstruct `Dog`, found struct `Cat`
   |
   = note: expected type `Dog`
              found type `Cat`
Copy the code

Here the compiler first zeroes out Dog as a return type and expects other return types in the function body to be of the same type. What about the code below?

fn animal() - >Box<dyn Animal> {
    if (is_dog_available()) {
        return Box::new(Dog {});
    } 

    Box::new(Cat {})
}
Copy the code

The compile result is now OK because it does not need to know the exact return type (dynamic scheduling). All it takes is some Trait Object (Box) type that satisfies a given Trait.

Object security

If a trait is in its method, the type that implements it as a value (rather than as a reference) is considered an unsafe object. Consider the following code:

trait Animal {
    fn nop(self) {}}struct Cat {}
impl Animal for Cat {}
	
fn main() {
    let c = Cat {};
    let ct:&dyn Animal = &c;
    c.nop();
}
Copy the code

The following errors will occur:

error[E0277]: the size for values of type `Self` cannot be known at compilation time
  |
  |     fn nop(self)  {}
  |            ^^^^ - help: consider further restricting `Self` : `where Self: std::marker::Sized`
  |            |
  |            doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `Self`
Copy the code

Nop () receives the transmitted value. But since this is distributed dynamically, the type and its size are not known. Even if we Sized Self with a constraint, fn nop(Self) where Self:Sized {}, the compiler still reported an error because there was no way to know the size at compile time, and we introduced a dependency on Self into our code (passing it as a value).