写点什么

Rust 从 0 到 1- 枚举 - 定义

用户头像
关注
发布于: 2021 年 04 月 14 日
Rust从0到1-枚举-定义

枚举(enums)允许你通过列举所有可能的值来定义一个类型。在很多语言中都有枚举类型,不过不同语言中其功能各不相同。Rust 的枚举与 F#、OCaml 和 Haskell 这样的函数式编程语言中的 algebraic data types 最为相似。

我们先来举一个例子来看看枚举适合的场景。假设我们要处理 IP 地址。目前被广泛使用的两个主要标准有 IPv4 和 IPv6。这是我们的程序可能会遇到的所有可能的 IP 地址类型,所以可以枚举出所有可能的值,这也正是枚举类型名称的由来。因为任何一个 IP 地址只能是 IPv4 和 IPv6 其中的一个,所以 IP 地址的这个特性非常适合枚举数据结构。IPv4 和 IPv6 根本上还是 IP 地址,所以当代码在处理 IP 地址数据的时候应该把它们当作相同的类型。可以通过在代码中定义一个 IpAddrKind 枚举类型来列出可能的 IP 地址类型(V4 和 V6),也称作枚举的成员(variants):

enum IpAddrKind {    V4,    V6}
复制代码

例子中 IpAddrKind 就是一个可以在代码中使用的自定义数据类型。

枚举值

可以通过如下的方式将 IpAddrKind 两个不同成员赋值给变量:

let four = IpAddrKind::V4;let six = IpAddrKind::V6;
复制代码

只需要定义一个函数参数就可以接受任意一个 IP 地址类型:

