最新要闻

广告

手机

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

家电

Rust - 接口设计建议之灵活(flexible)

来源:博客园

Rust - 接口设计建议之灵活(flexible)

灵活(flexible)

代码的契约(Contract)

  • 你写的代码包含契约
  • 契约:
    • 要求:代码使用的限制
    • 承诺:代码使用的保证
  • 设计接口时(经验法则):
    • 避免施加不必要的限制,只做能够兑现的承诺
      • 增加限制 或 取消承诺:
        • 重大的语义版本更改
        • 可导致其他代码出问题
      • 放宽限制 或 提供额外的承诺:
        • 通常是向后兼容的

限制(Restrictions)与承诺(Promises)

  • Rust中,限制的常见形式:
    • Trait 约束(Trait Bound)
    • 参数类型(Argument Types)
  • 承诺的常见形式:
    • Trait 的实现
    • 返回类型
  • fn frobnicate1(s: String) -> String
    • 契约:调用者进行内存分配,承诺返回拥有的 String -> 无法改为 “无需内存分配” 的函数
  • fn frobnicate2(s: &str) -> Cow<"_, str>
    • 放宽了契约:只接收字符串的引用,承诺返回字符串的引用或一个拥有的 String
  • fn frobnicate3(s: impl AsRef) -> impl AsRef
    • 进一步放宽契约:要求传入能产生字符串引用的类型,承诺返回值可产生字符串引用

例子一

use std::borrow::Cow;fn frobnicate3>(s: T) -> T {  s}fn main() {  let string = String::from("example");  let borrowed: &str = "hello";  let cow: Cow = Cow::Borrowed("world");    let result1: &str = frobnicate3::<&str>(string.as_ref());  let result2: &str = frobnicate3::<&str>(borrowed);  let result3 = frobnicate3(cow);    println!("Result1: {:?}", result1);  println!("Result2: {:?}", result2);  println!("Result3: {:?}", result3);}
  • 都传入字符串,返回字符串,但契约不同
  • 没有更好。要仔细规划契约,否则改变契约会引起破坏

泛型参数(Generic Arguments)

  • 通过泛型放宽对函数的要求
    • 大多数情况下值得使用泛型代替具体类型

例子二


(资料图片)

// 你有一个函数,它接受一个实现了 AsRef trait 的参数fn print_as_str>(s: T) {  println!("{}", s.as_ref());}// 这个函数是泛型的,它对 T 进行了泛型化,// 这意味着它会对你使用它的每一种实现了 AsRef 的类型进行单态化。// 例如,如果你用一个 String 和一个 &str 来调用它,// 你就会在你的二进制文件中有两份函数的拷贝:fn main() {  let s = String::from("hello");  let r = "world";  print_as_str(s);  // 调用 print_as_str::  print_as_str(r);  // 调用 print_as_str::<&str>}

例子三

// 为了避免这种重复,你可以把函数改成接受一个 &dyn AsRef:fn print_as_str(s: &dyn AsRef) {  println!("{}", s.as_ref());}// 这个函数不再是泛型的,它接受一个 trait 对象,// 它可以是任何实现了 AsRef 的类型// 这意味着它会在运行时使用动态分发来调用 as_ref 方法,// 并且你只会在你的二进制文件中有一份函数的拷贝:fn main() {  let s = String::from("hello");  let r = "world";  print_as_str(&s);  // 传递一个类型为 &dyn AsRef 的 trait 对象  print_as_str(&r);  // 传递一个类型为 &dyn AsRef 的 trait 对象}
  • 不要走极端
  • 经验法则:
    • 用户合理、频繁的使用其他类型代替你最初选定的类型,那么参数定义为泛型更合适
  • 问题:通过单态化(monomorphization),会为每个使用泛型代码的类型组合生成泛型代码的副本
    • 担心:让很多参数变成泛型 --> 二进制文件过大
  • 解决:动态分发(dynamic dispatch),以忽略不计的性能成本来缓解这个问题
    • 对于以引用方式获取的参数(dyn Trait 不是 Sized 的,需要使用宽指针来使用它们),可以使用动态分发代替泛型参数

