Like struct, trait defines a new type composed of named fields and methods. For example:
trait Id: id u32 fn print() // No implementation provided
Traits differ from structs in two important ways:
- It is not possible to use a constructor of this type (e.g., imm node = Node is illegal).
- A trait may define methods that have no implementation logic, as the print method demonstrates.
Traits are versatile, facilitating many useful roles:
- Mixins, where the trait's default methods and/or fields may be added to some other struct or trait type.
- Trait-based variants, enabling definition of variant structs that share some base trait's common fields and methods.
- Virtual reference interface, enabling method use and field access on values whose type is decided at runtime.
- Generic constraints, to restrict the types a generic type or function can safely support, when instantiated.
Traits as mixins
This page focuses on the mixin role of traits. This is effectively a different form of inheritance than field-based delegated inheritance. When a trait is used as a mixin by some other struct or trait, its fields and methods are added to that type as if part of the type.
Mixing in Fields
Suppose we have a trait that defines some fields:
trait LexerInfo: line i32 col i32
A struct may include these fields as part of its definition by using mixin:
struct Node: type u8 mixin LexerInfo count i32
The end result is that the Node struct ends up with four fields, with 'line' and 'col' located in between 'type' and 'count'. 'line' and 'col' are accessible the same way as 'type' and 'count', just as if they were explicitly declared by Node. No two fields may share the same name, no matter where they come from.
A struct may mix in multiple traits. Similarly, trait definitions may mix in one or more traits.
Mixing in Default Methods
In addition to its fields, a struct will also mix in the trait's methods. However, the mechanism here is slightly different. Any method implemented by a trait mixin will only be inherited by the struct if it has not already been implemented by the struct. This is done by matching the number and types of all method parameter types. Because the struct can effectively override the mixin trait's implementation of a method, we call the trait's implementation a default method.
Importantly, if a trait mixin includes methods without an implementation, the struct must define and implement these methods.
Traits, like structs, may specify Self as an alias for the trait type itself. This is the default type used for the self parameter. When a trait is mixed into a struct, Self is understood to refer to that struct. Where this behavior is not desired, one should use the trait's name instead of Self.
Knowing when to choose Self vs. the trait's name depends on what other roles the trait may play. Typically, self on every method should use Self and everywhere else (field types and other method parameters) should use the trait's name. This makes the trait suitable for virtual references and generic bounds.
A marker trait defines no fields nor methods. Its value comes from other trait or struct types inheriting it. Its use then becomes a testable attribute of that type (e.g., by generics), allowing one to distinguish between types that support a certain behavior and those that don't.