The Rust programming language 读书笔记——枚举类型

枚举类型(enum),通常也被简称为枚举,它允许我们列举所有可能的值来定义一个类型。
枚举搭配 match 表达式使用模式匹配,可以根据不同的枚举值来执行不同的代码。
Rust 中的枚举更类似于 Haskell 这类函数式编程语言中的代数数据类型(ADT)

定义枚举

假设我们需要对 IP 地址进行处理。目前只有两种广泛被使用的 IP 地址标准:IPv4 和 IPv6。
我们只需要处理这两种情形,且一个地址要么是 IPv4,要么是 IPv6,因此可以使用枚举将所有可能的值(IPv4 和 IPv6)列举出来,作为一种新的数据类型。

enum IpAddrKind {
    V4,
    V6,
}

现在,IpAddrKind 就是一个可以在代码中随处使用的自定义数据类型了。

枚举值

可以参照下面的代码使用 IpAddrKind 中的两个变体(V4V6)创建实例:

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

由于 IpAddrKind:V4IpAddrKind:V6 拥有相同的类型(都是 IpAddrKind),我们可以定义一个接收 IpAddrKind 类型参数的函数来统一处理它们:

fn route(ip_type: IpAddrKind) { }

现在,我们可以使用任意一个变体来调用这个函数了:

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"),
};

实际上,我们可以直接将枚举关联的数据嵌入其变体内,而不用像上面那样将枚举集成至结构体中。

下面的代码直接定义了 IpAddr 枚举,V4V6 两个变体都被关联上了一个 String 值:

enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

我们直接将数据附加到枚举的每个变体中,就不需要额外地使用结构体了。

另外一个枚举替代结构体的优势在于,每个变体可以拥有不同类型和数量的关联数据,同时所有变体仍属于同一个枚举类型

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

参考下面代码中定义的一个 Message 枚举:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

该枚举拥有 4 个内嵌了不同类型数据的变体:

  • Quit 没有关联任何数据
  • Move 包含了一个匿名结构体
  • Write 包含了一个 String
  • ChangeColor 包含了 3 个 i32 值

枚举有些类似于定义多个不同类型的结构体。但枚举除了不会使用 struct 关键字,还将变体们组合到了同一个 Message 类型中。
下面代码中的结构体可以存储与这些变体完全一样的数据:

struct QuitMessage; // 空结构体
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体

两种实现方式的差别在于,如果使用了不同的结构体,则每个结构体都会拥有自己的类型,无法轻易定义一个统一处理这些类型的函数。而前面的 Message 枚举是单独的一个类型

正如我们可以用 impl 关键字定义结构体的方法一样,我们同样可以为 Message 定义自己的方法:

impl Message {
    fn call(&self) {
        // 方法在这里定义
    }
}

fn main() {
    let m = Message::Write(String::from("hello"));
    m.call();
}

Option 枚举及空值处理

Option 是一种定义于标准库中的枚举类型,它描述了一种值可能不存在的情形。借助类型系统,编译器可以自动检查我们是否妥善地处理了所有应该被处理的情况。

Rust 没有像其他语言一样支持空值(Null)。空值本身是一个值,但它的含义却是没有值。
空值的问题在于,当你尝试像使用非空值那样使用空值时,就会触发某种程度上的错误。由于空或非空的属性广泛散布在程序中,因此很难避免引起此类问题。
但空值本身所尝试表达的概念仍是有意义的,它代表了因为某种原因而变得无效或缺失的值。

Rust 中虽然没有空值,但提供了一个拥有类似概念的枚举 Option<T>,它可以用来标识一个值无效或缺失。
Option<T> 在标准库中的定义如下:

enum Option<T> {
    Some(T),
    None,
}

Option<T> 是一个普通的枚举类型,Some<T>None 是该类型的变体。

let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

若使用 None 而不是 Some 变体来进行赋值,则需要明确声明这个 Option<T> 的具体类型,否则编译器无法进行类型推导。

当我们有了一个 Some 值时,就可以确定值是存在的,并且被 Some 所持有;当我们有了一个 None 值时,就知道当前并不存在一个有效的值。
Option<T> 的设计相对于空值的优势在于,Option<T>T 是不同的类型,编译器不会允许我们像使用普通值一样直接去使用 Option<T> 的值。如:

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

