Struct Embedding in Rust

Rust supports composition of types through a feature called “trait inheritance”. This is similar to struct embedding in other languages, but with some key differences. Let’s explore how this works in Rust.

use std::fmt;

struct Base {
    num: i32,
}

impl Base {
    fn describe(&self) -> String {
        format!("base with num={}", self.num)
    }
}

// A `Container` composes a `Base`. In Rust, this is done through composition
// rather than embedding.
struct Container {
    base: Base,
    str: String,
}

fn main() {
    // When creating structs in Rust, we initialize the fields explicitly.
    let co = Container {
        base: Base { num: 1 },
        str: String::from("some name"),
    };

    // In Rust, we need to access the base's fields through the `base` field of `Container`.
    println!("co={{num: {}, str: {}}}", co.base.num, co.str);

    // We can also spell out the full path using the field name.
    println!("also num: {}", co.base.num);

    // To call methods from `Base`, we need to access it through the `base` field.
    println!("describe: {}", co.base.describe());

    // In Rust, we use traits to define interfaces.
    trait Describer {
        fn describe(&self) -> String;
    }

    // We can implement traits for our structs.
    impl Describer for Container {
        fn describe(&self) -> String {
            self.base.describe()
        }
    }

    // Now we can use `Container` as a `Describer`.
    let d: &dyn Describer = &co;
    println!("describer: {}", d.describe());
}

To run the program:

$ rustc struct_composition.rs
$ ./struct_composition
co={num: 1, str: some name}
also num: 1
describe: base with num=1
describer: base with num=1

In this Rust example, we’ve demonstrated how to achieve similar functionality to struct embedding using composition and trait implementation. Rust’s approach provides more explicit control over the structure and behavior of types, while still allowing for flexible composition of functionality.

The Container struct contains a Base as a field, rather than embedding it directly. To access the num field or call the describe method of Base, we need to go through the base field of Container.

We’ve also shown how to implement a trait (Describer) for Container, which allows us to use Container wherever a Describer is expected. This is similar to how embedding in other languages can automatically implement interfaces.

Rust’s approach to composition and interfaces (traits) provides strong type safety and explicit control, while still allowing for flexible and reusable code structures.