19.2.1 Using Associated Types in Trait Definitions to Specify Placeholder Types
We first introduced traits in 10.3. Trait Pt.1 - Trait Definitions, Bounds, and Implementation, but we did not discuss more advanced details. Now let’s dig deeper.
An associated type is a type placeholder inside a trait. It can be used in trait method signatures. It is used to define traits for some types without needing to know those types in advance.
For example:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
The standard library’s Iterator trait is a trait with an associated type, and its definition looks like the example above.
Item is the associated type. During iteration, Item is used in place of the actual value type so that the logic is separated from the concrete data type. You can see Item in the return type of next: Option<Self::Item>.
Item is a type placeholder. Its core idea is similar to generics, but there are also differences:
| Generics | Associated Types |
|---|---|
| Specify the type each time a trait is implemented | No need to specify the type |
| A single type can implement the same trait multiple times with different generic parameters | A single type cannot implement the same trait multiple times |
19.2.2 Default Generic Type Parameters and Operator Overloading
When using generic parameters, we can give a generic a default concrete type. The syntax is <PlaceholderType=ConcreteType>. This technique is commonly used for operator overloading.
Although Rust does not allow you to create your own operators or overload arbitrary ones, you can overload certain operators by implementing the traits listed in std::ops.
Here is an example:
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
- In this example, we implement the
Addtrait for thePointstruct, which overloads the+operator. Specifically, theaddfunction insideAddadds the fields one by one. - In
main, we can use+directly to add twoPointvalues.
The definition of the Add trait looks like this:
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
It uses a default generic parameter, Rhs=Self. That means when we implement Add, if we do not specify a concrete type for Rhs, the default type is Self. So in the example above, Rhs is Point.
Now let’s look at another example, this time adding millimeters and meters:
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
- Here,
MillimetersandMetersare declared as tuple structs representing millimeters and meters. - We implement
AddforMillimeters, and explicitly specify that the other type isMeters. Inadd, we add the stored millimeter value to the meter value converted into millimeters.
19.2.3 Main Use Cases for Default Generic Parameters
- Extending a type without breaking existing code
- Allowing customization in special cases that most users do not need
19.2.4 Calling Methods with the Same Name Using Fully Qualified Syntax
Let’s go straight to an example:
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
- We define two traits,
PilotandWizard, and each has aflymethod, but no concrete implementation. - We have a
Humanstruct. Below, we implement both traits for it, which means we provide aflymethod for each trait. In addition, we also implement aflymethod for the struct itself in animplblock.
At this point there are three fly methods. If we call this in main:
fn main() {
let person = Human;
person.fly();
}
Running this code prints *waving arms furiously*, which shows that Rust directly calls the fly method implemented on Human.
To call fly from the Pilot trait or the Wizard trait, we need more explicit syntax to specify which fly we mean:
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
Specifying the trait name before the method name tells Rust exactly which fly implementation we want. person.fly() can also be written as Human::fly(&person).
Output:
This is your captain speaking.
Up!
*waving arms furiously*
However, associated functions that are not methods do not have a self parameter. When multiple methods or associated functions from different types or traits have the same name, Rust does not always know which one you mean unless you use fully qualified syntax:
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
- The
Animaltrait has ababy_namefunction.Dogis a struct that implements theAnimaltrait, and it also implementsbaby_namein its ownimplblock. So now there are twobaby_namefunctions. - In
main,Dog::baby_name()is used, so according to the logic above, thebaby_nameimplementation inDog’simplblock runs, producingSpot.
Output:
A baby dog is called a Spot
So how do we call the baby_name method from the Animal trait implementation for Dog? Let’s try the logic from the previous example:
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
Output:
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
The baby_name function on the Animal trait needs to know which type’s implementation to use, but baby_name itself has no parameters, so Rust cannot infer which type’s implementation is meant.
In that case, you need fully qualified syntax. Its form is:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
This syntax can be used anywhere a function or method is called, and it lets you ignore the parts that can be inferred from other context.
But you only need this syntax when Rust cannot distinguish which concrete implementation you want, because it is cumbersome to write. So in general, you should not reach for it unless necessary.
With that syntax, the code above should be changed to:
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
Output:
A baby dog is called a puppy
19.2.5 Using Supertraits to Require Additional Trait Functionality
Sometimes we need to use functionality from another trait inside a trait, which means the indirectly required trait must also be implemented. That indirectly required trait is the current trait’s supertrait.
For example:
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
OutlinePrint is actually used to print a shape in the terminal using characters. But during printing, self must implement to_string, which means self must implement the Display trait (to_string comes from the ToString trait, which is implemented for any type that implements Display). The syntax is trait keyword + trait name + : + supertrait.
Suppose we have a Point struct and want to print it in the terminal using OutlinePrint’s outline_print method. Because OutlinePrint requires Display, we must implement both OutlinePrint and Display, or it will fail:
struct Point {
x: i32,
y: i32,
}
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl OutlinePrint for Point {}
19.2.6 Using the Newtype Pattern to Implement an External Trait on an External Type
We already discussed the orphan rule: you can only implement a trait for a type if either the trait or the type is defined in your local crate. We can use the newtype pattern to work around this rule, specifically by building a new type locally with a tuple struct.
For example:
Suppose we want to implement Display for Vec<String>, but both Vec and Display are defined outside our crate, so we cannot implement it directly for Vec<String>. Instead, we wrap the vector in our own tuple struct Wrapper, and then implement Display for Wrapper:
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {w}");
}
Top comments (0)