Rust类型状态机(typestate)

翻译来源

https://yoric.github.io/post/rust-typestate/

内容

很久以前,Rust语言是一种带有typestate的语言。官方称,类型状态早在Rust 1.0之前就已经被抛弃了。 在这篇文章中,我会告诉你Rust社区最糟糕的秘密:Rust仍然有typestate。

什么是typestate

考虑一个表示文件的对象 - MyFile。 在MyFile打开之前,它不能被读取。 一旦MyFile关闭,它就不能被读取。 在这之间,文件可以被读取。 Typestates是一种机制,让类型检查器检查你是否没有犯下以下错误:

fn read_contents_of_file(path: &Path) -> String {
  let mut my_file = MyFile::new(path);
  my_file.open();  // Error: this may fail.
  let result = my_file.read_all(); // Invalid if `my_file.open()` failed.
  my_file.close();

  my_file.seek(Seek::Start); // Error: we have closed `my_file`.
  result
}

在这个例子中,我们犯了两个错误:
1. 读一个可能尚未打开的文件;
2. 在一个已经关闭的文件中seek。
在大多数编程语言中,我们可以轻松设计MyFile的API,以确保第一个错误是不可能的,只需在文件无法打开时抛出异常即可。 一些标准库和框架为了灵活性决定不遵循这个原则,但是这种能力存在于语言本身中。然而,第二个错误难以捉摸。 大多数编程语言都需要添加必要的功能来避免犯种错误,通常是通过在销毁或高级函数调用结束时关闭文件,实际上,我知道的唯一可以完全防止该错误的非学术语言是Rust。

Rust中的常见的typestate

那么,最简单的方法就是在MyFile上为我们的操作引入稳健的类型:

impl MyFile {
    // `open` 是建立 `MyFile`实例的唯一途径.
    pub fn open(path: &Path) -> Result<MyFile, Error> { ... }

    // `seek` 需要 `MyFile`实例.
    pub fn seek(&mut self, pos: Seek) -> Result<(), Error> { ... }

    // `read_all` 需要 `MyFile`实例.
    pub fn read_all(&mut self) -> Result<String, Error> { ... }

    // `close` 使用 `self`, 而不是 `&self` 或 `&mut self`,
    // 这意味着 它 'moves' 了对象, 消耗了它
    pub fn close(self) -> Result<(), Error> { ... }
}
impl Drop for MyFile {
    //引入析构,在未关闭时自动关闭`MyFile`实例
    fn drop(&mut self) { ... }
}

让我们重写例子:

fn read_contents_of_file(path: &Path) -> Result<String, Error> {
  let mut my_file = MyFile::open(path)?;
  // `?` 让编译器检查操作是否成功
  //  成功执行`MyFile::open` 是建立 `MyFile`实例的唯一途径.

  let result = my_file.read_all()?; // 正确.
  my_file.close(); // 正确

  // 由于 `my_file.close()` 消耗了 `my_file`,
  // `my_file` 再也不存在了.

  my_file.seek(Seek::Start)?; // Error, 编译器报错
  result
}

更复杂的情况它也奏效:

fn read_contents_of_file(path: &Path) -> Result<String, Error> {
  // 同上
  let mut my_file = MyFile::open(path)?;
  let result = my_file.read_all()?; // 正确.

  if are_we_happy_yet() {
      my_file.close(); // 正确
  }

 // 由于 `my_file.close()` 消耗了 `my_file`,
  // `my_file` 再也不存在了.

  my_file.seek(Seek::Start)?; //  Error, 编译器报错
  result

  // 如果我们没有关闭 `my_file`, 此时析构将关闭它
  // 
}

这里的关键是:Rust的类型系统强制被”move”后的变量不能被使用。 在我们的例子中,my_file.close()”move”了这个值,即使我们试图将变量隐藏在其他地方,并在调用my_file.close()之后重用它,被编译器也会进行阻止:

fn read_contents_of_file(path: &Path) -> Result<String, Error> {
  // 同上
  let mut my_file = MyFile::open(path)?;
  let result = my_file.read_all()?;

  let mut my_file_sneaky_backup = my_file;
  // 这里我们move `my_file` 到 `my_file_sneaky_backup`.
  // 所以我们不能再也不能使用 `my_file` .

  my_file.close(); // Error, 编译器报错

  my_file_sneaky_backup.seek(Seek::Start)?;
  result

  // 如果我们没有关闭 `my_file`, 此时析构将关闭它
  // 
}

让我们尝试另一种来欺骗编译器,同样是在文件关闭后访问文件:

fn read_contents_of_file(path: &Path) -> Result<String, Error> {
  let my_shared_file = Rc::new(RefCell::new(MyFile::open(path)?));
  // 这里, `my_shared_file` 是一个共享指针,它指向
  //  一个可变的`MyFile`实例, 它与Java, C# or Python中的引用一样

  let result = my_shared_file.borrow_mut()
    .read_all()?; // 正确

  let my_shared_file_sneaky_backup = my_shared_file.clone();
  // 克隆指针`my_shared_file`.

  // 验证副本和原始对象是否可用
  my_shared_file_sneaky_backup.seek(Seek::Start)?; // 正确
  my_shared_file.seek(Seek::Start)?; // 也正确

  // 想当然,也可以关闭 `my_shared_file`
  // 然后操作 `my_shared_file_sneaky_backup`,
  // 与在 Java, C# or Python中的一样

  // 然而 我们不能调用 `my_shared_file.close()`,
  // 因为 `MyFile` 被共享了,
  // 这意味着没有人能 *move* 它
  my_shared_file.close(); // Error, 编译器报错

  my_shared_file_sneaky_backup.seek(Seek::Start)?;
  result

  // 如果我们没有关闭 `my_file`, 此时析构将关闭它
  // 
}

我们再一次被编译器阻止了,事实上,没有明确指定进入”unsafe”模式,则无法在”close”之后使用”seek”.
这个例子展示了Rust typestate 的第一步: 类型移动操作(typed move operation)。我们关心两个状态:
openedclosed.

复杂 typestate

让我们来考虑一个很傻的网络协议:
1. Sender 发送信息”HELLO”.
2. Receiver 接收 “HELLO”,返回”HELLO, YOU”.
3. Sender 接收”HELLO, YOU”,返回随机数。
4. Receiver接收随机数,返回相同的数。
5. Sender 接收数,返回”BYE”。
6. Receiver接收”BYTE”,返回”BYTE,YOU”。
7. 转到 1.

我们可以设计Sender,以确保操作按照正确的顺序进行。 目前,我们不关心识别对方或号码。

为了这个目的,我们将把typed moves 与另一种技术结合起来,这就是强类型函数程序员所熟知的,称为phantom types。

// 一系列大小为0的类型表示sender的状态. 
// 值不重要,重要的是类型
// 用了phantom type技术.
struct SenderReadyToSendHello;
struct SenderHasSentHello;
struct SenderHasSentNumber;
struct SenderHasReceivedNumber;

struct Sender<S> {
  /// 事实上时I/O操作.
  inner: SenderImpl;  
  /// 大小为0的区域, 运行时不存在.
  state: S;
}

/// 以下的函数不论什么状态都会被调用
impl<S> Sender<S> {
    /// 链接 sender的端口.
    fn port(&self) -> usize;

    /// 关闭 sender,.
    fn close(self);
}

/// 只有当状态为 SenderReadyToSendHello时调用.
impl Sender<SenderReadyToSendHello> {
    /// 发送 hello.
    ///
    /// 这个函数消耗了此时的sender ,
    /// 在新状态中返回sender.
    fn send_hello(mut self) -> Sender<SenderHasSentHello> {
        self.inner.send_message("HELLO");
        Sender {
            /// Move the implementation of network I/O.
            /// The compiler is typically smart enough
            /// to realize that this requires no runtime
            /// operation.
            inner: self.inner,
            /// Replace the 0-sized field.
            /// This operation is erased at runtime.
            state: SenderHasSentHello
        }
    }
}

/// 只有当状态为SenderHasSentHello时调用.
impl Sender<SenderHasSentHello> {
    /// 等待直到receiver 发送 "HELLO, YOU",
    /// 返回数字.
    ///
    /// 返回具有`SenderHasSentNumber`状态的sender
    fn wait_respond_to_hello_you(mut self) -> Sender<SenderHasSentNumber> {
        // ...
    }

    /// 若 receiver 返回 "HELLO, YOU", 返回数字
    /// 返回具有`SenderHasSentNumber`状态的sender.
    ///
    /// 否则状态不变.
    fn try_respond_to_hello_you(mut self) -> Result<Sender<SenderHasSentNumber>, Self> {
        // ...
    }
}


/// 只有当状态为SenderHasSentNumber时调用.
impl Sender<SenderHasSentNumber> {
    /// 等待直到 receiver 返回数字, 返回 "BYE".
    ///
    /// 返回具有`SenderReadyToSendHello`的sender.
    fn wait_respond_to_hello_you(mut self) -> Sender<SenderReadyToSendHello> {
        // ...
    }

    /// 若 receiver 返回数字, 响应。 并返回具有`SenderReadyToSendHello`的sender.
    ///
    /// 否则状态不变.
    fn try_respond_to_hello_you(mut self) -> Result<Sender<SenderReadyToSendHello>, Self> {
        // ...
    }
}

根据以上的协议,Sender遵循以下规则:

  • 第1步只能转到第3步,SenderReadyToSendHello
  • 第3步保持不变或转到第5步,SenderHasSentHello
  • 第5步保持不变或转到第1步,SenderHasSentNumber
    类型系统会阻止任何试违背协议的企图。

如果您需要使用网络协议,设备驱动程序,具有特定安全说明的工业设备或OpenGL / DirectX,任何需要与硬件进行复杂交互的其他任何设备,您可能很乐意获得此类保证!

欢迎来到type state 的世界。

提示: 更进一步的type states

顺便说一下,继续我们的网络示例,如果我们想存储服务器发送的数字来检查响应是否匹配,该怎么办?

我们能够简单地将数字存储在SenderHasSentNumber中,

struct SenderHasSentNumber {
    number_sent: u32,
}

只有当SenderSenderHasSentNumber状态中,才有权限访问number_sent
我们将损失(少量)性能,编译器不能在相同表示之间优化Sender的转换,但这通常是值得的。

结束语

我希望这个快速演示让您确信,Rust typed movephantom types相结合的强大,是确保代码安全的绝佳工具。 它用在Rust标准库和许多精心设计的第三方库中。

目前,我不知道任何其他编程语言的typed move(注:特指C + +有无类型的移动语义),但我想其他语言将最终将模仿Rust,如果这个功能也会很受欢迎。 至少就我而言,我不能没有它:)

猜你喜欢

转载自blog.csdn.net/guiqulaxi920/article/details/79426705