【Rust中级教程】2.11. API设计原则之受约束性(constrained) Pt.2:封闭trait(sealed trait)、重新导出(re-exports)、自动trait
2.11. API设计原则之受约束性(constrained) Pt.2:封闭trait(sealed trait)、重新导出(re-exports)、自动trait(auto-trait)
2.11.1. trait实现
Rust的一致性规则禁止了对某个trait为某类型进行多重实现。
通常情况下,以下与trait相关的操作会产生破坏性变更:
- 为现有trait添加Blanket Implementation(详见 1.17.2. 泛实现)通常是破坏性变更
- 为现有类型实现外部trait,或为外部类型实现现有trait
- 移除trait实现(为新的类型实现trait不会产生破坏性变更)
大多数到现有trait的更改也是破坏性变更,例如:
- 为现有的trait改变方法的签名
- 添加新方法(如果新方法有默认实现就不产生破坏性变更)
为任何类型实现任何trait都要小心
多提一嘴,为任何类型实现任何trait都要小心。
看个例子:
lib.rs:
pub struct Unit; // 定义 trait pub trait Foo1 { fn foo(&self); } impl Foo1 for Unit { fn foo(&self) { println!("foo1"); } }main.rs:
use constrained::{Foo1, Unit}; // 定义 trait trait Foo2 { fn foo(&self); } // 为 Unit 实现 Foo2 trait impl Foo2 for Unit { fn foo(&self) { println!("foo2"); } } // 运行主函数 fn main() { Unit.foo(); }输出:
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); |这么写是会报错的,注意到错误在哪里了吗?问题出在foo方法,main.rs和lib.rs各有一个Foo2和Foo1trait,而两者都有foo这个方法,Unit结构体既实现了Foo1也实现了Foo2。在main.rs中使用foo方法时编译器就不清楚到底改使用哪个trait上的foo方法。
这就是为什么为任何类型实现任何trait都要小心——实现trait有可能在不经意间造成的破坏性变更。
封闭trait(sealed trait)
刚才我的用词都是“大多数”、“通常情况下”,这是因为Rust有封闭trait(sealed trait)。
它的特点是它只能被其它crate使用,而不能在其它crate中被实现。它可以防止trait添加新方法时造成破坏性的变更
封闭trait(sealed trait)并不是内建的功能,有几种实现方式。
封闭trait(sealed trait)常用于派生trait。具体来讲就是为实现特定其它trait的类型提供blanket implementation的trait。
看例子:
mod sealed { pub trait Sealed {} // 私有 trait,不对外公开 } // 只有 `i32` 和 `f64` 类型可以实现 `MyTrait` impl sealed::Sealed for i32 {} impl sealed::Sealed for f64 {} pub trait MyTrait: sealed::Sealed { fn describe(&self) -> String; } // Blanket Implementation:只有 `Sealed` 的实现者才可以使用 `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) } } // 测试 fn main() { let x: i32 = 42; let y: f64 = 3.14; println!("{}", x.describe()); // 输出: I am an i32: 42 println!("{}", y.describe()); // 输出: I am an f64: 3.14 }Sealed是私有的(因为它在 mod sealed 内),别的crate就不可能用的了它,以此实现了封闭的目的。- 只有
i32和f64被允许实现Sealed
上面的是一个比较简单的例子,我们接下来融入派生trait:
使用
Sealed作为封闭 trait,限制BaseTrait只能被特定类型实现。
派生DerivedTrait,让它继承BaseTrait,并提供额外的行为。
mod sealed { pub trait Sealed {} // 私有 trait,不对外公开 } // 只有 `i32` 和 `f64` 可以实现 `BaseTrait` impl sealed::Sealed for i32 {} impl sealed::Sealed for f64 {} /// 基础 trait,只能被 `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) } } /// 派生 trait,扩展 `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" }BaseTrait不能被外部类型实现,只能用于i32和f64,因为它继承了sealed::SealedDerivedTrait扩展了BaseTrait,并增加了derived_method()BaseTrait和DerivedTrait只对i32和f64提供实现,外部类型无法实现这些 trait
那么什么时候使用封闭trait呢?只有在外部crate不该实现你的trait时。这种形式会严重限制trait的可用性——下游trait无法为其自己类型实现该trait。
我们可以使用密封trait来限制可用作类型参数的类型。还记得我们之前写的Rocket结构体吗?(在 2.9.3. 类型系统 中)Rocket结构体的Stage泛型参数限制为了仅允许Grounded和Launched类型就使用这种方式。
2.11.2. 隐藏的契约
有时,你对代码的某一部分所做的更改会以微妙的方式影响到接口其他地方的契约。
这种情况主要发生在:
- 重新导出(re-exports)
- 自动trait(auto-trait)
重新导出(re-exports)
重新导出(re-exports)的操作在 【Rust自学】14.3.1. 使用pub use导出方便使用的API 中有过介绍。
如果你的接口的某部分暴露了外部类型,那么外部类型的任何更改也将成为你接口的变更。
最好用newtype(详见 【Rust自学】19.2.6. 使用newtype模式在外部类型上实现外部trait)包裹外部类型,仅仅暴露外部类型中你认为有用的部分。
自动trait(auto-trait)
有些trait根据类型的内容,会对其进行实现,比如说Send和Sync。而根据这些trait的特性,它们为接口中几乎每种类型都添加一个隐藏的承诺。
这些特性会传播,无论是具体的类型,还是impl Trait等类型擦除的情况。
这些trait的实现通常是编译器自动添加的,如果情况不适用,则不会自动添加。
举个例子:
- 类型A包含私有类型B,默认A和B都实现了Send。
- 后来修改了B,让B不再实现Send,那么A也就实现不了Send。
- 这样的情况就是破坏性的变化,而且这类变化还非常难以追踪和发现
针对这种问题,你可以在你的库里面包含一些简单的测试来检查你所有的类型是否实现了相关的trait。
举个代码例:
这是原本的代码:
use std::thread; /// 1. 私有类型 B,最初实现了 `Send` struct B; /// 2. 公开类型 A,包含 B struct A { _b: B, // 依赖 `B` 的特性 } // 3. 证明 `A` 实现了 `Send` fn assert_send<T: Send>() {} fn main() { assert_send::<A>(); // 通过,A 是 Send 的 // 4. 证明 A 可以在线程间安全传递 let a = A { _b: B }; thread::spawn(move || { let _ = a; // 运行成功,因为 A 仍然是 Send }).join().unwrap(); }后面我们进行修改,使B不再实现Sendtrait:
use std::rc::Rc; /// 1. 修改 `B`,让它不再实现 `Send` /// `Rc<T>` 不是 `Send`,所以 B 也不是 `Send` struct B { _data: Rc<i32>, } /// 2. A 仍然包含 B struct A { _b: B, } // 3. 证明 `A` 实现了 `Send` fn assert_send<T: Send>() {} fn main() { assert_send::<A>(); // 编译错误[E0277]:`Rc<i32>` cannot be sent between threads safely(因此 `A: Send` 失败) let a = A { _b: B { _data: Rc::new(42) } }; thread::spawn(move || { let _ = a; // 这里会报错,因为 Rc<i32> 不能在线程间安全传递 }).join().unwrap(); }- B现在包含
Rc<T>,但Rc<T>不是Send。这意味着B也不能Send,因为Rc<T>不能安全在线程间传递。 - A也就不再
Send了,导致assert_send::<A>()编译错误。我们就可以在编译时发现错误。
