Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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结构体使用了派生的方式实现了Debug trait,所以说它对泛型参数T自动添加了限定条件:T得实现了Debug trait
  • main函数里Pair的字段的类型是i32,实现了Debug trait,所以打印的出来

输出:

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_struct
  • debug_tuple
  • debug_list
  • debug_set
  • debug_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);  
}
  • 我们手动实现Debug trait而不是使用#[derive(Debug)]标注
  • fmt 是实现 fmt::Debug 时必须定义的方法
  • f: &mut fmt::Formatter<'_>:提供了格式化的上下文和工具
  • f.debug_struct("Pair"):声明格式化为一个带有字段的调试结构体,并指定结构体的名称为 "Pair"
  • .field("a", &self.a).field("b", &self.b):为 Pair 添加两个字段 ab,并关联各自的值
  • .finish():完成格式化的构建,并返回将被打印的结果

输出:

Pair { a: 1, b: 2 }

2.1.5. 建议实现SendUnpinSync trait

如果你的类型没有实现Send trait,就不能把它移动到另一个线程(例如 thread::spawn 要求 T: Send)。把 !Send 的值包进 Mutex<T> 也无济于事:只有当 T: SendMutex<T> 才是 Send/Sync,仍然无法跨线程共享。

看一个例子:

use std::rc::Rc;

fn main() {
    let x = Rc::new(42);

    std::thread::spawn(move || {
        println!("{:?}", x);
    });
}
  • Rc<T>没有实现Send trait所以不能在多线程间使用

我们可以自己写一个简单的元组结构体(当然就没有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实现了Send trait所以可以跨线程使用
  • 像Send trait这种只是作为标记而没有具体的实现的trait叫做标记trait(marker trait)。标记trait 用于 提供编译期的信息,但不会增加具体行为。所以为MyBox实现Send trait就不用写任何实现
  • 手动实现Send trait是不安全的,我们得在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>没有实现Sync trait所以不能用Arc<T>跨线程共享

Unpin trait表示取消固定。Unpin是一个 标记trait(marker trait),用于指示某个类型是否可以安全地从 Pin 中移出,即 是否能绕过Pin<P>的限制

大多数类型默认是Unpin。自引用类型通常通过嵌入 std::marker::PhantomPinned(或其它 !Unpin 字段)来变成 !Unpin。Rust 不会仅仅因为“包含指向自身内部数据的指针”就自动取消 Unpin;若没有这类标记,类型仍然是 Unpin,移动它就可能让内部指针失效。


如果你的类型没有实现上述任一trait都建议在文档中说明