为什么 Rust 中的枚举(enum)很常见很好用? ? ?
将 Rust 作为第二语言学习的人的一个常见的反应往往是,相比于其他任何语言, 枚举(enums) 在Rust中 得到了更好的支持 。粗略地浏览一下 Google 搜索"Rust 中的枚举"会在"人们也搜索过"中返回一个结果,询问"为什么 Rust 中的枚举如此好用"。乍一看,这似乎是一个好问题;孤立地讲,枚举是代表 有多个值的容器类型 - 例如:方向(东,南,西,北)或季节(春夏秋冬)。然而,Rust 以此为基础并以其他语言中根本不存在的方式增强枚举。
在本文中,我们将讨论 Rust 枚举明显优于其他语言的原因,以及它们的一些用例。
快速回顾enum
首先,为不熟悉的人快速回顾一下 Rust 枚举的实际含义:枚举是能够表示定义数量的变体的类型。考虑以下枚举:
rust
enum Directions {
Up,
Down,
Left,
Right
}
这代表了一些方向。与使用字符串相比,使用枚举的优点是,当我们进行模式匹配时,我们可以简单地匹配不同的变体,而不必考虑字符串的变化。
其他语言中的 enum
对于某些上下文,让我们看一下枚举在其他语言中的样子。在 TypeScript 中,在 Google 上粗略搜索 Typescript 枚举将返回许多结果,这些结果要么告诉您以下内容:
- 不要在 TypeScript 中使用枚举,因为它们很糟糕
- 使用枚举只有一种正确的方法
- 有许多使用枚举的错误方法,这些方法并不是立即显而易见的,因为枚举在编译为 JavaScript 时并不是一个东西
这告诉我们,虽然它们是 TypeScript 中的一项功能,但它们似乎并不是很受欢迎 - 通常是因为用户错误或语言怪癖导致使用枚举变得尴尬。
在Java和其他语言中,应该注意的是,枚举明显更加合理,因为它们没有编译到 底层语言 来支持------因此,必须在类中使用它们或使用方法重写之类的东西来做任何事情(就扩展或实现它们的功能而言)的本质意味着,作为一个整体,枚举并没有真正得到一流的支持。其他语言,比如Go,不一定有枚举,但是你可以用这样的方式来表示枚举(在Go中):
go
const (
A base = iota
C
T
G
)
然而,缺少官方的 enum 关键字意味着使用起来似乎有些令人沮丧。
在 Rust 中,枚举通过作为 类似结构类型获得一流的支持 - 因此您可以拥有一个包含类似结构的结构的枚举,其中枚举变体 中存在 命名值,或者您可以拥有一个元组结构按数字引用变量,或者您可以只使用枚举变量本身。尽管除非您实例化它,否则您无法(默认情况下)在没有额外的crate的情况下声明初始值,但通过实现与枚举变体匹配的方法,然后返回您想要的任何内容,将枚举变体转换为另一种类型相对容易。只要你想这样做。
借助 Result
和 Option
类型,枚举在 Rust 类型系统中也得到了相当多的使用,这两种类型构成了 Rust 中错误处理系统的基础。您还可以通过实现枚举的traits来增强枚举,我们将在下面看到更多内容。
为 Enum 实现方法
Rust 中的枚举能够专门为枚举实现方法,无需类。我们来看看下面的方法:
rust
enum Number {
Odd(i64),
Even(i64)
}
该枚举代表一个数字以及它是奇数还是偶数。我们可以为其实现一个方法,根据数字是否可以除以 2 自动实例化枚举变量,如下所示:
rust
impl Number {
fn from_i64(num: i64) => Self {
match num % 2 == 0 {
true => Number::Even(num),
false => Number::Odd(num)
}
}
}
这消除了许多样板代码,并使使用 Number::from_i64(number)
更容易使用该方法。在其他语言中,您当然可以编写一个返回枚举的单独方法,但是能够在枚举本身下对其进行命名空间使代码更加简洁。
就像 结构体(structs)一样,您也可以在枚举上使用派生宏;派生宏是 Rust 生态系统的重要组成部分,通过在编译时自动生成代码来简化样板代码生成。
Enum 作为错误类型
查看以下枚举:
rust
#[derive(Debug)]
enum MyError {
SQLError(sqlx::Error),
RedisError(redis::RedisError),
Forbidden,
BadRequest,
Unauthorized
}
此枚举表示 Web 应用程序可能失败的几种不同方式:例如,SQL 查询可能会因语法不正确而导致错误,您的 Redis 服务器可能在连接到它时出现错误,并且用户也可能尝试访问他们想要的页面。不应访问或填写错误的表格。
Error trait 要求我们的枚举类型同时实现 Debug
和 Display
- 我们已经为 Debug 特征使用了派生宏,因此我们不必手动实现它,但我们确实这样做了需要实现 Display
。我们可以通过匹配下面函数中的每个枚举变体来做到这一点:
rust
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::SQLError(e) => write!(f, format!("Something went wrong while using an SQL query: {e}")),
MyError::RedisError(e) => write!(f, format!("Something went wrong while using Redis: {e}")),
MyError::Forbidden => write!(f, "User tried to access a page but was forbidden!"),
MyError::BadRequest => write!(f, "User tried to submit a HTTP request but it returned 400!"),
MyError::Unauthorized => write!(f, "User tried to access a page but wasn't authorised!"),
}
}
}
实现此功能还可以免费为我们提供 .to_string()
,并且在完成后将根据枚举变体返回上述内容 - 这对我们很有用!
Error
trait类型如下所示:
rust
pub trait Error: Debug + Display {
fn description(&self) -> &str { /* ... */ }
fn cause(&self) -> Option<&Error> { /* ... */ }
fn source(&self) -> Option<&(Error + 'static)> { /* ... */ }
}
但是,所有这些函数都是可选的,并且已经有默认实现 - 因此您可以简单地为您的类型实现 Error
,如下所示:
rust
impl Error for MyError {}
从技术上讲,这将为您提供实现 - 当然,如果您想包含更多自定义行为(例如,包括特定枚举变体对保存变量的使用),您可能只想这样做。
当您使用像 Axum 或 Actix 这样的 Web 框架时,通常来说您不必自己实现 Error
- 您将实现框架使用的任何类型,同时也实现 Error
。例如,在 Axum 中, IntoResponse
特征实现了 Error
并且也是一个成功的返回类型,因此从技术上讲,您可以将 Result<impl IntoResponse, impl IntoResponse>
作为函数返回签名。让我们看看您将如何实现它。
rust
impl IntoResponse for MyError {
fn into_response(&self) -> Response {
match self {
MyError::SQLError(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error while using SQL: {e}")).into_response(),
MyError::RedisError(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error while using Redis: {e}")).into_response(),
MyError::Forbidden => (StatusCode::FORBIDDEN, "Forbidden!".to_string()).into_response(),
MyError::BadRequest => (StatusCode::FORBIDDEN, "Bad request. Did you fill something out wrong?".to_string()).into_response(),
MyError::Unauthorized => (StatusCode::FORBIDDEN, "Unauthorised!".to_string()).into_response(),
}
}
}
枚举作为错误类型非常有效:通过将错误类型设置为枚举,您只需要与枚举的每个分支进行匹配,而无需使用非详尽的模式标记( _
替换您不想匹配的枚举变体,然后为其返回一些内容。
Enum 作为new-type("包装类型")
我们还可以将类型包装在枚举中,该枚举也可能有多个变体,其中包含来自单个crate或多个crate 的类型。与仅仅公开另一位所述 crate 的 API 相比,这样做的好处是,您可以为自己的程序引入新功能,同时通过不需要与原始类型本身交互来保持向后兼容性 - 您还可以使用它来创建抽象原始类型。例如, poise
crate 构建在 serenity
crate之上,通过将新类型公开为抽象来提供更高级的函数,而不是使用低级函数。
另一个例子:利用我们之前对 Display
trait的了解,我们实际上可以在使用 .to_string()
时覆盖类型显示的内容!考虑一个包含密码和该结构创建时间的结构:
rust
struct Password {
password: String,
created_at: DateTime<Utc>
}
我们可以用一个枚举来覆盖它:
rust
enum PasswordEnum {
Secured(Password),
Unsecured(Password)
}
现在我们可以做两件事:
- 可以将密码显示为一堆星星(基于长度)
- 可以返回密码是否安全(根据某些标准)
See below for what this might look like:
请参阅下面的内容,了解它可能是什么样子:
rust
impl fmt::Display for PasswordEnum {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PasswordEnum::Secured(password) => {
password = password.chars().map(|_| "*".to_owned()).collect::<String>();
write!(f, password);
},
PasswordEnum::Unsecured(password) => {
password = password.chars().map(|_| "*".to_owned()).collect::<String>();
write!(f, password);
},
}
}
}
impl PasswordEnum {
fn is_secure(&self) -> bool {
match self {
PasswordEnum::Secured(_) => true,
PasswordEnum::Unsecured(_) => false
}
}
}
正如您所看到的,通过枚举使用new-type模式来发挥您的优势非常容易!您也可以使用结构来做到这一点。
尾声
感谢您的阅读,我希望您了解如何在 Rust 中使用枚举!枚举非常强大,是 Rust 开发的强大支柱的一部分。
有兴趣了解有关 Rust 的更多信息吗?这里有一些想法:
- 在 此处 了解有关宏的更多信息。
- 了解有关using design patterns in Rust. 。