一起聊聊在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<Self, &'static str> {
match status {
ACTIVE.0 => Ok(ACTIVE),
INACTIVE.0 => Ok(INACTIVE),
SUSPENDED.0 => Ok(SUSPENDED),
DELETED.0 => Ok(DELETED),
_ => Err("Invalid status"),
}
}
}
使用枚舉表示狀態
枚舉是為域內的狀態建模的好方法。它們以一種非常簡潔的方式表達你的意圖。
#[derive(Debug)]
pub enum UserStatus {
/// 用戶是活躍的,可以完全訪問他們的帳戶和任何相關功能。
Active,
/// 用戶的帳戶處于非活動狀態。該狀態可由用戶或管理員恢復為激活狀態。
Inactive,
/// 該用戶的帳戶已被暫時暫停,可能是由于可疑活動或違反政策。
/// 在此狀態下,用戶無法訪問其帳戶,并且可能需要管理員的干預才能恢復帳戶。
Suspended,
/// 該用戶的帳號已被永久刪除,無法恢復。
/// 與該帳戶關聯的所有數據都可能被刪除,用戶需要創建一個新帳戶才能再次使用該服務。
Deleted,
}
我們可以將這個枚舉插入到User結構體中:
struct User {
// ...
status: UserStatus,
}
但這還不是全部。在Rust中,枚舉比許多其他語言強大得多。例如,可以向枚舉變量中添加數據:
#[derive(Debug)]
pub enum UserStatus {
Active,
Inactive,
Suspended { until: DateTime<Utc> },
Deleted { deleted_at: DateTime<Utc> },
}
我們還可以表示狀態轉換:
use chrono::{DateTime, Utc};
#[derive(Debug)]
pub enum UserStatus {
Active,
Inactive,
Suspended { until: DateTime<Utc> },
Deleted { deleted_at: DateTime<Utc> },
}
impl UserStatus {
/// 暫停用戶直到指定日期
fn suspend(&mut self, until: DateTime<Utc>) {
match self {
UserStatus::Active => *self = UserStatus::Suspended { until },
_ => {}
}
}
/// 激活用戶
fn activate(&mut self) -> Result<(), &'static str> {
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<(), &'static str>{
let mut status = UserStatus::Active;
assert!(status.is_active());
// 暫停到明天
status.suspend(Utc::now() + Duration::days(1));
assert!(status.is_suspended());
status.activate()?;
assert!(status.is_active());
status.delete();
assert!(status.is_deleted());
Ok(())
}
#[test]
fn test_user_status_transition() {
let mut status = UserStatus::Active;
assert!(status.is_active());
status.delete();
assert!(status.is_deleted());
// 無法激活已刪除的用戶
assert!(status.activate().is_err());
}
}
看看我們僅僅用幾行代碼就涵蓋了多少內容!我們可以放心地擴展應用程序,因為我們知道不會意外地刪除用戶兩次或重新激活已刪除的用戶。非法的狀態轉換現在是不可能的!
使用枚舉與C代碼交互
C代碼:
typedef struct {
uint8_t status;
} User;
User *create_user(uint8_t status);
你可以寫一個Rust枚舉來表示狀態:
#[repr(u8)]
#[derive(Debug, PartialEq)]
pub enum UserStatus {
Active = 0,
Inactive,
Suspended,
Deleted,
}
impl TryFrom<u8> for UserStatus {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(UserStatus::Active),
1 => Ok(UserStatus::Inactive),
2 => Ok(UserStatus::Suspended),
3 => Ok(UserStatus::Deleted),
_ => Err(()),
}
}
}
注意到#[repr(u8)]屬性了嗎?它告訴編譯器將此枚舉表示為無符號8位整數。這對于與C代碼的兼容性至關重要。
現在,讓我們用一個安全的Rust包裝器包裝C函數:
extern "C" {
fn create_user(status: u8) -> *mut User;
}
pub fn create_user_wrapper(status: UserStatus) -> Result<User, &'static str> {
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中的枚舉比大多數其他語言更強大。它們可以用來優雅地表示狀態轉換——甚至可以跨越語言邊界。