2.11.1. Trait Implementations
Rust's coherence rules forbid multiple implementations of the same trait for the same type.
In general, the following trait-related operations are breaking changes:
- Adding a blanket implementation to an existing trait (see 1.17.2. Blanket Implementations) is usually a breaking change
- Implementing an external trait for an existing type, or implementing an existing trait for an external type
- Removing a trait implementation (implementing a trait for a new type does not cause a breaking change)
Most changes to an existing trait are also breaking changes, for example:
- Changing the signature of an existing trait method
- Adding a new method (if the new method has a default implementation, it is not a breaking change)
Be Careful When Implementing Any Trait for Any Type
A quick reminder: be careful when implementing any trait for any type.
Example:
lib.rs:
pub struct Unit;
// Define trait
pub trait Foo1 {
fn foo(&self);
}
impl Foo1 for Unit {
fn foo(&self) {
println!("foo1");
}
}
main.rs:
use constrained::{Foo1, Unit};
// Define trait
trait Foo2 {
fn foo(&self);
}
// Implement Foo2 for Unit
impl Foo2 for Unit {
fn foo(&self) {
println!("foo2");
}
}
// Run the main function
fn main() {
Unit.foo();
}
Output:
error[E0034]: multiple applicable items in scope
--> src/main.rs:14:10
|
14 | Unit.foo();
| ^^^ multiple `foo` found
|
= note: candidate #1 is defined in an impl of the trait `Foo1` for the type `Unit`
note: candidate #2 is defined in an impl of the trait `Foo2` for the type `Unit`
--> src/main.rs:8:5
|
8 | fn foo(&self) {
| ^^^^^^^^^^^^^
help: disambiguate the method for candidate #1
|
14 - Unit.foo();
14 + Foo1::foo(&Unit);
|
help: disambiguate the method for candidate #2
|
14 - Unit.foo();
14 + Foo2::foo(&Unit);
|
This code will fail to compile. Do you see where the error is? The problem is the foo method. main.rs and lib.rs each define a Foo2 and Foo1 trait, and both traits have a foo method. The Unit struct implements both Foo1 and Foo2. When foo is used in main.rs, the compiler does not know which trait's foo method it should use.
That is why you must be careful when implementing any trait for any type—implementing a trait can accidentally cause breaking changes.
Sealed Traits
Earlier, I kept saying “most of the time” and “in general,” because Rust has sealed traits.
Their characteristic is that they can be used by other crates, but cannot be implemented in other crates. They can prevent breaking changes when new methods are added to a trait.
Sealed traits are not a built-in language feature; there are several ways to implement them.
Sealed traits are often used for derived traits. More specifically, they are traits that provide blanket implementations for types that implement certain other traits.
Example:
mod sealed {
pub trait Sealed {} // private trait, not exposed publicly
}
// Only `i32` and `f64` can implement `MyTrait`
impl sealed::Sealed for i32 {}
impl sealed::Sealed for f64 {}
pub trait MyTrait: sealed::Sealed {
fn describe(&self) -> String;
}
// Blanket implementation: only `Sealed` implementers can use `MyTrait`
impl MyTrait for i32 {
fn describe(&self) -> String {
format!("I am an i32: {}", self)
}
}
impl MyTrait for f64 {
fn describe(&self) -> String {
format!("I am an f64: {}", self)
}
}
// Test
fn main() {
let x: i32 = 42;
let y: f64 = 3.14;
println!("{}", x.describe()); // output: I am an i32: 42
println!("{}", y.describe()); // output: I am an f64: 3.14
}
-
Sealedis private (because it lives insidemod sealed), so other crates cannot use it, which achieves the sealing goal - Only
i32andf64are allowed to implementSealed
The above is a relatively simple example. Now let us bring in a derived trait:
Use
Sealedas a sealed trait to restrictBaseTraitso that only certain types can implement it.
DeriveDerivedTrait, make it inheritBaseTrait, and provide additional behavior.
mod sealed {
pub trait Sealed {} // private trait, not exposed publicly
}
// Only `i32` and `f64` can implement `BaseTrait`
impl sealed::Sealed for i32 {}
impl sealed::Sealed for f64 {}
/// Base trait, implementable only by types that implement `sealed::Sealed`
pub trait BaseTrait: sealed::Sealed {
fn base_method(&self) -> String;
}
// Blanket implementation for BaseTrait
impl BaseTrait for i32 {
fn base_method(&self) -> String {
format!("I am an i32: {}", self)
}
}
impl BaseTrait for f64 {
fn base_method(&self) -> String {
format!("I am an f64: {}", self)
}
}
/// Derived trait that extends `BaseTrait`
pub trait DerivedTrait: BaseTrait {
fn derived_method(&self) -> String;
}
// Blanket implementation for DerivedTrait
impl DerivedTrait for i32 {
fn derived_method(&self) -> String {
format!("Derived trait: {} squared = {}", self, self * self)
}
}
impl DerivedTrait for f64 {
fn derived_method(&self) -> String {
format!("Derived trait: sqrt({}) = {}", self, self.sqrt())
}
}
fn main() {
let x: i32 = 5;
let y: f64 = 9.0;
println!("{}", x.base_method()); // "I am an i32: 5"
println!("{}", x.derived_method()); // "Derived trait: 5 squared = 25"
println!("{}", y.base_method()); // "I am an f64: 9"
println!("{}", y.derived_method()); // "Derived trait: sqrt(9) = 3"
}
-
BaseTraitcannot be implemented by external types; it can only be used fori32andf64, because it inherits fromsealed::Sealed -
DerivedTraitextendsBaseTraitand addsderived_method() -
BaseTraitandDerivedTraitare implemented only fori32andf64; external types cannot implement these traits
When should you use sealed traits? Only when external crates should not be able to implement your trait. This form severely limits the usability of the trait—downstream traits cannot implement it for their own types.
We can use sealed traits to restrict which types can be used as type parameters. Remember the Rocket struct we wrote earlier? (in 2.9.3. The Type System) The Stage generic parameter of Rocket was restricted to only Grounded and Launched using this approach.
2.11.2. Hidden Contracts
Sometimes, changes you make to one part of the code can subtly affect the contract of other parts of the interface.
This mainly happens with:
- Re-exports
- Auto-traits
Re-exports
If part of your interface exposes an external type, then any changes to that external type also become changes to your interface.
It is usually better to wrap the external type in a newtype and expose only the parts of the external type that you consider useful.
Auto-Traits
Some traits, based on the contents of a type, are implemented for it automatically, such as Send and Sync. Because of their nature, these traits add a hidden promise to almost every type in an interface.
These traits propagate, whether the type is concrete or type-erased through things like impl Trait.
Implementations of these traits are usually added automatically by the compiler, and if the situation does not apply, they are not added automatically.
For example:
- Type A contains private type B, and by default both A and B implement
Send - Later, B is changed so that it no longer implements
Send, and then A also stops implementingSend - That kind of change is breaking, and it is also very hard to trace and discover
For this kind of problem, you can include a few simple tests in your library to check whether all of your types implement the relevant traits.
Example:
This is the original code:
use std::thread;
/// 1. Private type B, initially `Send`
struct B;
/// 2. Public type A, containing B
struct A {
_b: B, // depends on B's traits
}
// 3. Prove that `A` is `Send`
fn assert_send<T: Send>() {}
fn main() {
assert_send::<A>(); // passes, A is Send
// 4. Prove that A can be safely passed between threads
let a = A { _b: B };
thread::spawn(move || {
let _ = a; // runs successfully because A is still Send
}).join().unwrap();
}
Then we modify B so that it no longer implements the Send trait:
use std::rc::Rc;
use std::thread;
/// 1. Modify `B` so that it is no longer `Send`
/// `Rc<T>` is not `Send`, so `B` is not `Send` either
struct B {
_data: Rc<i32>,
}
/// 2. A still contains B
struct A {
_b: B,
}
// 3. Prove that `A` is `Send`
fn assert_send<T: Send>() {}
fn main() {
assert_send::<A>(); // compile error[E0277]: `Rc<i32>` cannot be sent between threads safely (so `A: Send` fails)
let a = A { _b: B { _data: Rc::new(42) } };
thread::spawn(move || {
let _ = a; // this will fail because `Rc<i32>` cannot be safely sent across threads
}).join().unwrap();
}
-
Bnow containsRc<T>, butRc<T>is notSend. That means B is no longerSend, becauseRc<T>cannot be safely transferred between threads -
A is no longer
Sendeither, which makesassert_send::<A>()fail to compile. We can detect the error at compile time
Top comments (0)