Skip to content

Instantly share code, notes, and snippets.

@fwqaaq
Last active November 9, 2023 08:13
Show Gist options
  • Save fwqaaq/675f0c17b45146d8a71a57965c8d4cce to your computer and use it in GitHub Desktop.
Save fwqaaq/675f0c17b45146d8a71a57965c8d4cce to your computer and use it in GitHub Desktop.
Rust 接口设计原则

四个原则:

  • 不意外(unsurprising)
  • 灵活(flexible)
  • 显而易见(obvious)
  • 受约束(constrained)

不意外(unsurprising)

最少意外原则:

  • 接口应尽可能直观(可预测,用户能猜对)
  • 至少应该不让人感到惊奇

核心思想:

  • 贴近用户已经知道的东西(不必重学概念)

让接口可预测

  • 命名
  • 实现常用的 Trait
  • 人体工程学(Ergonomic)Trait
  • 包装类型(Wrapper Type)

命名实践

  • 接口的名称,应符合惯例,便于推断其功能。例如:
    • 方法 iter,大概率应将 &self 作为参数,并应该返回一个迭代器(iterator
    • 叫做 into_inner 的方法,大概率应将 self 作为参数,并返回某个包装的类型
    • 叫做 SomethingError 的类型,应实现 std::error::Error,并出现在各类 Result
  • 将通用/常用的名称依然用于相同的目的,让用户好猜、好理解
  • 推论:同名的事物应该以相同的方式工作
    • 否则,用户大概率会写出错误的代码

实现常用的 Trait

  • 用户通常会假设接口中的一切均可“正常工作”,例:
    • 使用 {:?} 打印任何类型
    • 可发送任何东西到另外的线程
    • 期望每个类型都是 Clone 的
  • 建议积极实现大部分标准 Trait,即使不立即用到
  • 用户无法为外部类型实现外部的 Trait
    • 即使能包装你的接口类型,也难以写出合理实现

Debug 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 和 Sync(unpin)

  • 不是 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,建议在文档中说明

Clone 和 Default

如果没实现上述 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、PartialOrd、Hash、Eq、Ord

  • 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);
    }
  • PartialOrdHash 相对更专门化

    • 将类型作为 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 下的 Serialize、Deserialize

  • 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

  • 用户通常不期望类型是 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

人体工程学 Trait 实现

Rust 不会自动为实现 Trait 的类型的引用提供对应的实现

  • Bar 实现了 Trait,也不能将 &Bar 传递给 fn foo<T: Trait>(t: T),因为 Trait 可能包含接受 &mut selfself 的方法,而这些方法无法在 &Bar 上调用

  • 对于看到 Trait 只有 &self 方法的用户来说,这会非常令人惊讶

  • 定义新的 Trait 时,通常需要为下列提供相应的全局实现

    • &T where T: Trait
    • &mut T where T: Trait
    • Box<T> where T: Trait
  • Iterator(迭代器):为类型的引用添加 Trait 实现

    • 对于任何可迭代的类型,考虑为 &MyType&mut MyType 实现 IntoIterator
    • 在循环中可直接使用借用实例,符合用户预期。

包装类型(Wrapper Types)

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

灵活(flexible)

  • 你写的代码包含契约(contract),而用户的代码则是契约的实现

    • 要求:代码使用的限制
    • 承诺:代码使用的保证
  • 设计接口时(经验法则):

    • 避免施加不必要的限制,只做能够兑现的承诺
  • 对于增加限制或取消承诺:重大的语义版本更改,也可能导致其他代码出问题

  • 放宽限制或提供额外的承诺,通常是向后兼容的

限制(Restrictions)与承诺(Promises)

  1. Rust中,限制的常见形式:Trait 约束(Bound),参数类型(Argument Type)
  2. 承诺的常见形式:Trait 的实现
  3. 返回类型

示例:

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

范型参数(Generic Arguments)

通过范型放宽对函数的要求

