为什么 Rust 中的枚举(enum)很常见很好用? ? ?

为什么 Rust 中的枚举(enum)很常见很好用? ? ?

将 Rust 作为第二言语学习的人的一个常见的反应往往是,比较于其他任何言语, 枚举(enums) 在Rust中 得到了更好的支撑 。粗略地浏览一下 Google 查找“Rust 中的枚举”会在“人们也查找过”中回来一个成果,询问“为什么 Rust 中的枚举如此好用”。乍一看,这好像是一个好问题;孤登时讲,枚举是代表 有多个值的容器类型 – 例如:方向(东,南,西,北)或季节(春夏秋冬)。可是,Rust 以此为基础并以其他言语中底子不存在的办法增强枚举。

在本文中,咱们将评论 Rust 枚举显着优于其他言语的原因,以及它们的一些用例。

快速回顾enum

首先,为不熟悉的人快速回顾一下 Rust 枚举的实践含义:枚举是能够表明界说数量的变体的类型。考虑以下枚举:

enum Directions {
  Up,
  Down,
  Left,
  Right
}

这代表了一些方向。与运用字符串比较,运用枚举的长处是,当咱们进行形式匹配时,咱们能够简略地匹配不同的变体,而不用考虑字符串的改变。

其他言语中的 enum

关于某些上下文,让咱们看一下枚举在其他言语中的姿态。在 TypeScript 中,在 Google 上粗略查找 Typescript 枚举将回来许多成果,这些成果要么告知您以下内容:

  • 不要在 TypeScript 中运用枚举,由于它们很糟糕
  • 运用枚举只需一种正确的办法
  • 有许多运用枚举的过错办法,这些办法并不是立即显而易见的,由于枚举在编译为 JavaScript 时并不是一个东西

这告知咱们,虽然它们是 TypeScript 中的一项功用,但它们好像并不是很受欢迎 – 通常是由于用户过错或言语怪癖导致运用枚举变得尴尬。

在Java和其他言语中,应该注意的是,枚举显着更加合理,由于它们没有编译到 底层言语 来支撑——因而,必须在类中运用它们或运用办法重写之类的东西来做任何事情(就扩展或完成它们的功用而言)的本质意味着,作为一个全体,枚举并没有真正得到一流的支撑。其他言语,比如Go,纷歧定有枚举,可是你能够用这样的办法来表明枚举(在Go中):

const (
    A base = iota
    C
    T
    G
)

可是,短少官方的 enum 关键字意味着运用起来好像有些令人沮丧。

在 Rust 中,枚举经过作为 相似结构类型获得一流的支撑 – 因而您能够具有一个包括相似结构的结构的枚举,其间枚举变体 中存在 命名值,或者您能够具有一个元组结构按数字引证变量,或者您能够只运用枚举变量自身。尽管除非您实例化它,不然您无法(默认情况下)在没有额外的crate的情况下声明初始值,但经过完成与枚举变体匹配的办法,然后回来您想要的任何内容,将枚举变体转换为另一种类型相对简略。只需你想这样做。

借助 ResultOption 类型,枚举在 Rust 类型系统中也得到了相当多的运用,这两种类型构成了 Rust 中过错处理系统的基础。您还能够经过完成枚举的traits来增强枚举,咱们将在下面看到更多内容。

为 Enum 完成办法

Rust 中的枚举能够专门为枚举完成办法,无需类。咱们来看看下面的办法:

enum Number {
  Odd(i64),
  Even(i64)
}

该枚举代表一个数字以及它是奇数还是偶数。咱们可认为其完成一个办法,根据数字是否能够除以 2 主动实例化枚举变量,如下所示:

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 作为过错类型

检查以下枚举:

#[derive(Debug)]
enum MyError {
  SQLError(sqlx::Error),
  RedisError(redis::RedisError),
  Forbidden,
  BadRequest,
  Unauthorized
}

此枚举表明 Web 应用程序或许失利的几种不同办法:例如,SQL 查询或许会因语法不正确而导致过错,您的 Redis 服务器或许在连接到它时出现过错,而且用户也或许尝试访问他们想要的页面。不应访问或填写过错的表格。

Error trait 要求咱们的枚举类型同时完成 DebugDisplay – 咱们现已为 Debug 特征运用了派生宏,因而咱们不用手动完成它,但咱们确实这样做了需要完成 Display 。咱们能够经过匹配下面函数中的每个枚举变体来做到这一点:

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类型如下所示:

pub trait Error: Debug + Display {
    fn description(&self) -> &str { /* ... */ }
    fn cause(&self) -> Option<&Error> { /* ... */ }
    fn source(&self) -> Option<&(Error + 'static)> { /* ... */ }
}

可是,所有这些函数都是可选的,而且现已有默认完成 – 因而您能够简略地为您的类型完成 Error ,如下所示:

impl Error for MyError {}

从技术上讲,这将为您供给完成 – 当然,假如您想包括更多自界说行为(例如,包括特定枚举变体对保存变量的运用),您或许只想这样做。

当您运用像 Axum 或 Actix 这样的 Web 结构时,通常来说您不用自己完成 Error – 您将完成结构运用的任何类型,同时也完成 Error 。例如,在 Axum 中, IntoResponse 特征完成了 Error 而且也是一个成功的回来类型,因而从技术上讲,您能够将 Result<impl IntoResponse, impl IntoResponse> 作为函数回来签名。让咱们看看您将怎么完成它。

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() 时掩盖类型显示的内容!考虑一个包括暗码和该结构创立时刻的结构:

struct Password {
  password: String,
  created_at: DateTime<Utc>
}

咱们能够用一个枚举来掩盖它:

enum PasswordEnum {
  Secured(Password),
  Unsecured(Password)
}

现在咱们能够做两件事:

  • 能够将暗码显示为一堆星星(基于长度)
  • 能够回来暗码是否安全(根据某些规范)

See below for what this might look like:
请参阅下面的内容,了解它或许是什么姿态:

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 的更多信息吗?这里有一些想法:


原文地址:Why Enums in Rust feel so much better