fn route(ip_kind: IpAddrKind) {     //to do}route(IpAddrKind::V4);route(IpAddrKind::V6);
复制代码

使用枚举还有其他优点。目前 IpAddrKind 只是 IP 地址的类型,并没有存储实际的 IP 地址数据。如果使用结构体,我们可能会向下面这样处理:

enum IpAddrKind {    V4,    V6}struct IpAddr {    kind: IpAddrKind,    address: String}let home = IpAddr {    kind: IpAddrKind::V4,    address: String::from("127.0.0.1")};let loopback = IpAddr {    kind: IpAddrKind::V6,    address: String::from("::1")};
复制代码

在 Rust 中我们可以使用一种更简洁的方式来处理,仅仅使用枚举并将数据直接放进每一个枚举成员而不是作为结构体的一部分。参考下面的例子:

enum IpAddr {        V4(String),        V6(String)}let home = IpAddr::V4(String::from("127.0.0.1"));let loopback = IpAddr::V6(String::from("::1"));
复制代码

看起来和结构体效果差不多,但是用枚举替代结构体还有另一个优势:每个成员可以处理不同类型和数量的数据。IPv4 类型的 IP 地址总是包含四个值在 0 和 255 之间的数字。如果我们想要将 IPv4 地址存储为四个 u8 值而 IPv6 地址仍然使用 String,这就无法使用结构体了。枚举则可以轻易处理的这种场景:

enum IpAddr {    V4(u8, u8, u8, u8),    V6(String),}let home = IpAddr::V4(127, 0, 0, 1);let loopback = IpAddr::V6(String::from("::1"));
复制代码

前面的例子展示了使用枚举来存储两种不同类型 IP 地址的方式。实际上由于 IP 地址的处理非常常见,Rust 的标准库提供了开箱即用的定义!让我们看看标准库是如何定义 IpAddr 的:

struct Ipv4Addr {    // --snip--}struct Ipv6Addr {    // --snip--}enum IpAddr {    V4(Ipv4Addr),    V6(Ipv6Addr),}
复制代码

和我们在例子里的方法类似,不过它将不同的 IP 地址类型定义为了结构体。实际上可以将任意类型的数据放入枚举成员中:例如字符串、数字或者结构体,甚至可以是另一个枚举!另外需要注意的是:虽然 Rust 标准库中包含一个 IpAddr 的定义,仍然可以创建和使用我们自己的定义,并且不会有冲突,因为我们并没有将标准库中的定义引入作用域。

结构体和枚举还有另一个相似点:枚举也可以像结构体一样使用 impl 定义方法:

impl Message {  fn call(&self) {        // 在这里定义方法体  }}let m = Message::Write(String::from("hello"));m.call();
复制代码

Option

Option 是 Rust 标准库中定义的一种枚举类型。Option 类型应用于一个非常常见的场景,即一个变量可能没有值。这在其他编程语言中是很常见的(Null),但是 Rust 中变量并没有 Null 这个状态,编译器会提醒你这一点。但是我们时常又会用到,譬如,当别人付款了我们就发货,如果还没付款(付款信息为空)就发送提醒通知(当然我们也可以用一个特别的值来代替空,但总是会麻烦一些,有时候也不易理解)。Option 就是用来处理“空”的场景,而且编译器会帮助我们检查是否处理了所有应该处理的情况(即有值和没有值的情况),这样就可以避免在其他编程语言中非常常见的 bug(譬如,Java 中的 NullPointerException)。

Tony Hoare,Null 的发明者,在他 2009 年的演讲 “Null References: The Billion Dollar Mistake” 中说到引入 Null 的设计,在之后的四十多年中,引发了无数错误、漏洞和系统崩溃,造成了数十亿美元的损失。

空值的问题在于当你尝试像一个非空值那样使用一个空值时,会出现错误。而空值又是如此容易出现,因此非常容易出现这类错误。但是,空值的概念仍然在很多场景是有意义的:因为某种原因所需要的值目前是无效或缺失的。

为此,Rust 并没有空值,它通过定义于标准库中的枚举类型 Option<T>来实现空值的概念:

enum Option<T> {    Some(T),    None}
复制代码

由于非常的常用,Option<T> 枚举及它的成员是被默认导入的,因此你不需要将其显式引入作用域,可以不需要 Option:: 前缀而直接使用 Some 和 None。(<T> 语法是是一个泛型类型参数,在其他语言也有类似的语法,后面会进行详细介绍。意味着 Option 枚举的 Some 成员可以包含任意类型的数据):

let some_number = Some(5);let some_string = Some("a string");let absent_number: Option<i32> = None;
复制代码

需要注意的是如果使用 None ,需要声明变量的类型,因为编译器只通过 None 无法推断出值的类型。

那么,Option<T> 为什么就比空值要好呢?简而言之,Option<T> 可能有值也可能没值,编译器不允许像一个肯定有效的值那样使用 ,也就是说必须要对可能没值的情况做处理,从而编译器确保了我们在使用值之前处理了为空的情况:

let x: i8 = 5;let y: Option<i8> = Some(5);let sum = x + y;
复制代码

上面的例子在编译时会报错,因为 Option<i8> 和 i8 的类型不同。当在 Rust 中定义一个 i8 类型的值时,编译器会确保它总是有一个有效的值。我们可以自信使用而无需做空值检查。但是当使用 Option<i8> 的时候需要担心可能没有值,而编译器会确保我们在使用它之前处理了为空的情况。

为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 Option<T> 中。并且当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 Option<T> 类型,我们就可以认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,以增加 Rust 代码的安全性。

总之,为了使用 Option<T> 的值,就需要处理空的情况。建议查看 Option<T> 的官方文档,官方实现了大量用于各种情况的方法。

发布于: 2021 年 04 月 14 日阅读数: 11
用户头像

关注

公众号"山 顽石"欢迎大家关注;) 2021.03.23 加入

IT老兵,关注Rust、Java、前端、架构、大数据、AI、算法等;平时喜欢美食、旅游、宠物、健身、篮球、乒乓球等。希望能和大家交流分享。

评论

发布
暂无评论
Rust从0到1-枚举-定义