单态化:

// 假设有一个函数,它接受了一个实现了 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>>());

对象安全(Object Safety)

  • 定义 Trait 时,它是否是对象安全的,也是契约未写明的一部分(以下是非常简单的解释)

对象安全:描述一个 Trait 可否安全的包装成 Trait Object 对象安全的 Trait 是满足以下条件的 Trait(RFC 255):

  • 所有的 supertrait 必须是对象安全的

  • Sized 不能作为 supertrait(不能要求 Self: Sized)

  • 不能有任何关联常量。

  • 不能有任何带有泛型的关联类型。

  • 所有的关联函数必须满足以下条件之一:

    1. 可以从 Trait 对象分发的函数(Dispatchable functions)
      • 没有任何类型参数(生命周期参数是允许的)
      • 是一个方法,只在接收器类型中使用 Self
      • 接收器是以下类型之一:&Self(即 &self)、&mut Self(即 &mut self)、Box<Self>Rc<Self>Arc<Self>Pin<P>,其中 P 是上述类型之一
      • 没有 where Self: Sized 约束(Self 的接收器类型(即 self)暗含了这一点)
    2. 显式不可分发的函数(non-dispatchable functions)要求:
      • 具有 where Self: Sized 约束(Self 的接收器类型(即 self)暗含了这一点)
  • 如果 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 的多种不同实例 → 努力实现对象安全

借用 vs 引用(Borrowed vs Owned)

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

有时,引用生命周期会让接口复杂,难以使用

  • 如果用户使用接口时遇到编译问题,这表明您可能需要(即使不必要)拥有某些数据的所有权
    • 这样做的话,建议首先考虑容易克隆或不涉及性能敏感性的数据,而不是直接对大块数据的内容进行堆分配
    • 这样做可以避免性能问题并提高接口的可用性

可失败和阻塞的析构函数(Fallible and Blocking Destructors)

  • 析构函数(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());
}

解决办法:

  1. 将顶层类型作为包装了 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());
    }
  2. 方法二:所有字段都可以 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());
    }
  3. 将数据持有在 ManuallyDrop 类型内,它会解引用内部类型,不必再 unwrap。在 drop 中销毁时,可用 ManuallyDrop::take 来获取所有权

    • 缺点:ManuallyDrop::takeunsafe
    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 方案也挺好

显而易见(Obvious)

用户可能不会完全理解接口的所有规则和限制。重要的是让用户容易理解接口,并难以用错

文档

接口透明化的第一步:写出好的文档

  1. 清楚的记录:可能出现意外的情况,或它依赖于用户执行超出类型签名要求的操作
    • 例:panic、返回错误、unsafe 函数...
  2. 在 crate 或 module 级,包括端到端的用例
    • 不是针对特定类型或方法,了解所有内容如何组合到一起。对接口的整体结构有一个相对清晰的理解
    • 让开发者快速了解到各方法和类型的功能,以及在哪使用。
    • 提供定制化使用的起点。通过复制粘贴,结合需求进行修改。
  3. 组织好文档
    • 利用模块来将语义相关的项进行分组
    • 使用内部文档链接将这些项相互连接起来
    • 考虑使用 #[doc(hidden)] 标记那些不打算公开但出于遗留原因需要的接口部分,避免弄乱文档
  4. 尽可能的丰富文档
    • 可以链接到解释这些内容的外部资源“相关的规范文件(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 类型,编泽器将发出警告。
// 这有助于提醒用户在处理潜在的错误情况时要小心,并减少可能的错误。

受约束(Constrained)

做出用户可见的更改,需三思而后行

  • 确保你做出的变化:不会破坏现有用户的代码,这次变化应保留一段时间
  • 频繁的向后不兼容的更改(主版本增加),会引起用户不满

有些是显而易见的,有些则很微妙(与 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 为某类型 进行多重实现
  • 破坏性变更:
    • 为现有 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>()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment