• 如何在Rust中使用枚举表示状态
  • 发布于 2个月前
  • 177 热度
    0 评论
许多具有系统编程背景的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);
// 堆代码 duidaima.com
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中的枚举比大多数其他语言更强大。它们可以用来优雅地表示状态转换——甚至可以跨越语言边界。

用户评论