[Advanced Rust] 2.11. API Design Principles of Constrained Pt.2 – Sealed Traits, Re-exports, and Auto Traits

작성자

카테고리:

← 피드로
DEV Community · SomeB1oody · 2026-09-03 개발(SW)

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");
    }
}

Enter fullscreen mode Exit fullscreen mode

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();
}

Enter fullscreen mode Exit fullscreen mode

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);
   |

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

  • Sealed is private (because it lives inside mod sealed), so other crates cannot use it, which achieves the sealing goal
  • Only i32 and f64 are allowed to implement Sealed

The above is a relatively simple example. Now let us bring in a derived trait:

Use Sealed as a sealed trait to restrict BaseTrait so that only certain types can implement it.
Derive DerivedTrait, make it inherit BaseTrait, 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"
}

Enter fullscreen mode Exit fullscreen mode

  • BaseTrait cannot be implemented by external types; it can only be used for i32 and f64, because it inherits from sealed::Sealed
  • DerivedTrait extends BaseTrait and adds derived_method()
  • BaseTrait and DerivedTrait are implemented only for i32 and f64; 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 implementing Send
  • 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();
}

Enter fullscreen mode Exit fullscreen mode

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();
}

Enter fullscreen mode Exit fullscreen mode

  • B now contains Rc<T>, but Rc<T> is not Send. That means B is no longer Send, because Rc<T> cannot be safely transferred between threads
  • A is no longer Send either, which makes assert_send::<A>() fail to compile. We can detect the error at compile time

원문에서 계속 ↗