运行上述代码会导致编译器报错,因为 i8Option<i8> 是不同的类型。
当我们持有的类型是 i8 时,编译器可以确保该值是有效的。但是当我们持有的类型是 Option<i8> 时,我们必须要考虑值不存在的情况,编译器会迫使我们在使用值之前正确地做出处理操作

为了持有一个可能为空的值,我们总是需要将其显式地放入对应类型的 Option<T> 值当中。当我们随后使用这个值时,也必须显式地处理它可能为空的情况
即在处理 Option<T> 时,必须编写应对每个变体的代码。某些代码只会在持有 Some(T) 值时运行,它们可以使用变体中存储的 T;另外一些代码则只会在持有 None 值时运行,这些代码没有可用的 T 值。

match 表达式就是一种可以用来处理 Option<T> 这类枚举的控制流结构。它允许我们基于枚举拥有的变体来决定运行的代码分支,并允许代码通过模式匹配来获取变体内的数据。

控制流运算符 match

match 是 Rust 中一个强大的控制流运算符,它允许将一个值与一系列模式相比较,并根据匹配的模式执行相应的代码。这些模式可以由字面量、变量名、通配符及许多其他东西组成。

下面的代码会接收一个美国的硬币作为输入,确定硬币的类型并返回其分值:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {
    let coin = Coin::Dime;
    println!("{}", value_in_cents(coin));
}

每个 match 分支所关联的代码同时也是一个表达式,这个表达式运行的结果同时也会作为整个 match 表达式的结果返回。

绑定值的模式

匹配分支还可以绑定匹配对象的部分值,这使得我们能够从枚举变体中提取特定的值。

比如美国的 25 美分硬币 50 个州采用了不同的设计。现在将这些信息添加至枚举中:

#[derive(Debug)] // 方便打印输出默认不支持打印的类型
enum UsState {
    Alabama,
    Alaska,
    // ...
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}.", state);
            25
        }
    }
}

fn main() {
    let alaska = UsState::Alaska;
    let coin = Coin::Quarter(alaska);
    value_in_cents(coin);
    // => State quarter from Alaska.
}

上面的代码中,我们在模式中加入了一个名为 state 的变量用于匹配变体 Coin::Quarter 中的值。当匹配到 Coin::Quarter 时,变量 state 就会绑定到 25 美分所包含的值上。
比如代码中 Coin::Quarter(UsState::Alaska) 作为 coin 的值传入 value_in_cents 函数,最终值 UsState::Alaska 被绑定到变量 state 上。

匹配 Option<T>

可以使用 match 表达式来处理 Option<T>,从 Some 中取出内部的 T 值。
比如编写一个接收 Option<i32> 的函数,若其中有值存在,则将这个值加 1;若其中不存在值,则直接返回 None

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => {
            println!("The result is None");
            None
        }
        Some(i) => {
            println!("The result is {}", i + 1);
            Some(i + 1)
        }
    }
}

fn main() {
    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

需要注意的是,匹配必须穷举所有的可能。尤其是 Option<T> 这个例子中,Rust 会强迫我们明确地处理值为 None 的情形。

简单控制流 if let

if let 能让我们通过一种不那么繁琐的语法结合使用 iflet,处理那些只关心某一种匹配而忽略其他匹配的情况。
下面的代码会匹配一个 Option<u32> 的值,并只在值为 3 时执行代码:

fn main() {
    let some_number = Some(3);
    match some_number {
        Some(3) => println!("three"),
        _ => (),
    }
}

为了满足 match 表达式穷尽性的要求,我们不得不在处理完 Some(3) 变体后额外加上一句 _ => ()
可以使用 if let 以一种更简单的方式实现上述代码:

if let Some(3) = some_number {
    println!("three");
}

还可以在 if let 中搭配使用 else

fn main() {
    let some_number = Some(8);
    if let Some(3) = some_number {
        println!("three");
    } else {
        println!("other number");
    }
}

参考资料

The Rust Programming Language

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容