一起聊聊在Rust中使用枚举表示状态
许多具有系统编程背景的Rust初学者倾向于使用bool(甚至u8—8位无符号整数类型)来表示“状态”。
例如,如何使用bool来指示用户是否处于活动状态?
struct User {
// ...
active: bool,
}
一开始,这可能看起来不错,但是随着代码库的增长,会发现“active”不是二进制状态。用户可以处于许多不同的状态,用户可能被挂起或删除。但是,扩展User结构体可能会出现问题,因为代码的其他部分有可能依赖active是bool类型。
另一个问题是bool不是自文档化的。active = false是什么意思?用户是否处于非活动状态,或者用户被删除了,或者用户被挂起了?我们不知道!
或者,可以使用一个无符号整数来表示状态:
struct User {
// ...
status: u8,
}
这稍微好一点,因为我们现在可以使用不同的值来表示更多的状态:
const ACTIVE: u8 = 0;
const INACTIVE: u8 = 1;
const SUSPENDED: u8 = 2;
const DELETED: u8 = 3;
let user = User {
// ...
status: ACTIVE,
};
u8的一个常见用例是与C代码交互,在这种情况下,使用u8似乎是唯一的选择。我们还可以将u8包装在一个新类型中!
struct User {
// ...
status: UserStatus,
}
struct UserStatus(u8);
const ACTIVE: UserStatus = UserStatus(0);
const INACTIVE: UserStatus = UserStatus(1);
const SUSPENDED: UserStatus = UserStatus(2);
const DELETED: UserStatus = UserStatus(3);
let user = User {
// ...
status: ACTIVE,
};
这样我们就可以在UserStatus上定义方法:
impl UserStatus {
fn is_active(&self) -> bool {
self.0 == ACTIVE.0
}
}
我们甚至还可以定义一个构造函数来验证输入:
impl UserStatus {
fn new(status: u8) -> Result {
match self {
// A deleted user can't be activated!
UserStatus::Deleted { .. } => return Err("can't activate a deleted user"),
_ => *self = UserStatus::Active
}
Ok(())
}
/// 删除用户,这是一个永久的动作!
fn delete(&mut self) {
if let UserStatus::Deleted { .. } = self {
// 已经删除,不要再设置deleted_at字段。
return;
}
*self = UserStatus::Deleted {
deleted_at: Utc::now(),
}
}
fn is_active(&self) -> bool {
matches!(self, UserStatus::Active)
}
fn is_suspended(&self) -> bool {
matches!(self, UserStatus::Suspended { .. })
}
fn is_deleted(&self) -> bool {
matches!(self, UserStatus::Deleted { .. })
}
}
#[cfg(test)]
mod tests {
use chrono::Duration;
use super::*;
#[test]
fn test_user_status() -> Result {
let user = unsafe { create_user(status as u8) };
if user.is_null() {
Err("Failed to create user")
} else {
Ok(unsafe { *Box::from_raw(user) })
}
}
Rust代码现在使用丰富的enum类型与C代码通信。
总结
Rust中的枚举比大多数其他语言更强大。它们可以用来优雅地表示状态转换——甚至可以跨越语言边界。