2.1. API设计原则之不意外性(unsurprising) Pt.1:命名的技巧、实现常用的trait(Debug、Send、Sync和Unpin)
2.1.1. 什么是不意外(unsurprising)原则
不意外原则也叫做最少意外原则,它的意思是你写的API应该尽可能的直观。
用户一看到接口就应该能猜出来是干什么用的。至少你写的接口不应该让人感到意外。它的核心思想是贴近用户已经知道的东西,这样用户就不需要重学概念。比如说接口名字里有error,那么用户大概就能猜到这是用来做错误处理的。
也就是说,我们需要让我们写的接口可以预测,这就要求在以下几点:
- 命名
- 实现常用的trait
- “人体工程学的(Ergonomic)” trait
- 包装类型(Wrapper Type)
2.1.2. 命名的技巧
接口的名称,应该符合惯例,便于推断功能。惯例指的是Rust标准库和Rust社区常用的惯例。
举几个例子:
- 方法
iter(或者是名字以iter结尾),大概率应将&self作为参数,并应该返回一个迭代器(iterator) - 叫做
into_inner的方法,大概率将self作为参数,并返回某个包装的类型 - 叫做
SomethingError的类型,应该实现std::error::Error,并出现在各类Result类型里
将通用/常用的名称用于相同的目的,有助于用户的理解。这又引出来一个推论:同名的事物应该以相同的方式工作,否则用户大概率会写出错误的代码。
2.1.3. 实现常用的trait
用户通常会假设接口中的一切皆可“按预期地工作”,例如:
- 可以使用
{:?}打印任何类型 - 可发送任何东西到另外的线程(可以跨线程的)
- 每个类型都是
Clone的
所以在写代码时要积极地去实现大部分标准trait,即使不立即能用到。
从另一个方面去想,用户无法为外部的类型实现外部的trait(因为违背了孤儿规则,详见 1.17.1. 连贯性(一致性)属性),这使得他们很难为你的类型实现他们想要的trait。所以你应该积极地去实现大部分标准trait,让你的类型能够实现大部分用户想要的trait。
2.1.4. 建议实现Debug trait
几乎所有的类型,都能且应该实现Debug trait。
最简单的、最佳的实现方式是使用#[derive(Debug)]注解。需要注意的是,派生的trait会为任意的泛型参数添加相同的约束(bound)。
看个例子就明白了:
use std::fmt::Debug;
#[derive(Debug)]
struct Pair<T> {
a: T,
b: T,
}
fn main() {
let pair = Pair { a: 5, b: 10 };
println!("{:?}", pair);
}
Pair结构体使用了派生的方式实现了Debugtrait,所以说它对泛型参数T自动添加了限定条件:T得实现了Debugtraitmain函数里Pair的字段的类型是i32,实现了Debugtrait,所以打印的出来
输出:
Pair { a: 5, b: 10 }
那如果我把字段类型改成没有实现Debug trait的呢:
use std::fmt::Debug;
struct Person {
name: String,
}
#[derive(Debug)]
struct Pair<T> {
a: T,
b: T,
}
fn main() {
let pair = Pair {
a: Person { name: "Dave".to_string() },
b: Person { name: "Nick".to_string() },
};
println!("{:?}", pair);
}
输出:
error[E0277]: `Person` doesn't implement `Debug`
--> src/main.rs:18:22
|
18 | println!("{:?}", pair);
| ---- ^^^^ `Person` cannot be formatted using `{:?}` because it doesn't implement `Debug`
| |
| required by this formatting parameter
|
= help: the trait `Debug` is not implemented for `Person`
= note: add `#[derive(Debug)]` to `Person` or manually `impl Debug for Person`
help: the trait `Debug` is implemented for `Pair<T>`
--> src/main.rs:7:10
|
7 | #[derive(Debug)]
| ^^^^^
note: required for `Pair<Person>` to implement `Debug`
--> src/main.rs:8:8
|
7 | #[derive(Debug)]
| ----- in this derive macro expansion
8 | struct Pair<T> {
| ^^^^ - type parameter would need to implement `Debug`
= help: consider manually implementing `Debug` to avoid undesired bounds
help: consider annotating `Person` with `#[derive(Debug)]`
|
3 + #[derive(Debug)]
4 | struct Person {
|
另外我们也可以使用标准库里的fmt::Formatter提供的各种debug_xxx辅助方法手动实现:
debug_structdebug_tupledebug_listdebug_setdebug_map
看例子:
use std::fmt;
struct Pair<T> {
a: T,
b: T,
}
impl<T: fmt::Debug> fmt::Debug 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: 1, b: 2 };
println!("{:?}", pair);
}
- 我们手动实现
Debugtrait而不是使用#[derive(Debug)]标注 fmt是实现fmt::Debug时必须定义的方法f: &mut fmt::Formatter<'_>:提供了格式化的上下文和工具f.debug_struct("Pair"):声明格式化为一个带有字段的调试结构体,并指定结构体的名称为"Pair".field("a", &self.a)和.field("b", &self.b):为Pair添加两个字段a和b,并关联各自的值.finish():完成格式化的构建,并返回将被打印的结果
输出:
Pair { a: 1, b: 2 }
2.1.5. 建议实现Send、Unpin和Sync trait
如果你的类型没有实现Send trait,就不能把它移动到另一个线程(例如 thread::spawn 要求 T: Send)。把 !Send 的值包进 Mutex<T> 也无济于事:只有当 T: Send 时 Mutex<T> 才是 Send/Sync,仍然无法跨线程共享。
看一个例子:
use std::rc::Rc;
fn main() {
let x = Rc::new(42);
std::thread::spawn(move || {
println!("{:?}", x);
});
}
Rc<T>没有实现Sendtrait所以不能在多线程间使用
我们可以自己写一个简单的元组结构体(当然就没有Rc<T>的引用计数功能了)来实现:
#[derive(Debug)]
struct MyBox(*mut u8);
unsafe impl Send for MyBox {}
fn main() {
let mb = MyBox(Box::into_raw(Box::new(42)));
std::thread::spawn(move || {
println!("{:?}", mb);
});
}
MyBox实现了Sendtrait所以可以跨线程使用- 像Send trait这种只是作为标记而没有具体的实现的trait叫做标记trait(marker trait)。标记trait 用于 提供编译期的信息,但不会增加具体行为。所以为
MyBox实现Sendtrait就不用写任何实现 - 手动实现
Sendtrait是不安全的,我们得在impl块前添加unsafe标注。Rust 的类型系统默认会自动推导 Send,确保线程安全,而手动实现 Send 可能会绕过 Rust 的安全检查。
没有实现Sync trait的类型无法通过Arc<T>(原子引用计数指针,Rc<T>的多线程版)跨线程共享,也无法被放到需要 Sync 的静态变量中。
看个例子:
use std::cell::RefCell;
use std::sync::Arc;
fn main() {
let x = Arc::new(RefCell::new(42));
std::thread::spawn(move || {
let mut x = x.borrow_mut();
*x += 1;
});
}
RefCell<T>没有实现Synctrait所以不能用Arc<T>跨线程共享
Unpin trait表示取消固定。Unpin是一个 标记trait(marker trait),用于指示某个类型是否可以安全地从 Pin 中移出,即 是否能绕过Pin<P>的限制。
大多数类型默认是Unpin。自引用类型通常通过嵌入 std::marker::PhantomPinned(或其它 !Unpin 字段)来变成 !Unpin。Rust 不会仅仅因为“包含指向自身内部数据的指针”就自动取消 Unpin;若没有这类标记,类型仍然是 Unpin,移动它就可能让内部指针失效。
如果你的类型没有实现上述任一trait都建议在文档中说明。