- Rust API 指南:https://rust-lang.github.io/api-guidelines/checklist.html
- 中文讲解地址:https://www.bilibili.com/video/BV1Pu4y1Z7dT/?spm_id_from=333.337.search-card.all.click&vd_source=2294cb13a82ee07684c6d3607932c747
四个原则:
- 不意外(unsurprising)
- 灵活(flexible)
- 显而易见(obvious)
- 受约束(constrained)
最少意外原则:
- 接口应尽可能直观(可预测,用户能猜对)
- 至少应该不让人感到惊奇
核心思想:
- 贴近用户已经知道的东西(不必重学概念)
让接口可预测
- 命名
- 实现常用的 Trait
- 人体工程学(Ergonomic)Trait
- 包装类型(Wrapper Type)
- 接口的名称,应符合惯例,便于推断其功能。例如:
- 方法
iter
,大概率应将&self
作为参数,并应该返回一个迭代器(iterator
) - 叫做
into_inner
的方法,大概率应将self
作为参数,并返回某个包装的类型 - 叫做 SomethingError 的类型,应实现
std::error::Error
,并出现在各类Result
里
- 方法
- 将通用/常用的名称依然用于相同的目的,让用户好猜、好理解
- 推论:同名的事物应该以相同的方式工作
- 否则,用户大概率会写出错误的代码
- 用户通常会假设接口中的一切均可“正常工作”,例:
- 使用
{:?}
打印任何类型 - 可发送任何东西到另外的线程
- 期望每个类型都是 Clone 的
- 使用
- 建议积极实现大部分标准 Trait,即使不立即用到
- 用户无法为外部类型实现外部的 Trait
- 即使能包装你的接口类型,也难以写出合理实现
几乎所有的类型都能、应该实现 Debug,
#[derive(Debug)]
,通常是最佳实现方式
注意:派生的 Trait 会为任意范型参数添加相同的约束(bound)
use std::fmt::Debug;
// 使用派生的方式为 Pair 实现 Trait,也会为 T 添加约束
#[derive(Debug)]
struct Pair<T> {
a: T,
b: T,
}
fn main() {
let pair = Pair { a: 1, b: 2 };
println!("Pair: {:?}", pair);
}
如果 T 不能满足约束,编译器会报错:
use std::fmt::Debug;
// #[derive(Debug)]
struct Person {
name: String,
}
#[derive(Debug)]
struct Pair<T> {
a: T,
b: T,
}
fn main() {
let pair = Pair {
a: Person {
name: "Alice".to_string(),
},
b: Person {
name: "Bob".to_string(),
},
};
println!("Pair: {:?}", pair);
}
- 利用
fmt::Formatter
提供的各种 debug_xxx 辅助方法手动实现- debug_struct
- debug_tuple
- debug_list
- debug_set
- debug_map
use std::fmt;
#[derive(Debug)]
struct Pair<T> {
a: T,
b: T,
}
impl<T: fmt::Debug> fmt::Display for Pair<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Pair")
.field("a", &self.a)
.field("b", &self.b)
.finish()
}
}
fn main() {
let pair = Pair { a: 5, b: 10 };
println!("Pair: {:?}", pair);
}
-
不是 Send 的类型无法放在 Mutex 中,也不能在包含线程池的应用程序中传递使用
#[derive(Debug)] struct MyBox(*mut u8); unsafe impl Send for MyBox {} use std::rc::Rc; fn main() { let mb = MyBox(Box::into_raw(Box::new(0))); // Rc 没有实现 Send Trait,所以跨线程调用会报错 let x = Rc::new(42); std::thread::spawn(move || { // 报错 print!("{:?}", x); // print!("{:?}", mb); }); }
-
不是 Sync 的类型无法通过 Arc 共享,也无法被放置在静态变量中
use std::cell::RefCell; use std::sync::Arc; fn main() { let x = Arc::new(RefCell::new(42)); std::thread::spawn(move || { // RefCell 并没有实现 Sync Trait,所以跨线程调用会报错 // `std::cell::RefCell<i32>` cannot be shared between threads safely let mut x = x.borrow_mut(); *x += 1; }); }
-
如果没实现上述 Trait,建议在文档中说明
如果没实现上述 Trait,建议在文档中说明
Clone:
#[derive(Debug, Clone)]
struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: String, age: u32) -> Self {
Self { name, age }
}
}
fn main() {
let person1 = Person::new("John".to_string(), 30);
let person2 = person1.clone();
println!("Person 1: {:?}", person1);
println!("Person 2: {:?}", person2);
}
Default:
#[derive(Default)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point::default();
println!("Point: {}, {}", point.x, point.y);
}
-
PartialEq 特别有用:用户会希望使用
==
或assert_eq!
比较你类型的两个实例#[derive(Debug, PartialEq)] struct Point { x: i32, y: i32, } fn main() { let point1 = Point { x: 1, y: 2 }; let point2 = Point { x: 1, y: 2 }; let point3 = Point { x: 3, y: 4 }; println!("point1 == point2: {}", point1 == point2); println!("point1 == point3: {}", point1 == point3); }
-
PartialOrd
和Hash
相对更专门化- 将类型作为 Map 中的 Key,须实现 PartialOrd,以便进行 Key 的比较
- 使用
std::collection
的集合类型进行去重的类型,须实现 Hash,以便进行哈希计算
use std::collections::BTreeMap;
#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone)]
struct Person {
name: String,
age: u32,
}
fn main() {
let mut args = BTreeMap::new();
let person1 = Person {
name: "Alice".to_string(),
age: 25,
};
let person2 = Person {
name: "Bob".to_string(),
age: 30,
};
let person3 = Person {
name: "Charlie".to_string(),
age: 27,
};
args.insert(person1, "Alice's age");
args.insert(person2, "Bob's age");
args.insert(person3, "Charlie's age");
for (person, description) in &args {
println!("{}: {} - {:?}", person.name, person.age, description);
}
}
Hash:
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
#[derive(Debug, PartialEq, Eq, Clone)]
struct Person {
name: String,
age: u32,
}
impl Hash for Person {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.age.hash(state);
}
}
fn main() {
let mut persons = HashSet::new();
let person1 = Person {
name: "Alice".to_string(),
age: 25,
};
let person2 = Person {
name: "Bob".to_string(),
age: 30,
};
let person3 = Person {
name: "Charlie".to_string(),
age: 27,
};
persons.insert(person1);
persons.insert(person2);
persons.insert(person3);
println!("persons: {:?}", persons);
}
-
Eq 和 Ord 有额外的语义要求(相对 PartialEq 和 PartialOrd)
- 只应在确信这些语义适用于你的类型时才实现它们
-
Eq
- 自反性(Reflexivity): 对于任何对象 x,x == x 必须为真
- 对称性(Symmetry): 对于任何对象 x 和 y,如果 x == y 为真,则 y == x 也必须为真
- 传递性(Transitivity): 对于任何对象 x、y 和 z,如果 x == y 为真,y == z 也为真,则 x == z 也必须为真
-
Ord
- 自反性(Reflexivity): 对于任何对象 x,x <= x 和 x >= x 必须为真
- 反对称性(Antisymmetry): 对于任何对象 x 和 y,如果 x <= y 为真,y <= x 也为真,则 x == y 也必须为真
- 传递性(Transitivity): 对于任何对象 x、y 和 z,如果 x <= y 为真,y <= z 也为真,则 x <= z 也必须为真
- serde_derive 提供了机制,可以覆盖单个字段或枚举变体的序列化
- 由于 serde 是第三方库,你可能不希望强制添加对它的依赖
- 大多数库选择提供一个 serde 功能(feature),只有当用户选择启用该功能时才添加对 serde 的支持
# mylib
[dependencies]
serde = { version = "1.0", optional = true }
[features]
serde = ["serde"]
其他人使用 mylib 时,可以选择启用 serde 功能:
[dependencies]
mylib = { version = "1.0", features = ["serde"] }
- 用户通常不期望类型是 Copy 的,如果想要两个副本,通常希望调用 clone
- Copy 改变了移动给定类型值的语义,让用户 surprise
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let point1 = Point { x: 10, y: 20 };
let point2 = point1; // 这里发生了复制,而不是移动
println!("point1: {:?}, point2: {:?}", point1, point2);
}
- Copy 类型受到很多限制,一个最初简单的类型很容易变得不再满足 Copy 的要求
- 例如持有了 String 或者其他非 Copy 的类型 ---> 不得不移除 Copy
Rust 不会自动为实现 Trait 的类型的引用提供对应的实现
-
Bar 实现了 Trait,也不能将
&Bar
传递给fn foo<T: Trait>(t: T)
,因为 Trait 可能包含接受&mut self
或self
的方法,而这些方法无法在&Bar
上调用 -
对于看到 Trait 只有
&self
方法的用户来说,这会非常令人惊讶 -
定义新的 Trait 时,通常需要为下列提供相应的全局实现
&T where T: Trait
&mut T where T: Trait
Box<T> where T: Trait
-
Iterator(迭代器):为类型的引用添加 Trait 实现
- 对于任何可迭代的类型,考虑为
&MyType
和&mut MyType
实现IntoIterator
- 在循环中可直接使用借用实例,符合用户预期。
- 对于任何可迭代的类型,考虑为
- Rust 没有传统意义上的继承
- Deref 和 AsRef 提供了类似继承的东西。你有一个类型为 T 的值,并满足
T: Deref<Target = U>
,可以在 T 类型值上直接调用类型 U 的方法
use std::ops::Deref;
struct MyVec(Vec<i32>);
impl Deref for MyVec {
type Target = Vec<i32>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn main() {
let my_vec = MyVec(vec![1, 2, 3, 4, 5]);
println!("Length: {}", my_vec.len());
println!("Second element: {}", my_vec[1]);
}
如果你提供了相对透明的类型(例 Arc)
- 实现 Deref 允许你的包装类型在使用点运算符时,自动解引用为内部类型,从而可以直接调用内部类型的方法
- 如果访问内部类型不需要任何复杂或潜在的低效逻辑,应考虑实现
AsRef
,这样用户可以轻松地将&WrapperType
作为&InnerType
使用 - 对于大多数包装类型,还应该在可能的情况下实现
From<InnerType>
和Into<InnerType>
,以便用户可轻松地添加或移除包装。
use std::ops::Deref;
struct Wrapper(String);
impl Deref for Wrapper {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for Wrapper {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<String> for Wrapper {
fn from(s: String) -> Self {
Wrapper(s)
}
}
impl From<Wrapper> for String {
fn from(wrapper: Wrapper) -> Self {
wrapper.0
}
}
fn main() {
let wrapper = Wrapper::from("Hello".to_string());
// 使用 . 运算符调用 String 的方法
println!("wrapper.len() = {}", wrapper.len());
// 使用 as_ref() 方法将 Wrapper 转换为 &str
let inner_ref: &str = wrapper.as_ref();
println!("inner_ref = {}", inner_ref);
// 将 Wrapper 转换为 String
let inner_string: String = wrapper.into();
println!("inner_string = {}", inner_string);
}
Borrow Trait(与 Deref 和 AsRef 有些类似)。针对更为狭窄的使用情况进行了定制:
- 允许调用者提供同一类型的多个本质上相同的变体中的任意一个。可叫做:
Equivalent
- 例:对于一个
HashSet<String>
,Borrow 允许调用者提供&str
或&String
。 - 虽然使用 AsRef 也可以实现类似的效果,但如果没有 Borrow 的额外要求,这种实现是不安全的,因为 Borrow 要求目标类型实现的 Hash、Eq 和 Ord 必须与实现类型完全相同
- 例:对于一个
- Borrow 还为
Borrow<T>
、&T
和&mut T
提供了通用实现- 这使得在 Trait 约束中使用它来接受给定类型的拥有值或引用值非常方便。
- Borrow 仅适用于当你的类型本质上与另一个类型等价时
- 而 Deref 和 AsRef 则适用于更广泛地实现你的类型可以“充当”的情况
use std::borrow::Borrow;
fn point_length<S>(string: S)
where
S: Borrow<str>,
{
println!("Length: {}", string.borrow().len());
}
fn main() {
let str1 = "Hello, world!";
let string1 = String::from("Hello, world!");
point_length(string1);
point_length(str1);
}
-
你写的代码包含契约(contract),而用户的代码则是契约的实现
- 要求:代码使用的限制
- 承诺:代码使用的保证
-
设计接口时(经验法则):
- 避免施加不必要的限制,只做能够兑现的承诺
-
对于增加限制或取消承诺:重大的语义版本更改,也可能导致其他代码出问题
-
放宽限制或提供额外的承诺,通常是向后兼容的
- Rust中,限制的常见形式:Trait 约束(Bound),参数类型(Argument Type)
- 承诺的常见形式:Trait 的实现
- 返回类型
示例:
-
fn frobnicate1(s:String)->String
- 契约:调用者进行内存分配,承诺返回拥有的 String,但是无法改为“无需内存分配”的函数
-
fn frobnicate2(s:&str)->Cow<'_,str>
- 放宽了契约:只接收字符串的引用,承诺返回字符串的引用或一个拥有的 String
- 同样比较死板,只能接受
&str
,无法接受String
或&String
。返回值也只能是Cow
类型
-
fn frobnicate3(s:implAsRef<str>)->implAsRef<str>
- 进一步放宽契约:要求传入能产生字符串引用的类型,承诺返回值可产生字符串引用
use std::borrow::Cow; // 都传入字符串,返回字符串,但契约不同 // 没有更好。要仔细规划契约,否则改变契约会引起破坏 fn frobnicate3<T: AsRef<str>>(s: T) -> T { s } fn main() { let string = String::from("example"); let borrowed = "hello"; let cow = Cow::Borrowed("world"); let res1: &str = frobnicate3::<&str>(string.as_ref()); let res2: &str = frobnicate3(borrowed); let res3: Cow<'_, str> = frobnicate3(cow); println!("{} {} {}", res1, res2, res3); }
通过范型放宽对函数的要求
单态化:
// 假设有一个函数,它接受了一个实现了 AsRef<str> trait 的参数
fn print_as_str<T: AsRef<str>>(s: T) {
println!("{}", s.as_ref());
}
// 这个函数是范型的,它对 T 进行了范型化
// 这意味着它会对你使用它的每一种实现了 AsRef<str> 的类型进行单态化
// 例如,如果你用一个 String 和一个 &str 来调用它
// 你就会在你的二进制文件中有两份函数的拷贝
fn main() {
let s = String::from("hello");
let r = "world";
print_as_str(s); // 调用 print_as_str::<String>
print_as_str(r); // 调用 print_as_str::<&str>
}
动态派发:
// 为了避免这种重复,将函数改成接受 &dyn AsRef<str>:
fn print_as_str(s: &dyn AsRef<str>) {
println!("{}", s.as_ref());
}
// 这个函数不再是泛型的,它接受一个trait 对象
// 它可以是任何实现了 AsRef<str> 的类型。
// 这意味着它会在运行时使用动态分发来调用 as_ref 方法
// 并且你只会在你的二进制文件中有一份西数的拷贝:
fn main() {
let s = String::from("hello");
let r = "world";
print_as_str(&s); // 传递一个类型为 &dyn AsRef<str> 的 trait 对象
print_as_str(&r); // 传递一个类型为 &dyn AsRef<str> 的 trait 对象
}
并且不要走极端,并不需要将每个函数都变成范型的,都变成 trait 对象。如果用户合理、频繁的使用其他类型代替你最初选定的类型,那么参数定义为范型更合适
问题:通过单态化(monomorphization),会为每个使用泛型代码的类型组合生成泛型代码的副本
- 担心:让很多参数变成泛型 --> 二进制文件过大
- 解决:动态分发(dynamic dispatch),以忽略不计的性能成本来缓解这个问题
- 对于以引用方式获取的参数(dyn Trait 不是 Sized 的,需要使用宽指针来使用它们),可以使用动态分发代替泛型参数
// 假设我们有一个名为 process 的范型函数,它接受一个类型参数 T 并对其执行某些操作
fn process<T>(value: T) {
println!("处理 T");
}
// 上述函数使用静态分发,这意味着在编译时将为每个具体类型 T 生成相应的实现
// 现在,假设调用者想要提供动态分发的方式,允许在运行时选择实现。
// 他们可以通过传道 Trait 对象作为参数,
// 使用 dyn 关键宇来实现。以下是一个例子:
trait Processable {
fn process(&self);
}
struct TypeA;
impl Processable for TypeA {
fn process(&self) {
println!("处理 TypeA");
}
}
struct TypeB;
impl Processable for TypeB {
fn process(&self) {
println!("处理 TypeB");
}
}
fn process_trait_object(processable: &dyn Processable) {
processable.process();
}
// 如果调用者想要使用动态分发并在运行时选择实现
// 他们可以调用 process_trait_object 函数,并传递 Trait 对象作为参数
// 调用着可以根据需要选择要提供的具体实现
fn main() {
let a = TypeA;
let b = TypeB;
process_trait_object(&a);
process_trait_object(&b);
// 静态分发
process(&a);
process(&b);
// 转化为 trait 对象传入
process(&a as &dyn Processable);
process(&b as &dyn Processable);
}
- 使用动态分发(dynamic dispatch:代码不会对性能敏感(可以接受);在高性能应用中,在频繁调用的热循环中使用动态分发可能会成为一个致命问题
- 在撰写本文时,只有在简单的 Trait 约束时,才能使用动态分发。如
T: AsRef<str>
或impl AsRef<str>
- 对于更复杂的约束,Rust 无法构造动态分发的虚函数表(vtable)。因此无法使用类似
&dyn Hash + Eq
这样的组合约束
使用泛型时,调用者始终可以通过传递一个 Trait 对象来选择动态分发。反过来不成立:如果你接受一个 Trait 对象作为参数,那么调用者必须提供 Trait 对象,而无法选择使用静态分发
从具体类型开始编写接口,然后逐渐将它们转换为泛型(可行,但不一定是向下兼容)
fn foo(v: &Vec<usize>) {
// 处理 v 的代码
// ...
}
// 现在,我们决定将函数改为使用 Trait 限定 AsRef<[usizeJ>,
// 即 impl AsRef<[usize]>:
// fn foo(v: impl AsRef<[usize]>) {
// // 处理 v
// // ...
// }
fn main() {
let iter = vec![1, 2, 3].into_iter();
foo(&iter.collect());
}
// 在原始版本中,编译器可以推断出 iter.collect()应该收集为一个 Vec<usize>类型
// 因为我们将其传递给了接受 &Vec<usize> 的 foo 函数。
// 然而,在更改为使用特质限定后,编译器只知道 foo 函数
// 接受一个实现了 AsRef<[usize]>特质的类型。
// 这里有多个类型满足这个条件,例如 vec<usize> 和 &[usize]。
// 因此,编泽器无法确定应该将 iter.collect()的结果解释为哪个具体类型。
// 这样的更改将导致编译器无法推断类型,并且调用者的代码将无法通过编译。
// 为了解决这个问题,调用者可能需要显示制定期望的类型,例如:
// let iter = vec![1, 2, 3].into_iter();
// foo(&iter.collect::<Vec<usize>>());
- 定义 Trait 时,它是否是对象安全的,也是契约未写明的一部分(以下是非常简单的解释)
对象安全:描述一个 Trait 可否安全的包装成 Trait Object 对象安全的 Trait 是满足以下条件的 Trait(RFC 255):
-
所有的 supertrait 必须是对象安全的
-
Sized 不能作为 supertrait(不能要求 Self: Sized)
-
不能有任何关联常量。
-
不能有任何带有泛型的关联类型。
-
所有的关联函数必须满足以下条件之一:
- 可以从 Trait 对象分发的函数(Dispatchable functions)
- 没有任何类型参数(生命周期参数是允许的)
- 是一个方法,只在接收器类型中使用 Self
- 接收器是以下类型之一:
&Self
(即&self
)、&mut Self
(即&mut self
)、Box<Self>
、Rc<Self>
、Arc<Self>
、Pin<P>
,其中 P 是上述类型之一 - 没有
where Self: Sized
约束(Self 的接收器类型(即 self)暗含了这一点)
- 显式不可分发的函数(non-dispatchable functions)要求:
- 具有
where Self: Sized
约束(Self 的接收器类型(即 self)暗含了这一点)
- 具有
- 可以从 Trait 对象分发的函数(Dispatchable functions)
-
如果 Trait 对象是安全的:可以使用 dyn Trait 将该 Trait 的不同类型视为单一通用类型。
-
如果 Trait 对象不是安全的:编译器会禁止使用 dyn Trait。
建议 Trait 是对象安全的(即使稍微降低使用的便利程度):提供了使用的新方式和灵活性
// 假设我们有 Animal Trait,它有两个方法:name 和 speak
// name 方法返回一个 &str,表示动物的名字
// speak 方法打印出动物发出的声音
// 我们可以为 Dog 和 Cat 类型实现这个 Trait
trait Animal {
fn name(&self) -> &str;
fn speak(&self);
// fn clone(&self) -> Self; // 返回 Self 类型,所以这个 Trait 不是对象安全的
/// 如果我们想让 Animal trait 是对象安全的,同时保留 clone 方法
/// 我们可以给 clone 方法添加一个 Sized 限定条件
/// 它只能在具体类型上调用,而不能在 trait 对象上调用
fn clone(&self) -> Self
where
Self: Sized;
}
struct Dog {
name: String,
}
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn speak(&self) {
println!("woof!");
}
fn clone(&self) -> Self
where
Self: Sized,
{
todo!()
}
}
struct Cat {
name: String,
}
impl Animal for Cat {
fn name(&self) -> &str {
&self.name
}
fn speak(&self) {
println!("meow!");
}
fn clone(&self) -> Self
where
Self: Sized,
{
todo!()
}
}
// 这个 Animal trait 是对象安全的,因为没有返回 Self 类型或使用范型参数
// 所以我们可以使用它来创建一个 trait 对象
fn main() {
let dog = Dog {
name: String::from("Rusty"),
};
let cat = Cat {
name: String::from("Misty"),
};
let animals: Vec<&dyn Animal> = vec![&dog, &cat];
for animal in animals {
println!("This is {}", animal.name());
animal.speak();
// 只能在具体类型上调用,例如 cat.clone();
// animal.clone();
}
}
如果 Trait 必须有范型方法,范型参数放在 Trait 上:
use std::{collections::HashSet, hash::Hash};
// 将范型参数放在 Trait 本身
trait Container<T> {
fn contains(&self, item: &T) -> bool;
}
// 我们可以为不同的容器类型实现 Container trait,每个实现都具有自己特定的元素类型
// 例如,我们可以为 Vec<T> 实现 HashSet<T>,其中 Container Trait:
impl<T> Container<T> for Vec<T>
where
T: PartialEq,
{
fn contains(&self, item: &T) -> bool {
self.iter().any(|x| x == item)
}
}
impl<T> Container<T> for HashSet<T>
where
T: Eq + Hash,
{
fn contains(&self, item: &T) -> bool {
self.contains(item)
}
}
fn main() {
let vec_container: Box<dyn Container<i32>> = Box::new(vec![1, 2, 3]);
let hash_container: Box<dyn Container<i32>> =
Box::new(vec![4, 5, 6].into_iter().collect::<HashSet<_>>());
println!("Vec contains 2: {}", vec_container.contains(&2));
println!("HashSet contains 6: {}", hash_container.contains(&6));
}
范型参数可否使用动态分发,来保证 Trait 的对象安全(使用动态分发代替范型参数):
use std::fmt::Debug;
// 假设我们有一个 Trait 对象,它有一个范型方法 bar,它接受一个范型参数 T:
// trait Foo{
// fn bar<T>(&self, x: T);
// }
// 这个 Trait 是不是对象安全的?其实并不是对象安全的。
// 当 T 是一个具体的类型,比如 i32 或者 String,需要在运行时才能知道 T 的具体类型,才能调用 bar 方法
// 当 T 也是 trait object,比如 &dyn Debug 或 &dyn Display,那么这个 trait 就是对象安全的
// 因为它可以通过动态派发来调用 T 的方法
trait Foo {
fn bar(&self, x: &dyn Debug);
}
struct A {
name: String,
}
impl Foo for A {
fn bar(&self, x: &dyn Debug) {
println!("A {:?} says {:?}", self.name, x);
}
}
struct B {
id: i32,
}
impl Foo for B {
fn bar(&self, x: &dyn Debug) {
println!("B {:?} says {:?}", self.id, x);
}
}
fn main() {
let a = A {
name: "Alice".to_string(),
};
let b = B { id: 42 };
let foos: Vec<&dyn Foo> = vec![&a, &b];
for f in foos {
f.bar(&"Hello World");
}
}
- 为实现对象安全,需要做出多大牺牲?
- 考虑你的 Trait 会被怎样使用,用户是否想把它当作 Trait 对象;用户想使用你的 Trait 的多种不同实例 → 努力实现对象安全
-
针对 Rust 中几乎每个函数、Trait 和类型,须决定:是否应该拥有数据还是仅持有对数据的引用
-
如果代码需要数据的所有权:它必须存储拥有的数据
-
当你的代码必须拥有数据时:必须让调用者提供拥有的数据,而不是引用或克隆
-
这样可让调用者控制分配,并且可清楚地看到使用相关接口的成本
-
如果代码不需拥有数据:应操作于引用,例如:
- 像 i32、bool、f64 等“小类型”,直接存储和复制的成本与通过引用存储的成本相同
- 并不是所有 Copy 类型都适用。例如:
[u8; 8192]
是 Copy 类型,但在多个地方存储和复制它会很昂贵
-
无法确定代码是否需要拥有数据,因为它取决于运行时情况:
- 则使用Cow 类型,允许在需要时持有引用或拥有值
- 如果只有引用的情况下要求生成拥有的值:Cow 将使用 ToOwned trait 在后台创建一个,通常是通过克隆
- 通常在返回类型中使用 Cow 来表示有时会分配内存的函数
use std::borrow::Cow;
// 假设有一个函数 process_data,接收一个字符串
// 并根据一些条件对其进行处理。有时,我们需要修改输入字符串
// 并拥有对修改后字符串的所有权
// 然而大多数情况下,我们只是对输入字符串进行读取操作,而不妖修改
fn process_data(data: Cow<str>) {
if data.contains("invalid") {
// 如果输入数据包含 "invalid",我们需要修改它
let owned_data = data.into_owned();
println!("Processed data: {}", owned_data);
} else {
// 如果输入数据不包含 "invalid",我们不需要修改它
println!("Processed data: {}", data);
}
}
// 在这个示例中,我们使用 Cow<str> 类型作为参数类型。
// 当调用函数时,我们可以传递一个普通的字符串引用(&str),
// 或一个拥有所有权的字符串(String)作为参数
fn main() {
let input1 = "This is valid data";
process_data(Cow::Borrowed(input1));
let input2 = "This is invalid data";
process_data(Cow::Owned(input2.to_owned()));
}
有时,引用生命周期会让接口复杂,难以使用
- 如果用户使用接口时遇到编译问题,这表明您可能需要(即使不必要)拥有某些数据的所有权
- 这样做的话,建议首先考虑容易克隆或不涉及性能敏感性的数据,而不是直接对大块数据的内容进行堆分配
- 这样做可以避免性能问题并提高接口的可用性
-
析构函数(Destructor):在值被销毁时执行特定的清理操作
-
析构函数由 Drop trait 实现:它定义了一个 drop 方法
-
析构函数通常是不允许失败的,并且是非阻塞执行的。但有时:
- 例如释放资源时,可能需要关闭网络连接或写入日志文件,这些操作都有可能发生错误
- 可能需要执行阻塞操作,例如等待一个线程的结束或等待一个异步任务的完成
-
针对 I/O 操作的类型,在丢弃时需要执行清理。例:将写入的数据刷新到磁盘、关闭打开的文件、断开网络连接
-
这些清理操作应在类型的 Drop 实现中完成。
- 问题:一旦值被丢弃,就无法向用户传递错误信息,除非通过 panic
- 异步代码也有类似问题:希望在清理过程中完成这些工作,但有其他工作处于 pending 状态。可尝试启动另一个执行器,但这会引入其他问题,例如在异步代码中阻塞。
-
没有完美解决方案:需要通过 Drop 尽力清理。
- 如果清理出错了,至少我们尝试了——忽略错误并继续
- 如果还有可用的执行器,可尝试生成一个 future 来做清理,但如果 future 永不会运行,我们也尽力了
-
若用户不想留下“松散”线程:提供显式的析构函数
- 这通常是一个方法,它获得
self
的所有权并暴露任何错误(使用 ->Result<_, _>
)或异步性(使用async fn
),这些都是与销毁相关的
- 这通常是一个方法,它获得
use std::os::fd::AsRawFd;
// 表示文件句柄的类型
struct File {
name: String,
fd: i32,
}
impl File {
// 一个构造函数,打开文件并返回一个 File 实例
fn open(name: &str) -> Result<File, std::io::Error> {
// 使用 std::fs::OpenOptions 打开文件,具有读写权限
let f = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(name)?;
// 使用 std::os::unix::io::AsRawFd 获取文件描述符
let fd = f.as_raw_fd();
Ok(File {
name: name.to_string(),
fd,
})
}
// 一个显示的析构函数,关闭文件并返回任何错误
fn close(self) -> Result<(), std::io::Error> {
// 移除 name 字段,并打印
// let name = self.name; //不能从 `self.name` 移除,因为它位于 `&mut` 引用之后
println!("Closing file: {}", name);
// 使用 std::os::unix::io::FromRawFd 将 fd 转换回 std::fs::File
let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(self.fd) };
// 使用 std::fs::File::sync_all 将任何挂起的写入刷新到磁盘
file.sync_all()?;
// 使用 std::fs::File::set_len 将文件截断为 0 字节
file.set_len(0)?;
// 再次使用 std::fs::File::sync_all 将截断刷新到磁盘
file.sync_all()?;
// 丢弃 file 实例,会自动关闭文件
drop(file);
Ok(())
}
}
// * 添加显式析构函数时会遇问题:
// * 当类型实现了 Drop,在析构函数中无法将该类型的任何字段移出。因为在显式析构函数运行后,`Drop::drop` 仍会被调用,它接受 `&mut self`,要求 self 的所有部分都没有被移动
// * Drop 接受的是 `&mut self`,而不是 self,因此 Drop 无法实现简单地调用显式析构函数并忽略其结果(因为 Drop 不拥有 self)
// impl Drop for File {
// fn drop(&mut self) {
// // 地啊用 close 方法并忽略它的结果
// let _ = self.close(); // 不能从 `*self` 中移除值,因为它位于 `&mut` 引用之后
// // 打印一条信息,表明文件被丢弃了
// println!("File {} dropped.", self.name)
// }
// }
fn main() {
// 创建一个名为“test.txt”的文件,包含一些内容
std::fs::write("test.txt", "Hello, world!").unwrap();
// 打开文件并获取 File 实例
let file = File::open("test.txt").unwrap();
// 打印文件名和 fd
println!("File name: {}, fd: {}", file.name, file.fd);
// 关闭文件处理任何错误
match file.close() {
Ok(()) => println!("File closed successfully."),
Err(e) => println!("Failed to close file: {}", e),
}
// 检查文件关闭后的大小
let metadata = std::fs::metadata("test.txt").unwrap();
println!("File size: {} bytes", metadata.len());
}
解决办法:
-
将顶层类型作为包装了 Option 的新类型,Option 持有一个内部类型,该类型包含所有的字段。在两个析构函数中使用
Option::take
;当内部类型还没有被取走时,调用内部类型的显式析构函数。由于内部类型没有实现 Drop,你可以获取所有字段的所有权- 想在顶层类型上提供所有的方法,都必须包含通过 Option 来 获取内部类型上字段的代码
use std::os::fd::AsRawFd; struct File { inner: Option<InnerFile>, } // 表示文件句柄的类型 struct InnerFile { name: String, fd: i32, } impl File { // 一个构造函数,打开文件并返回一个 File 实例 fn open(name: &str) -> Result<File, std::io::Error> { // 使用 std::fs::OpenOptions 打开文件,具有读写权限 let f = std::fs::OpenOptions::new() .read(true) .write(true) .open(name)?; // 使用 std::os::unix::io::AsRawFd 获取文件描述符 let fd = f.as_raw_fd(); Ok(File { inner: Some(InnerFile { name: name.to_string(), fd, }), }) } // 一个显示的析构函数,关闭文件并返回任何错误 fn close(mut self) -> Result<(), std::io::Error> { if let Some(inner) = self.inner.take() { let name = inner.name; let fd = inner.fd; println!("Closing file {} with fd: {}", name, fd); // 使用 std::os::unix::io::FromRawFd 将 fd 转换回 std::fs::File let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(fd) }; // 使用 std::fs::File::sync_all 将任何挂起的写入刷新到磁盘 file.sync_all()?; // 使用 std::fs::File::set_len 将文件截断为 0 字节 file.set_len(0)?; // 再次使用 std::fs::File::sync_all 将截断刷新到磁盘 file.sync_all()?; // 丢弃 file 实例,会自动关闭文件 drop(file); Ok(()) } else { Err(std::io::Error::new( std::io::ErrorKind::Other, "File already closed or dropped", )) } } } impl Drop for File { fn drop(&mut self) { if let Some(inner) = self.inner.take() { let InnerFile { name, fd } = inner; println!("Closing file {} with fd: {}", name, fd); // 使用 std::os::unix::io::FromRawFd 将 fd 转换回 std::fs::File let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(fd) }; // 丢弃 file 实例,会自动关闭文件 drop(file); } else { // 如果 inner 字段是 None,说明已经丢弃,不做任何操作 } } } fn main() { // 创建一个名为“test.txt”的文件,包含一些内容 std::fs::write("test.txt", "Hello, world!").unwrap(); // 打开文件并获取 File 实例 let file = File::open("test.txt").unwrap(); // 打印文件名和 fd println!( "File name: {}, fd: {}", file.inner.as_ref().unwrap().name, file.inner.as_ref().unwrap().fd ); // 关闭文件处理任何错误 match file.close() { Ok(()) => println!("File closed successfully."), Err(e) => println!("Failed to close file: {}", e), } // 检查文件关闭后的大小 let metadata = std::fs::metadata("test.txt").unwrap(); println!("File size: {} bytes", metadata.len()); }
-
方法二:所有字段都可以 take。如果类型具有合理的“空”值,那么效果很好
- 如果您必须将几乎每个字段都包装在 Option 中,然后对这些字段的每次访问 都进行匹配的 unwrap,很繁琐
use std::os::fd::AsRawFd; // 表示文件句柄的类型 struct File { name: Option<String>, fd: Option<i32>, } impl File { // 一个构造函数,打开文件并返回一个 File 实例 fn open(name: &str) -> Result<File, std::io::Error> { // 使用 std::fs::OpenOptions 打开文件,具有读写权限 let f = std::fs::OpenOptions::new() .read(true) .write(true) .open(name)?; // 使用 std::os::unix::io::AsRawFd 获取文件描述符 let fd = f.as_raw_fd(); Ok(File { name: Some(name.to_string()), fd: Some(fd), }) } // 一个显示的析构函数,关闭文件并返回任何错误 fn close(mut self) -> Result<(), std::io::Error> { if let Some(name) = std::mem::take(&mut self.name) { if let Some(fd) = std::mem::take(&mut self.fd) { println!("Closing file: {} with fd {}", name, fd); // 使用 std::os::unix::io::FromRawFd 将 fd 转换回 std::fs::File let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(fd) }; // 使用 std::fs::File::sync_all 将任何挂起的写入刷新到磁盘 file.sync_all()?; // 使用 std::fs::File::set_len 将文件截断为 0 字节 file.set_len(0)?; // 再次使用 std::fs::File::sync_all 将截断刷新到磁盘 file.sync_all()?; // 丢弃 file 实例,会自动关闭文件 drop(file); Ok(()) } else { Err(std::io::Error::new( std::io::ErrorKind::Other, "File already closed or dropped", )) } } else { Err(std::io::Error::new( std::io::ErrorKind::Other, "File already closed or dropped", )) } } } impl Drop for File { fn drop(&mut self) { if let Some(name) = std::mem::take(&mut self.name) { if let Some(fd) = std::mem::take(&mut self.fd) { println!("Dropping file {} with fd {}", name, fd); // 使用 std::os::unix::io::FromRawFd 将 fd 转换回 std::fs::File let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(fd) }; drop(file); } else { } } else { } } } fn main() { // 创建一个名为“test.txt”的文件,包含一些内容 std::fs::write("test.txt", "Hello, world!").unwrap(); // 打开文件并获取 File 实例 let file = File::open("test.txt").unwrap(); // 打印文件名和 fd println!( "File name: {}, fd: {}", file.name.as_ref().unwrap(), file.fd.as_ref().unwrap() ); // 关闭文件处理任何错误 match file.close() { Ok(()) => println!("File closed successfully."), Err(e) => println!("Failed to close file: {}", e), } // 检查文件关闭后的大小 let metadata = std::fs::metadata("test.txt").unwrap(); println!("File size: {} bytes", metadata.len()); }
-
将数据持有在
ManuallyDrop
类型内,它会解引用内部类型,不必再unwrap
。在 drop 中销毁时,可用ManuallyDrop::take
来获取所有权- 缺点:
ManuallyDrop::take
是unsafe
的
use std::{ mem::ManuallyDrop, os::fd::{AsRawFd, FromRawFd}, }; // 表示文件句柄的类型 struct File { name: ManuallyDrop<String>, fd: ManuallyDrop<i32>, } impl File { // 一个构造函数,打开文件并返回一个 File 实例 fn open(name: &str) -> Result<File, std::io::Error> { // 使用 std::fs::OpenOptions 打开文件,具有读写权限 let f = std::fs::OpenOptions::new() .read(true) .write(true) .open(name)?; // 使用 std::os::unix::io::AsRawFd 获取文件描述符 let fd = f.as_raw_fd(); Ok(File { name: ManuallyDrop::new(name.to_string()), fd: ManuallyDrop::new(fd), }) } // 一个显示的析构函数,关闭文件并返回任何错误 fn close(mut self) -> Result<(), std::io::Error> { // 使用 std::mem::replace 将 name 字段替换为一个空的 String,并获取原来的值 let name = std::mem::replace(&mut self.name, ManuallyDrop::new(String::new())); let fd = std::mem::replace(&mut self.fd, ManuallyDrop::new(-1)); println!("Closing file: {:?} with fd {:?}", name, fd); // 使用 std::os::unix::io::FromRawFd 将 fd 转换回 std::fs::File let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(*fd) }; println!("Flushing file: {:?}", file); // 使用 std::fs::File::sync_all 将任何挂起的写入刷新到磁盘 file.sync_all()?; // 使用 std::fs::File::set_len 将文件截断为 0 字节 file.set_len(0)?; // 再次使用 std::fs::File::sync_all 将截断刷新到磁盘 file.sync_all()?; // 丢弃 file 实例,会自动关闭文件 drop(file); Ok(()) } } impl Drop for File { fn drop(&mut self) { let name = unsafe { ManuallyDrop::take(&mut self.name) }; let fd = unsafe { ManuallyDrop::take(&mut self.fd) }; println!("Dropping file {} with fd {}", name, fd); if fd != -1 { // 使用 std::os::unix::io::FromRawFd 将 fd 转换回 std::fs::File let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(fd) }; drop(file); } } } fn main() { // 创建一个名为“test.txt”的文件,包含一些内容 std::fs::write("test.txt", "Hello, world!").unwrap(); // 打开文件并获取 File 实例 let file = File::open("test.txt").unwrap(); // 打印文件名和 fd println!("File name: {}, fd: {}", *file.name, *file.fd); // 关闭文件处理任何错误 match file.close() { Ok(()) => println!("File closed successfully."), Err(e) => println!("Failed to close file: {}", e), } // 检查文件关闭后的大小 let metadata = std::fs::metadata("test.txt").unwrap(); println!("File size: {} bytes", metadata.len()); }
- 缺点:
根据实际情况选择方案:倾向于选择第二个方案。只有发现自己处于一堆 Option 中时才切换到其他选项。如果代码足够简单,可轻松检查代码安全性,那么 ManuallyDrop 方案也挺好
用户可能不会完全理解接口的所有规则和限制。重要的是让用户容易理解接口,并难以用错
接口透明化的第一步:写出好的文档
- 清楚的记录:可能出现意外的情况,或它依赖于用户执行超出类型签名要求的操作
- 例:panic、返回错误、unsafe 函数...
- 在 crate 或 module 级,包括端到端的用例
- 不是针对特定类型或方法,了解所有内容如何组合到一起。对接口的整体结构有一个相对清晰的理解
- 让开发者快速了解到各方法和类型的功能,以及在哪使用。
- 提供定制化使用的起点。通过复制粘贴,结合需求进行修改。
- 组织好文档
- 利用模块来将语义相关的项进行分组
- 使用内部文档链接将这些项相互连接起来
- 考虑使用
#[doc(hidden)]
标记那些不打算公开但出于遗留原因需要的接口部分,避免弄乱文档
- 尽可能的丰富文档
- 可以链接到解释这些内容的外部资源“相关的规范文件(RFC)、博客、白皮书 ...
- 使用
#[doc(cfg(..))]
突出显示仅在特定配置下可用的项:用户能快速了解为什么在文档中列出的某个方法不可用 - 使用
#[doc(alias = "...")]
可让用户以其他名称搜索到类型和方法 - 在顶层文档中,引导用户了解常用的模块、Trait、类型、方法
类型系统可确保:接口明显、自我描述、难以被误用
语义化类型:添加类型来表示值的意义(不仅仅适用基本类型)
enum DryRun {
Yes,
No,
}
enum Verbose {
Yes,
No,
}
enum Overwrite {
Yes,
No,
}
fn processData(dryrun: DryRun, verbose: Verbose, overwrite: Overwrite) {
// ...
}
processData(DryRun::Yes, Verbose::Yes, Overwrite::Yes);
使用“零大小”类型来表示关于类型实例的特定事实
struct Grounded;
struct Lanunched;
enum Color {
White,
Black,
}
struct Kilograms(u32);
struct Rocket<Stage = Grounded> {
stage: std::marker::PhantomData<Stage>,
}
impl Default for Rocket<Grounded> {
fn default() -> Self {
Self {
stage: Default::default(),
}
}
}
impl Rocket<Grounded> {
pub fn launch(self) -> Rocket<Lanunched> {
Rocket {
stage: Default::default(),
}
}
}
impl Rocket<Lanunched> {
pub fn accelerate(&mut self) {}
pub fn decelerate(&mut self) {}
}
impl<Stage> Rocket<Stage> {
pub fn color(&self) -> Color {
Color::White
}
pub fn weight(&self) -> Kilograms {
Kilograms(0)
}
}
#[must_use]
注解
将其添加到类型、Trait 或函数中,如果用户的代码接收到该类型或 Trait 的元素,或调用了该函数,并且没有明确处理它,编译器将发出警告
#[must_use]
fn process_data(data: Data) -> Result<(), Error> {
// ...
Ok(())
}
// 在这个示例中,我们使用 #[must_use] 注解将 process_data 函数标记为必领使用其返回值。
// 如果用户在调用该函数后没有昆式处理返回的 Result 类型,编泽器将发出警告。
// 这有助于提醒用户在处理潜在的错误情况时要小心,并减少可能的错误。
做出用户可见的更改,需三思而后行
- 确保你做出的变化:不会破坏现有用户的代码,这次变化应保留一段时间
- 频繁的向后不兼容的更改(主版本增加),会引起用户不满
有些是显而易见的,有些则很微妙(与 Rust 工作方式相关),主要介绍微妙棘手的更改,以及如何为其制定计划,有时需要在接口灵活性上做出权衡、妥协
移除或重命名公共类型几乎肯定会破坏用户的代码
尽可能利用可见性修饰符。例如 pub(crate)
、pub(in path)
...公共类型越少,更改时(保证不会破坏现有代码)就越自由
pub mod outer_mod {
pub mod inner_mod {
// 仅在 crate::outer_mod 路径下可见
pub(in crate::outer_mod) fn outer_mod_visible_fn() {}
// 整个 crate 内可见
pub(crate) fn crate_visible_fn() {}
// 上一级(outer_mod)可见
pub(super) fn super_mod_visible_fn() {
inner_mod_visible_fn();
}
// 仅在内部(inner_mod)可见
pub(self) fn inner_mod_visible_fn() {}
}
pub fn foo() {
inner_mod::outer_mod_visible_fn();
inner_mod::crate_visible_fn();
inner_mod::super_mod_visible_fn();
}
}
fn bar() {
outer_mod::inner_mod::crate_visible_fn();
outer_mod::inner_mod::super_mod_visible_fn();
outer_mod::inner_mod::outer_mod_visible_fn();
outer_mod::foo();
}
fn main() {
bar()
}
用户代码不仅仅通过名称依赖于你的类型
// lib
pub struct Unit {
pub field: bool,
}
// main
fn is_true(u: rust_interface::Unit) -> bool {
matches!(u, rust_interface::Unit { field: true })
}
fn main() {
let u = rust_interface::Unit; // v0
}
- Rust 提供
#[non_exhaustive]
来缓解这些问题non_exhaustive
表示类型或枚举在将来可能会添加更多字段或变体,它可以应用于 struct、enums 和 enum variants。- 在其它 crate,使用
non_exhaustive
定义的类型,编译器会禁止:隐式构造,lib::Unit { field1: true }
- 以及非穷尽模式匹配(即没有尾随 , .. 的模式)
- 若接口稳定的话,尽量避免使用该注解
// lib.rs
#[non_exhaustive]
pub struct Config {
pub window_width: u16,
pub window_height: u16,
}
fn SomeFunction(config: Config) {
let config = Config {
window_width: 800,
window_height: 600,
};
if let Config {
window_height,
window_width,
} = config
{}
}
// main.rs
use rust_interface::Config;
fn main() {
// 不被允许
let config = Config {
window_width: 800,
window_height: 600,
};
if let Config {
window_height,
window_width,
.. // 必须使用..来忽略其他字段
} = config
{}
}
- 一致性规则禁止把某个 Trait 为某类型 进行多重实现
- 破坏性变更:
- 为现有 Trait 添加 Blanket Implementation 通常是破坏性变更
- 为现有类型实现外部 Trait,或为外部类型实现现有 Trait
- 移除 Trait 实现
- 为新类型实现 Trait 就不是问题
- 为现有类型实现任何 Trait 都要小心
// lib
pub struct Unit;
pub trait Foo1 {
fn foo(&self);
}
// 用例 1
impl Foo1 for Unit {
fn foo(&self) {
println!("foo1");
}
}
// main
use rust_interface::{Foo1, Unit};
trait Foo2 {
fn foo(&self);
}
impl Foo2 for Unit {
fn foo(&self) {
println!("foo2");
}
}
fn main() {
Unit.foo(); // 报错
}
示例二:
// lib
pub struct Unit;
pub trait Foo1 {
fn foo(&self);
}
pub trait Bar1 {
fn foo(&self);
}
impl Bar1 for Unit {
fn foo(&self) {
println!("bar1");
}
}
// main.rs
use rust_interface::Unit;
// use rust_interface::*; // 这个会报错
trait Foo2 {
fn foo(&self);
}
impl Foo2 for Unit {
fn foo(&self) {
println!("foo2");
}
}
fn main() {
Unit.foo();
}
大多数到现有 Trait 的更改也是破坏性更改。改变方法签名、添加新方法,如果有默认实现倒是可以。
Sealed Trait
- 封闭 Trait(Sealed Trait):只能被其它 crate 用,不能实现;防止 Trait 添加新方法时造成破坏性变更;不是内建功能,有多种实现方法
- Sealed Trait 常用于派生 Trait,为实现特定其他 Trait 的类型提供 blanket implementation 的 Trait
- 只有在外部 crate 不该实现你的 Trait 时,才使用 Sealed Trait
- 严重限制 Trait 的可用性,下游 crate 无法为其自己类型实现该 Trait
- 可使用 Sealed Trait 来限制可用作类型参数的类型
- 例:将 Rocket 示例中的 Stage 类型限制为仅允许 Grounded 和 Launched 类型
// lib
use std::fmt::{Debug, Display};
mod sealed {
use std::fmt::{Debug, Display};
pub trait Sealed {}
impl<T> Sealed for T where T: Debug + Display {}
}
pub trait CanUseCannotImplement: sealed::Sealed {}
impl<T> CanUseCannotImplement for T where T: Debug + Display {}
// main
use std::fmt::{Debug, Display};
use rust_interface::CanUseCannotImplement;
pub struct Bar{};
impl Debug for Bar {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Ok(())
}
}
impl Display for Bar {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Ok(())
}
}
// 在 lib 中已经实现好了
// impl CanUseCannotImplement for Bar {}
pub struct Foo{}
// Foo 并不满足条件
impl CanUseCannotImplement for Foo {}
有时,你对代码的某一部分所做的更改会以微妙的方式影响到 接口其他地方的契约。这种情况主要发生在:重新导出(re-exports)、自动 Traits(auto-traits)
在你所依赖的外部 crate 从 1.0 更新到 2.0 版本,所以将此依赖升级到 2.0,其它所有都没有变更。但是用户的代码会因为你的依赖升级而出现编译错误,因为 crate1.0 使用的和 crate2.0 使用的是不同的类型
自动 Trait:有些 Trait 根据类型的内容,会对其进行实现
- 根据它们的特性,它们为接口中几乎每种类型都添加一个隐藏的承诺。Send、Sync,以及 Unpin、Sized、UnwindSafe 也存在类似问题
- 这些特性会传播,无论是具体类型,还是 impl Trait 等类型擦除情况
- 这些 Trait 的实现通常是编译器自动添加的;如果情况不适用,则不会自动添加
例如:类型 A 包含私有类型 B,默认 A 和 B 都是 Send 的。如果修改 B,让 B 不再是 Send 的,那么 A 也变成不 Send 的了。破坏性变化。
这类变化难以追踪和发现:包含一些简单的测试,检查你所有的类型都实现了相关的 Trait
fn is_normal<T: Sized + Send + Sync + Unpin>() {}
#[test]
fn normal_types() {
is_normal::<MyType>()
}