例子四

// 假设我们有一个名为 process 的泛型函数,它接受一个类型参数 T 并对其执行某些操作:fn process(value: T) {  // 处理 value 的代码  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(value: &dyn Processable) {  value.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);  process(&a as &dyn Processable);  process(&b as &dyn Processable);  }
  • 使用动态分发(dynamic dispatch):
    • 代码不会对性能敏感:可以接受
    • 在高性能应用中:在频繁调用的热循环中使用动态分发可能会成为一个致命问题
  • 在撰写本文时,只有在简单的 Trait 约束时,才能使用动态分发
    • T: AsRefimpl AsRef
  • 对于更复杂的约束,Rust 无法构造动态分发的虚函数表(vtable)
    • 因此无法使用类似 &dyn Hash + Eq这样的组合约束。
  • 使用泛型时,调用者始终可以通过传递一个 Trait 对象来选择动态分发
  • 反过来不成立:如果你接受一个 Trait 对象作为参数,那么调用者必须提供 Trait 对象,而无法选择使用静态分发
  • 从具体类型开始编写接口,然后逐渐将它们转换为泛型
    • 可行,但不一定是向下兼容

例子五

fn foo(v: &Vec) {  // 处理 v 的代码  // ...}// 现在,我们决定将函数改为使用 Trait 限定 AsRef<[usize]>,// 即 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 类型,// 因为我们将其传递给了接受 &Vec 的 foo 函数。// 然而,在更改为使用特质限定后,编译器只知道 foo 函数// 接受一个实现了 AsRef<[usize]> 特质的类型。// 这里有多个类型满足这个条件,例如 Vec 和 &[usize]。// 因此,编译器无法确定应该将 iter.collect() 的结果解释为哪个具体类型。// 这样的更改将导致编译器无法推断类型,并且调用者的代码将无法通过编译。// 为了解决这个问题,调用者可能需要显示指定期望的类型,例如:// let iter = vec![1, 2, 3].into_iter();// foo(&iter.collect::>());

泛型的优点

  • 可复用:泛型函数能应用在广泛的类型上,同时明确给出了这些类型的必须满足的关系。

  • 静态分派和编译器优化: 每个泛型函数都被专门用于实现了 trait bounds 的具体的类型 (即 单态化 monomorphized ),这意味着:

    1. 调用的 trait 方法是静态生成的,因此是直接对 trait 实现的调用
    2. 编译器能对这些调用做内联 (inline) 和其他优化
  • 内联式布局:如果结构体和枚举体类型具有某个泛型参数 TT的值将在结构体和枚举体里以内联方式排列,不产生任何间接调用。

  • 可推断:由于泛型函数的类型参数通常是推断出来的, 泛型函数可以减少复杂的代码,比如显式转换、通常必须的一些方法调用。

  • 精确的类型:因为泛型给实现了某个 trait 的具体类型一个名称, 从而有可能清楚这个类型需要或创建的地方在哪。比如这个函数:

    fn binary(x: T, y: T) -> T

    会保证消耗和创建具有相同类型 T的值;不可能传入实现了 Trait的但不同名称的两个类型。

泛型的缺点

  • 增加代码大小:单态化泛型函数意味着函数体会被复制。 增加代码大小和静态分派的性能优势之间必须做出衡量。
  • 类型同质化:这是 “精确的类型” 带来的另一面: 如果 T是类型参数,那么它代表一个单独的实际类型。 对于像 Vec这样具体的单独的元素类型也是一样, 而且 Vec实际上为了内联这些元素,进行了专门的处理。 有时候,不同的类型会更有用,参考 trait objects 。
  • 签名冗余:过度使用泛型会造成阅读和理解函数签名更困难。

The Rust RFC Book:https://rust-lang.github.io/rfcs/introduction.html

对象安全(Object Safety)

  • 定义 Trait 时,它是否对象安全,也是契约未写明的一部分
  • 如果 Trait 是对象安全的:
    • 可使用 dyn Trait 将实现该 Trait 的不同类型视为单一通用类型
  • 如果 Trait 不是对象安全的:
    • 编译器会禁止使用 dyn Traie
  • 建议 Trait 是对象安全的(即使稍微降低使用的便利程度):
    • 提供了使用的新方式和灵活性

对象安全:描述一个 Trait 可否安全的包装成 Trait Object

对象安全的 Trait 是满足以下条件的 Trait(RFC 255):

  • 所有的 supertrait 必须是对象安全的
  • Sized 不能作为 supertrait(不能要求 Self: Sized)
  • 不能有任何关联常量
  • 不能有任何带有泛型的关联类型
  • 所有的关联函数必须满足以下条件之一:
    • 可以从 Trait 对象分发的函数(Dispatchable functions):
      • 没有任何类型参数(生命周期参数是允许的)
      • 是一个方法,只在接收器类型中使用 Self
      • 接收器是以下类型之一:
        • &Self(即 &self)
        • &mut Self(即 &mut self
        • Box
        • Rc
        • Arc
        • Pin

          ,其中 P 是上述类型之一

      • 没有 where Self: Sized约束(Self 的接收器类型(即 self)暗含了这一点)
    • 显示不可分发的函数(non-dispatchable functions)要求:
      • 具有 where Self: Sized约束(Self 的接收器类型(即 self)暗含了这一点)

例子六

// 假设我们有一个 Animal 特征,它有两个方法:name 和 speak。// name 方法返回一个&str,表示动物的名字;// speak 方法打印出动物发出的声音。// 我们可以为 Dog 和 Cat 类型实现这个特征:trait Animal {  fn name(&self) -> &str;  fn speak(&self);}struct Dog {  name: String,}impl Animal for Dog {  fn name(&self) -> &str {    &self.name  }    fn speak(&self) {    println!("Woof!");  }}struct Cat {  name: String,}impl Animal for Cat {  fn name(&self) -> &str {    &self.name  }    fn speak(&self) {    println!("Meow!");  }}// 这个 Animal 特征是 object-safe 的,因为它没有返回 Self 类型或使用泛型参数。// 所以我们可以用它来创建一个 trait object:fn main() {  let dog = Dog {    name: "Fido".to_string(),  };  let cat = Cat {    name: "Whiskers".to_string(),  };    let animals: Vec<&dyn Animal> = vec![&dog, &cat];    for animal in animals {    println!("This is {}", animal.name());    animal.speak();  }}// 这样我们就可以用一个统一的类型 Vec<&dyn Animal> 来存储不同类型的动物,// 并且通过 trait object 来调用它们的方法。

例子七

// 但是如果我们给 Animal 特征添加一个新的方法 clone,它返回一个 Self 类型:trait Animal {  fn name(&self) -> &str;  fn speak(&self);  fn clone(&self) -> Self;}// 那么这个特征就不再是 object-safe 的了,// 因为 clone 方法违反了规则:返回类型不能是 Self。// 这样我们就不能用它来创建 trait object 了,// 因为编译器无法知道 Self 具体指代哪个类型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!()  }}fn main() {  let dog = Dog {    name: "Fido".to_string(),  };  let cat = Cat {    name: "Whiskers".to_string(),  };    let animals: Vec<&dyn Animal> = vec![&dog, &cat]; // 报错 the trait `Animal` cannot be made into an object consider moving `clone` to another trait    for animal in animals {    println!("This is {}", animal.name());    animal.speak();  }}

例子八

// 如果我们想让 Animal 特征保持 object-safe,// 我们就不能给它添加返回 Self 类型的方法。// 或者,我们可以给 clone 方法添加一个 where Self: Sized 的特征界定,// 这样他就只能在具体类型上调用,而不是在 trait object 上:trait Animal {  fn name(&self) -> &str;  fn speak(&self);  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 object 了,// 但是我们不能用 trait object 来调用 clone 方法fn main() {  let dog = Dog {    name: "Fido".to_string(),  };  let cat = Cat {    name: "Whiskers".to_string(),  };    cat.clone(); // 只能在具体的类型上调用    let animals: Vec<&dyn Animal> = vec![&dog, &cat];     for animal in animals {    println!("This is {}", animal.name());    animal.speak();    animal.clone(); // 报错 the `clone` method cannot be invoked on a trait object   }}
  • 如果 Trait 必须有泛型方法,考虑:
    • 泛型参数放在 Trait 上
    • 泛型参数可否使用动态分发,来保证Trait 的对象安全

例子九

use std::collections::HashSet;use std::hash::Hash;// 将泛型参数放在 Trait 本身上trait Container {  fn contains(&self, item: &T) -> bool;}// 我们可以为不同的容器类型实现 Container Trait,每个实现都具有自己特定的元素类型。// 例,我们可以为 Vec 和 HashSet 实现 Container Trait:impl Container for VecwhereT: PartialEq,{  fn contains(&self, item: &T) -> bool {    self.iter().any(|x| x == item)  }}impl Container for HashSetwhereT: Hash + Eq,{  fn contains(&self, item: &T) -> bool {    self.contains(item)  }}fn main() {  // 创建一个 Vec 和 HashSet 的实例  let vec_container: Box> = Box::new(vec![1, 2, 3]);  let hashset_container: Box> = Box::new(vec![4, 5, 6].into_iter().collect::>());    // 调用 contains 方法  println!("Vec contains 2: {}", vec_container.contains(&2));  println!("HashSet contains 6: {}", hashset_container.contains(&6));}

例子十

use std::fmt::Debug;// 假设我们有一个 Trait Foo,它有一个泛型方法 bar,它接受一个泛型参数 T:// trait Foo {//fn bar(&self, x: T);//}// 这个 Trait 是不是 object-safe 的呢?答案是:取决于 T 的类型。  注意:它不是对象安全的// 如果 T 是一个具体类型,比如 i32或 String,那么它就不是 object-safe 的,// 因为它需要在运行时知道 T 的具体类型才能调用 bar 方法。// 但如果 T 也是一个 trait object,比如 &dyn Debug 或 &dyn Display,// 那么这个 Trait 就是 object-safe 的,因为它可以用动态分发的方式来调用 T 的方法。// 所以我们可以这样写:trait Foo {  fn bar(&self, x: &dyn Debug);}// 定义一个结构体 A,它实现了 Foo 特征struct A {  name: String,}impl Foo for A {  fn bar(&self, x: &dyn Debug) {    println!("A {} says {:?}", self.name, x);  }}// 定义一个结构体 B,它也实现了 Foo 特征struct B {  id: i32,}impl Foo for B {  fn bar(&self, x: &dyn Debug) {    println!("B {} says {:?}", self.id, x);  }}// 这样我们就可以用 Foo 特征来创建 trait object 了,比如:fn main() {  // 创建两个不同类型的值,它们都实现了 Foo 特征  let a = A {    name: "Alice".to_string(),  };  let b = B { id: 42};    // 创建一个 Vec,它存储了 Foo 的 trait object  let foos: Vec<&dyn Foo> = vec![&a, &b];    // 遍历 Vec,并用 trait object 调用 bar 方法  for foo in foos {    foo.bar(&"Hello"); // "Hello" 实现了 Debug 特征  }}
  • 为实现对象安全,需要做出多大牺牲?
    • 考虑你的 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) {  if data.contains("invalid") {    // 如果输入字符串包含 “invalid”,我们需要修改它    let owned_data: String = data.into_owned();    // 进行一些修改操作    println!("Processed data: {}", owned_data);  } else {    // 如果输入字符串不包含 “invalid”,我们只需要读取它    println!("Data: {}", data);  }}// 在这个例子中,我们使用了 Cow 类型作为参数类型。// 当调用函数时,我们可以传递一个普通的字符串引用(&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,}// File 类型的方法实现impl File {  // 一个构造函数,打开一个文件并返回一个 File 实例  fn open(name: &str) -> Result {    // 使用 std::fs::OpenOptions 打开文件,具有读写权限    let file = std::fs::OpenOptions::new().read(true).write(true).open(name)?;    // 使用 std::os::unix::io::AsRawFd 获取文件描述符    let fd = file.as_raw_fd();    // 返回一个 File 实例,包含 name 和 fd 字段    Ok(File {      name: name.to_string(),      fd,    })  }    // 一个显式的析构器,关闭文件并返回任何错误  fn close(self) -> Result<(), std::io::Error> {    // 使用 std::os::unix::io::FromRawFd 将 fd 转换回 std::fs::File    let file: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(self.id) };    // 使用 std::fs::File::sync_all 将任何挂起的写入刷新到磁盘    file.sync_all()?;    // 使用 std::fs::File::set_len 将文件截断为零字节    file.set_len(0)?;    // 再次使用 std::fs::File::sync_all 刷新截断    file.sync_all()?;    // 丢弃 file 实例,它会自动关闭    drop(file);    // 返回 Ok(())    Ok(())  }}// 一个测试 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!("Error closing file: {}", e),  }  // 检查关闭后的文件大小  let metadata = std::fs::metadata("test.txt").unwrap();  println!("File size: {} bytes", metadata.len());}

注意:显式的析构函数需要在文档中突出显示

  • 添加显式析构函数时会遇问题:
    • 当类型实现了 Drop,在析构函数中无法将该类型的任何字段移出
      • 因为在显式析构函数运行后,Drop::drop 仍会被调用,它接收 &mut self,要求 self 的所有部分都没有被移动
    • Drop 接受的是 &mut self,而不是 self,因此 Drop 无法实现简单地调用显式析构函数并忽略其结果(因为 Drop 不拥有 self)

例子十三

use std::os::fd::AsRawFd;// 一个表示文件句柄的类型struct File {  // 文件名  name: String,  // 文件描述符  fd: i32,}// File 类型的方法实现impl File {  // 一个构造函数,打开一个文件并返回一个 File 实例  fn open(name: &str) -> Result {    // 使用 std::fs::OpenOptions 打开文件,具有读写权限    let file = std::fs::OpenOptions::new().read(true).write(true).open(name)?;    // 使用 std::os::unix::io::AsRawFd 获取文件描述符    let fd = file.as_raw_fd();    // 返回一个 File 实例,包含 name 和 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.id) };    // 使用 std::fs::File::sync_all 将任何挂起的写入刷新到磁盘    file.sync_all()?;    // 使用 std::fs::File::set_len 将文件截断为零字节    file.set_len(0)?;    // 再次使用 std::fs::File::sync_all 刷新截断    file.sync_all()?;    // 丢弃 file 实例,它会自动关闭    drop(file);    // 返回 Ok(())    Ok(())  }}// Drop trait 的实现,用于在值离开作用域时运行一些代码impl Drop for File {  // drop 方法,接受一个可变引用到 self 作为参数  fn drop(&mut self) {    // 调用 close 方法并忽略它的结果    let _ = self.close(); // 报错 不能从 `*self` 中移出值,因为它位于 `&mut` 引用后面    // 打印一条消息,表明文件被丢弃了    println!("Dropping file {}", self.name);  }}// 一个测试 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!("Error closing 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 {  // 一个包装在 Option 中的内部类型  inner: Option,}// 一个内部类型,持有文件名和文件描述符struct InnerFile {  // 文件名  name: String,  // 文件描述符  fd: i32,}// File 类型的方法实现impl File {  // 一个构造函数,打开一个文件并返回一个 File 实例  fn open(name: &str) -> Result {    // 使用 std::fs::OpenOptions 打开文件,具有读写权限    let file = std::fs::OpenOptions::new().read(true).write(true).open(name)?;    // 使用 std::os::unix::io::AsRawFd 获取文件描述符    let fd = file.as_raw_fd();    // 返回一个 File 实例,包含一个 Some(InnerFile) 的 inner 字段    Ok(File {      inner: Some(InnerFile {        name: name.to_string(),        fd,      }),    })  }    // 一个显式的析构器,关闭文件并返回任何错误  fn close(mut self) -> Result<(), std::io::Error> {    // 使用 Option::take 取出 inner 字段的值,并检查是否是 Some(InnerFile)    if let Some(inner) = self.inner.take() {      // 移出 name 和 fd 字段并打印它们      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(self.id) };      // 使用 std::fs::File::sync_all 将任何挂起的写入刷新到磁盘      file.sync_all()?;      // 使用 std::fs::File::set_len 将文件截断为零字节      file.set_len(0)?;      // 再次使用 std::fs::File::sync_all 刷新截断      file.sync_all()?;      // 丢弃 file 实例,它会自动关闭      drop(file);      // 返回 Ok(())      Ok(())    } else {      // 如果 inner 字段是 None,说明文件已经被关闭或丢弃,返回一个错误      Err(std::io::Error::new(        std::io::ErrorKind::Other,        "File already closed or dropped",      ))    }  }}// Drop trait 的实现,用于在值离开作用域时运行一些代码impl Drop for File {  // drop 方法,接受一个可变引用到 self 作为参数  fn drop(&mut self) {    // 使用 Option::take 取出 inner 字段的值,并检查是否是 Some(InnerFile)    if let Some(inner) = self.inner.take() {      // 移出 name 和 fd 字段并打印它们      let name = inner.name;      let fd = inner.id;      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) };      // 丢弃 file 实例,它会自动关闭      drop(file);    } else {      // 如果 inner 字段是 None,说明文件已经被关闭或丢弃,不做任何操作    }  }}// 一个测试 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.inner.as_ref().unwrap().name,     file.inner.as_ref().unwrap().fd  );  // 关闭文件并处理任何错误  match file.close() {    Ok(()) => println!("File closed successfully"),    Err(e) => println!("Error closing 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 {  // 文件名,包装在一个 Option 中  name: Option,  // 文件描述符,包装在一个 Option 中  fd: Option,}// File 类型的方法实现impl File {  // 一个构造函数,打开一个文件并返回一个 File 实例  fn open(name: &str) -> Result {    // 使用 std::fs::OpenOptions 打开文件,具有读写权限    let file = std::fs::OpenOptions::new().read(true).write(true).open(name)?;    // 使用 std::os::unix::io::AsRawFd 获取文件描述符    let fd = file.as_raw_fd();    // 返回一个 File 实例,包含一个 Some(name) 和一个 Some(fd) 的字段    Ok(File {      name: Some(name.to_string()),      fd: Some(fd),    })  }    // 一个显式的析构器,关闭文件并返回任何错误  fn close(mut self) -> Result<(), std::io::Error> {    // 使用 std::mem::take 取出 name 字段的值,并检查是否是 Some(name)    if let Some(name) = std::mem::take(&mut self.name) {      // 使用 std::mem::take 取出 fd 字段的值,并检查是否是 Some(fd)      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 将文件截断为零字节        file.set_len(0)?;        // 再次使用 std::fs::File::sync_all 刷新截断        file.sync_all()?;        // 丢弃 file 实例,它会自动关闭        drop(file);        // 返回 Ok(())        Ok(())      } else {        // 如果 fd 字段是 None,说明文件已经被关闭或丢弃,返回一个错误        Err(std::io::Error::new(          std::io::ErrorKind::Other,          "File descriptor already taken or dropped",      ))    }  } else {    // 如果 name 字段是 None,说明文件已经被关闭或丢弃,返回一个错误    Err(std::io::Error::new(      std::io::ErrorKind::Other,      "File name already taken or dropped",    ))    }  }}// Drop trait 的实现,用于在值离开作用域时运行一些代码impl Drop for File {  // drop 方法,接受一个可变引用到 self 作为参数  fn drop(&mut self) {    // 使用 std::mem::take 取出 name 字段的值,并检查是否是 Some(name)    if let Some(name) = std::mem::take(&mut self.name) {      // 使用 std::mem::take 取出 fd 字段的值,并检查是否是 Some(fd)      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) };        // 丢弃 file 实例,它会自动关闭        drop(file);      } else {        // 如果 fd 字段是 None,说明文件已经被关闭或丢弃,不做任何操作      }    } else {      // 如果 name 字段是 None,说明文件已经被关闭或丢弃,不做任何操作    }  }}// 一个测试 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.inner.as_ref().unwrap().name,     file.inner.as_ref().unwrap().fd  );  // 关闭文件并处理任何错误  match file.close() {    Ok(()) => println!("File closed successfully"),    Err(e) => println!("Error closing file: {}", e),  }  // 检查关闭后的文件大小  let metadata = std::fs::metadata("test.txt").unwrap();  println!("File size: {} bytes", metadata.len());}
  • 方法三:
    • 将数据持有在 ManuallyDrop 类型内,它会解引用内部类型,不必再 unwrap
    • 在 drop 中销毁时,可用 ManuallyDrop::take 来获取所有权
    • 缺点:ManuallyDrop::take 是 unsafe 的

例子十六

// 引入 std 库中的一些模块use std::{mem::ManuallyDrop, os::fd::AsRawFd};// 定义一个表示文件句柄的结构体struct File {  // 文件名,包装在一个 ManuallyDrop 中  name: ManuallyDrop,  // 文件描述符,包装在一个 ManuallyDrop 中  fd: ManuallyDrop,}// 为 File 结构体实现一些方法impl File {  // 一个构造函数,打开一个文件并返回一个 File 实例  fn open(name: &str) -> Result {    // 使用 std::fs::OpenOptions 打开文件,具有读写权限    let file = std::fs::OpenOptions::new().read(true).write(true).open(name)?;    // 使用 std::os::unix::io::AsRawFd 获取文件描述符    let fd = file.as_raw_fd();    // 返回一个 File 实例,包含一个 ManuallyDrop(name) 和一个 ManuallyDrop(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 字段替换为一个空字符串,并获取原来的值    if let name = std::mem::replace(&mut self.name, ManuallyDrop::new(String::new()));     // 使用 std::mem::replace 将 fd 字段替换为一个无效的值,并获取原来的值    if 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) };    // 使用 std::fs::File::sync_all 将任何挂起的写入刷新到磁盘    file.sync_all()?;    // 使用 std::fs::File::set_len 将文件截断为零字节    file.set_len(0)?;    // 再次使用 std::fs::File::sync_all 刷新截断    file.sync_all()?;    // 丢弃 file 实例,它会自动关闭    drop(file);    // 返回 Ok(())    Ok(())  }}// 为 File 结构体实现 Drop trait,用于在值离开作用域时运行一些代码impl Drop for File {  // drop 方法,接受一个可变引用到 self 作为参数  fn drop(&mut self) {    // 使用 ManuallyDrop::take 取出 name 字段的值,并检查是否是空字符串    let name = unsafe { ManuallyDrop::take(&mut self.name) };    // 使用 ManuallyDrop::take 取出 fd 字段的值,并检查是否是无效的值    let fd = unsafe { ManuallyDrop::take(&mut self.id) };    // 打印文件名和文件描述符    println!("Dropping file {:?} with fd {:?}", name, fd);        // 如果 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) };      // 丢弃 file 实例,它会自动关闭      drop(file);    }  }}// 一个测试 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!("Error closing file: {}", e),  }  // 检查关闭后的文件大小  let metadata = std::fs::metadata("test.txt").unwrap();  println!("File size: {} bytes", metadata.len());}
  • 根据实际情况选择方案
    • 倾向于选择第二个方案
      • 只有发现自己处于一堆 Option 中时才切换到其他选项
    • 如果代码足够简单,可轻松检查代码安全性,那么 ManuallyDrop 方案也挺好

关键词: