翻译来源
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)。我们关心两个状态:
opened 和 closed.
复杂 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,
}
只有当Sender
在SenderHasSentNumber
状态中,才有权限访问number_sent
。
我们将损失(少量)性能,编译器不能在相同表示之间优化Sender的转换,但这通常是值得的。
结束语
我希望这个快速演示让您确信,Rust typed move
与phantom types
相结合的强大,是确保代码安全的绝佳工具。 它用在Rust标准库和许多精心设计的第三方库中。
目前,我不知道任何其他编程语言的typed move
(注:特指C + +有无类型的移动语义),但我想其他语言将最终将模仿Rust,如果这个功能也会很受欢迎。 至少就我而言,我不能没有它:)