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

这篇具有很好参考价值的文章主要介绍了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<str>) -> impl AsRef<str>
    • 进一步放宽契约:要求传入能产生字符串引用的类型,承诺返回值可产生字符串引用

例子一

use std::borrow::Cow;

fn frobnicate3<T: AsRef<str>>(s: T) -> T {
  s
}

fn main() {
  let string = String::from("example");
  let borrowed: &str = "hello";
  let cow: Cow<str> = 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<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 对象
}
  • 不要走极端
  • 经验法则:
    • 用户合理、频繁的使用其他类型代替你最初选定的类型,那么参数定义为泛型更合适
  • 问题:通过单态化(monomorphization),会为每个使用泛型代码的类型组合生成泛型代码的副本
    • 担心:让很多参数变成泛型 --> 二进制文件过大
  • 解决:动态分发(dynamic dispatch),以忽略不计的性能成本来缓解这个问题
    • 对于以引用方式获取的参数(dyn Trait 不是 Sized 的,需要使用宽指针来使用它们),可以使用动态分发代替泛型参数

例子四

// 假设我们有一个名为 process 的泛型函数,它接受一个类型参数 T 并对其执行某些操作:
fn process<T>(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: AsRef<str>impl AsRef<str>
  • 对于更复杂的约束,Rust 无法构造动态分发的虚函数表(vtable)
    • 因此无法使用类似 &dyn Hash + Eq 这样的组合约束。
  • 使用泛型时,调用者始终可以通过传递一个 Trait 对象来选择动态分发
  • 反过来不成立:如果你接受一个 Trait 对象作为参数,那么调用者必须提供 Trait 对象,而无法选择使用静态分发
  • 从具体类型开始编写接口,然后逐渐将它们转换为泛型
    • 可行,但不一定是向下兼容

例子五

fn foo(v: &Vec<usize>) {
  // 处理 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<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 bounds 的具体的类型 (即 单态化 monomorphized ),这意味着:

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

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

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

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

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

泛型的缺点

  • 增加代码大小:单态化泛型函数意味着函数体会被复制。 增加代码大小和静态分派的性能优势之间必须做出衡量。
  • 类型同质化:这是 “精确的类型” 带来的另一面: 如果 T 是类型参数,那么它代表一个单独的实际类型。 对于像 Vec<T> 这样具体的单独的元素类型也是一样, 而且 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<Self>
        • Rc<Self>
        • Arc<Self>
        • Pin<P>,其中 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<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: Hash + Eq,
{
  fn contains(&self, item: &T) -> bool {
    self.contains(item)
  }
}

fn main() {
  // 创建一个 Vec<T> 和 HashSet<T> 的实例
  let vec_container: Box<dyn Container<i32>> = Box::new(vec![1, 2, 3]);
  let hashset_container: Box<dyn Container<i32>> = Box::new(vec![4, 5, 6].into_iter().collect::<HashSet<_>>());
  
  // 调用 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<T>(&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<str>) {
  if data.contains("invalid") {
    // 如果输入字符串包含 “invalid”,我们需要修改它
    let owned_data: String = data.into_owned();
    // 进行一些修改操作
    println!("Processed data: {}", owned_data);
  } else {
    // 如果输入字符串不包含 “invalid”,我们只需要读取它
    println!("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,
}

// File 类型的方法实现
impl File {
  // 一个构造函数,打开一个文件并返回一个 File 实例
  fn open(name: &str) -> Result<File, std::io::Error> {
    // 使用 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<File, std::io::Error> {
    // 使用 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<InnerFile>,
}

// 一个内部类型,持有文件名和文件描述符
struct InnerFile {
  // 文件名
  name: String,
  // 文件描述符
  fd: i32,
}

// File 类型的方法实现
impl File {
  // 一个构造函数,打开一个文件并返回一个 File 实例
  fn open(name: &str) -> Result<File, std::io::Error> {
    // 使用 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<String>,
  // 文件描述符,包装在一个 Option 中
  fd: Option<i32>,
}

// File 类型的方法实现
impl File {
  // 一个构造函数,打开一个文件并返回一个 File 实例
  fn open(name: &str) -> Result<File, std::io::Error> {
    // 使用 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 的

例子十六文章来源地址https://www.toymoban.com/news/detail-477348.html

// 引入 std 库中的一些模块
use std::{mem::ManuallyDrop, os::fd::AsRawFd};

// 定义一个表示文件句柄的结构体
struct File {
  // 文件名,包装在一个 ManuallyDrop 中
  name: ManuallyDrop<String>,
  // 文件描述符,包装在一个 ManuallyDrop 中
  fd: ManuallyDrop<i32>,
}

// 为 File 结构体实现一些方法
impl File {
  // 一个构造函数,打开一个文件并返回一个 File 实例
  fn open(name: &str) -> Result<File, std::io::Error> {
    // 使用 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 方案也挺好

到了这里,关于Rust - 接口设计建议之灵活(flexible)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Rust 基础篇】Rust 模式:高效、安全和灵活的匹配工具

    在编程中,经常需要对数据进行匹配和处理,例如从一个复杂的数据结构中提取特定的值,或者根据不同的情况执行不同的逻辑。Rust是一门现代的系统编程语言,它引入了一种称为\\\"模式\\\"(Pattern)的强大特性,使得数据的匹配和处理变得高效、安全和灵活。本篇博客将深入探

    2024年02月08日
    浏览(46)
  • Rust字符串:安全、高效和灵活的数据类型

    Rust是一种现代的系统级编程语言,以其出色的内存安全性和高性能而受到广泛关注。在Rust中,字符串是一种重要的数据类型,它具有独特的特点,使其在处理文本和字符数据时成为理想的选择。本文将深入探讨Rust字符串的特性,包括安全性、高效性和灵活性,以帮助您更好

    2024年01月19日
    浏览(46)
  • 灵活使用Postman环境变量和全局变量,提高接口测试效率!

     前言: 环境变量和全局变量的概念 环境变量和全局变量的使用方法 1. 定义变量 2. 使用变量 环境变量和全局变量的实例代码

    2024年02月08日
    浏览(40)
  • 【C++】string的接口从生涩到灵活使用——这篇文章就够了

    目录 第一类题目:反转字符串类型  1. 反转字母(初级) 正向迭代器   题目讲解    2.反转字母(中级) reverse和size 题目讲解   3.反转字母(高级)  find和resize   题目讲解   第二类题目:一个字符串的子字符串 1.模拟实现strStr() KMP算法 理论讲解   代码实现   使用next数组

    2024年01月22日
    浏览(30)
  • 接口优化的目录(建议收藏)

    目录 前言 ​编辑 批处理 优点 缺点 场景 同步转异步  优点 缺点 场景 空间换时间 优点 缺点 场景 预处理 优点 缺点 场景 池化技术 优点 缺点 场景 串行改并行 优点 缺点 场景 索引 优点 缺点 场景 避免大事务 优点 缺点 场景 深度分页 优点 缺点 数据一致性问题的特殊处理

    2023年04月09日
    浏览(28)
  • 【C#】当重复使用一段代码倒计时时,定义接口类和通过实现类继承接口方式进行封装方法和体现代码灵活性

    欢迎来到《小5讲堂》 大家好,我是全栈小5。 这是《C#》序列文章,每篇文章将以博主理解的角度展开讲解, 特别是针对知识点的概念进行叙说,大部分文章将会对这些概念进行实际例子验证,以此达到加深对知识点的理解和掌握。 温馨提示:博主能力有限,理解水平有限

    2024年01月19日
    浏览(32)
  • 支付宝代扣接口签约的各种问题排查(建议收藏)

    之前对接支付宝商家扣款的时候,在 签约协议 的部分卡了很久,今天把之前遇到的签约问题汇总记录一下~   首先帮大家捋一下签约的顺序,便于直观理解:     其次还需要知道的是,支付宝的商家扣款的签约接口有 两个 : 一个是 单独签约 接口:     另一个是 支付并签

    2024年02月06日
    浏览(48)
  • 6个步骤轻松实现 postman 接口压力测试(建议收藏)

    这里讲是postman做接口并发测试,基础用法不做赘述 最后: 可以在我的VX公众号:【自动化测试老司机】免费领取一份216页软件测试工程师面试宝典文档资料。以及相对应的视频学习教程免费分享!,其中包括了有基础知识、Linux必备、Shell、互联网程序原理、Mysql数据库、抓

    2024年02月04日
    浏览(28)
  • SD-WAN组网设计原则:灵活、安全、高效

    在实现按需、灵活和安全的 SD-WAN组网 方案中,我们必须遵循一系列关键的设计原则,以确保网络的可靠性和效率。通过以下几点设计原则,SD-WAN能够满足企业对灵活性、安全性和高效性的迫切需求。 灵活的Overlay网络互联 SD-WAN 通过IP地址在站点之间建立Overlay网络,将Overla

    2024年01月19日
    浏览(38)
  • 一文1400字使用Jmeter进行http接口测试【建议收藏】

    本文主要针对http接口进行测试,使用Jmeter工具实现。Jmter工具设计之初是用于做性能测试的,它在实现对各种接口的调用方面已经做的比较成熟,因此,本次直接使用Jmeter工具来完成对Http接口的测试。 一、开发接口测试案例的整体方案: 第一步:我们要分析出测试需求,并

    2024年03月14日
    浏览(61)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包