Trait objects and polymorphism in Rust

Trait objects – it is a way to implement abstract data types. So we can group different types, united by common properties or functionality. Let’s say we have different structures, each of which represents some kind of geometric figure. They are different, but they all have a common method for calculating area. We define Trait Shape with method area, and then implement this Trait for each of the structures. Trait objects in Rust allow you to use polymorphism right at runtime!

Encapsulation using Trait objects allows you to hide implementation details. Returning to our geometric shapes example, when we use Trait objects, the user of our code only sees the method area, but not the details of how each shape calculates its area. And polymorphism here acts as a way to use the same interface (in our case, the method area) for various data types.

Interchangeability of Trait objects – this is when the same code can work with different data types that implement a certain Trait. We can easily extend our code by adding new types that implement the desired Trait, without having to change existing code.

For example, if we wanted to add another shape, say a trapezoid, to our program, we would simply need to implement Trait Shape for this new structure. And all the functions that worked with Shapewill automatically be able to work with trapezoids.

Static and dynamic dispatching

Static dispatch means that the decision about which function implementation to call is made at compile time rather than at program runtime.

Static dispatch via parametric polymorphism

In Rust, parametric polymorphism is achieved through the use of generics. This way you can write functions or data structures that can operate on any type of data. Eg fn generic_function<T>(arg: T) { ... }. T can be any type, and the function will operate on that type without knowing in advance what it will be.

Monomorphization is the process that Rust uses to implement parametric polymorphism. This means creating specific copies of functions or structures for each unique data type used.

If there is a function generic_function<T>(arg: T) and it is called with i32 And f64the Rust compiler will create two versions of this function – one for i32 and one for f64.

IN In the static approach, Rust generates unique functions for each data type that is used with the function. This improves performance because the decision about which function to call is made at compile time. But, yeahIf the same generic function is used with many different types, this can result in a larger compiled binary file because each variant of the function will be represented separately.

Example implementation:

trait Walkable {
    fn walk(&self);
}

struct Cat;
struct Dog;

impl Walkable for Cat {
    fn walk(&self) {
        println!("Cat is walking");
    }
}

impl Walkable for Dog {
    fn walk(&self) {
        println!("Dog is walking");
    }
}

fn generic_walk<T: Walkable>(t: T) {
    t.walk();
}

generic_walk is a generic function that takes an argument Timplementing Trait Walkable. At compile time, Rust will create versions of this function for each type Cat And Dogwhich is used to call generic_walk.

Dynamic dispatch

Dynamic dispatch refers to a mechanism in which method calls are resolved during program execution rather than at compile time. This contrasts with static dispatch, where method calls are resolved at compile time

Trait objects allow the use of pointers to data that implement a specific Trait, without specifying a specific data type. This is a must-have when you need to work with a collection of objects of different types, but which implement a common Trait.

Worddyn is an explicit way to indicate that a variable or parameter should be processed using dynamic dispatch.

Example implementation:

trait Walkable {
    fn walk(&self);
}

struct Cat;
struct Dog;

impl Walkable for Cat {
    fn walk(&self) {
        println!("Cat is walking");
    }
}

impl Walkable for Dog {
    fn walk(&self) {
        println!("Dog is walking");
    }
}

fn dynamic_dispatch(w: &dyn Walkable) {
    w.walk();
}

fn main() {
    let cat = Cat{};
    let dog = Dog{};

    dynamic_dispatch(&cat);
    dynamic_dispatch(&dog);
}

dynamic_dispatch accepts a reference to any type that implements Walkable. Unlike static dispatch, a concrete implementation walk is determined at runtime, not at compile time.

IN In the dynamic approach, the specific function to call is determined during program execution, which is usually slower than in the static approach. The main reason is the need to look up the corresponding function in the virtual function table (vtable) at runtime. ABOUTHowever, the dynamic approach uses memory more efficiently because it does not require creating multiple copies of the function for different types.

Trait Bounds

Trait Bounds, or restrictions on traitsallow you to set restrictions on the data types used in generics, ensuring that they implement certain traits

A trait constraint is used to specify that a type must implement a specific set of behaviors. For example, if there is a function that works with types that implement the trait Displayyou can specify this constraint using the syntax <T: Display>. This way the types passed to the function can be mapped.

Trait Bounds can be applied not only to functions, but also to structures and enumerations. An example would be the structure Printer<T: Display>Where T limited by trait Display. Printer can only be created with types that implement Display.

Word where provides an alternative syntax for specifying trait constraints. For example, in a function that compares two values, fn compare<T, U>(a: T, b: U) where T: PartialOrd + Display, U: PartialOrd + Display, where allows you to clearly separate trait constraints from function parameters.

Polymorphism using Enums

Enums allow us to determine a type that can take one of several different forms. Each form can include data and even have different data types.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

Message is an Enum that represents four different message types. Each option can store different data.

fn process_message(msg: Message) {
    match msg {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move to x: {}, y: {}", x, y),
        Message::Write(text) => println!("Text message: {}", text),
        Message::ChangeColor(r, g, b) => println!("Change color to RGB: {}, {}, {}", r, g, b),
    }
}

process_message(Message::Write(String::from("Hello, Rust!")));

process_message uses pattern matching to handle different Enum variants Message. Each option is handled differently.

Enums can be combined with trait objects to create more complex polymorphic structures:

trait Draw {
    fn draw(&self);
}

struct Circle {
    radius: f64,
}

impl Draw for Circle {
    fn draw(&self) {
        // рисование круга
    }
}

struct Square {
    side: f64,
}

impl Draw for Square {
    fn draw(&self) {
        // рисование квадрата
    }
}

enum Shape {
    Circle(Circle),
    Square(Square),
}

impl Draw for Shape {
    fn draw(&self) {
        match self {
            Shape::Circle(c) => c.draw(),
            Shape::Square(s) => s.draw(),
        }
    }
}

Shape is an Enum which can be either Circleor Square. Each of these types implements the trait Draw. Let’s implement Draw for the most Shapewhich allows you to call draw on copies Shapewithout knowing what specific type it contains.


Enums and Traits are used to implement polymorphism, but each is suitable for different scenarios. Enums, which contain different variants of data, are great for situations where all possible forms of data are known in advance and efficient use of memory is required.

On the other hand, Traits offer very good flexibility, allowing you to create data structures that can contain any type that implements a particular Trait.

Overall, the choice between Enums and Traits depends on the flexibility and memory requirements of your application.

My friends from OTUS talk more about Rust and other programming languages ​​in practical online courses. For a complete list of courses you can check out the catalog.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *