枚举和模式(Enums and Patterns)

Surprising how much computer stuff makes sense viewed as tragic deprivation of sum types (cf. deprivation of lambdas) —Graydon Hoare

本章的第一个主题是有力的,像山丘一样古老,很乐意帮助你在短时间内(以某种代价)完成许多工作,并且在许多文化中被许多名字所熟知.但这不是魔鬼.它是一种用户定义的数据类型,ML和Haskell黑客早就知道它们是联合类型(sum types),有区别的联合体(unions)或代数数据类型.在Rust中,它们被称为 列举(enumerations) ,或简称为 枚举(enums) .与魔鬼不同,它们非常安全,而且他们所要求的代价也不是什么大的损失.

C++和C#有枚举;你可以使用它们来定义你自己的类型,其值是一组命名常量.例如,你可以定义名为Color的类型,其值有Red,Orange,Yellow等.这种枚举也适用于Rust.但Rust更进一步.Rust枚举也可以包含数据,甚至包含不同类型的数据.例如,Rust的Result<String, io::Error>类型是枚举;这样的值可以是包含StringOk值,也可以是包含io::ErrorErr值.这超过了C++和C#枚举的功能.它更像是一个Cunion—但与联合体不同,Rust枚举是类型安全的.

只要值可能是一个或另一个值,枚举就很有用.使用它们的”代价”是你必须使用模式匹配安全地访问数据,这是本章后半部分的主题.

如果您在Python中使用解包或在JavaScript中进行解构,那么模式也可能很熟悉,但Rust会进一步采用模式. Rust模式有点像所有数据的正则表达式.它们用于测试值是否具有特定的所需形状.他们可以一次从结构或元组中提取几个字段到局部变量.和正则表达式一样,它们很简洁,通常在一行代码中完成所有操作.

枚举(Enums)

简单地,C风格枚举很直接:

  1. enum Ordering {
  2. Less,
  3. Equal,
  4. Greater
  5. }

这声明了一个带有三个可能值的类型Ordering,称为 变体(variants)构造函数(constructors)Ordering::Less,Ordering::EqualOrdering::Greater.这个特殊的枚举是标准库的一部分,因此Rust代码可以自己导入它:

  1. use std::cmp::Ordering;
  2. fn compare(n: i32, m: i32) -> Ordering {
  3. if n < m {
  4. Ordering::Less
  5. } else if n > m {
  6. Ordering::Greater
  7. } else {
  8. Ordering::Equal
  9. }
  10. }

或者用其所有构造函数:

  1. use std::cmp::Ordering;
  2. use std::cmp::Ordering::*; // `*` to import all children
  3. fn compare(n: i32, m: i32) -> Ordering {
  4. if n < m {
  5. Less
  6. } else if n > m {
  7. Greater
  8. } else {
  9. Equal
  10. }
  11. }

在导入构造函数之后,我们可以编写Less而不是Ordering::Less等等,但由于这不太明确,因此通常认为更好的方式是 不(not) 导入它们,除非它使代码更具可读性.

要导入当前模块中声明的枚举的构造函数,请使用self导入:

  1. enum Pet {
  2. Orca,
  3. Giraffe,
  4. ...
  5. }
  6. use self::Pet::*;

在内存中,C风格枚举的值存储为整数.有时,告诉Rust使用哪个整数是有用的:

  1. enum HttpStatus {
  2. Ok = 200,
  3. NotModified = 304,
  4. NotFound = 404,
  5. ...
  6. }

否则Rust将为你分配数字,从0开始.

默认情况下,Rust使用可容纳它们的最小内置整数类型存储C风格的枚举.大多数适合单个字节.

  1. use std::mem::size_of;
  2. assert_eq!(size_of::<Ordering>(), 1);
  3. assert_eq!(size_of::<HttpStatus>(), 2); // 404 doesn't fit in a u8

你可以通过向枚举添加#[repr]属性来覆盖Rust对内存中表示的选择.有关详细信息,请参阅第21章.

允许将C风格枚举转换为整数:

  1. assert_eq!(HttpStatus::Ok as i32, 200);

但是,另一个方向的转换(从整数到枚举)不可以.与C和C++不同,Rust保证枚举值只是enum声明中拼写的值之一.从整数类型到枚举类型的未经检查的强制转换可能会破坏此保证,因此不允许这样做.你可以编写自己的受检查转换:

  1. fn http_status_from_u32(n: u32) -> Option<HttpStatus> {
  2. match n {
  3. 200 => Some(HttpStatus::Ok),
  4. 304 => Some(HttpStatus::NotModified),
  5. 404 => Some(HttpStatus::NotFound),
  6. ...
  7. _ => None
  8. }
  9. }

或使用enum_primitivecrate.它包含一个为你自动生成此类转换代码的宏.

与结构一样,编译器会为你实现类似==运算符的功能,但你必须要求.

  1. #[derive(Copy, Clone, Debug, PartialEq)]
  2. enum TimeUnit {
  3. Seconds, Minutes, Hours, Days, Months, Years
  4. }

枚举可以有方法,就像结构一样:

  1. impl TimeUnit {
  2. /// Return the plural noun for this time unit.
  3. fn plural(self) -> &'staticstr {
  4. match self {
  5. TimeUnit::Seconds => "seconds",
  6. TimeUnit::Minutes => "minutes",
  7. TimeUnit::Hours => "hours",
  8. TimeUnit::Days => "days",
  9. TimeUnit::Months => "months",
  10. TimeUnit::Years => "years"
  11. }
  12. }
  13. /// Return the singular noun for this time unit.
  14. fn singular(self) -> &'static str {
  15. self.plural().trim_right_matches('s')
  16. }
  17. }

C风格的枚举就到这里.更有趣的Rust枚举是包含数据的那种.

带数据的枚举(Enums with Data)

有些程序总是需要显示毫秒级的完整日期和时间,但对于大多数应用程序来说,使用粗略的近似值更加用户友好,比如”两个月前(two months ago)”.我们可以写一个枚举来帮助它:

  1. // A timestamp that has been deliberately rounded off, so our program
  2. /// says "6 months ago" instead of "February 9, 2016, at 9:49 AM".
  3. #[derive(Copy, Clone, Debug, PartialEq)]
  4. enum RoughTime {
  5. InThePast(TimeUnit, u32),
  6. JustNow,
  7. InTheFuture(TimeUnit, u32)
  8. }

此枚举中的两个变体,InThePastInTheFuture采用了参数.这些被称为 元组变体(tuple variants) .与元组结构一样,这些构造函数是创建新RoughTime值的函数.

  1. let four_score_and_seven_years_ago =
  2. RoughTime::InThePast(TimeUnit::Years, 4*20 + 7);
  3. let three_hours_from_now =
  4. RoughTime::InTheFuture(TimeUnit::Hours, 3);

枚举也可以包含 结构变体(struct variants) ,其中包含命名字段,就像普通结构一样:

  1. enum Shape {
  2. Sphere { center: Point3d, radius: f32 },
  3. Cuboid { corner1: Point3d, corner2: Point3d }
  4. }
  5. let unit_sphere = Shape::Sphere { center: ORIGIN, radius: 1.0 };

总而言之,Rust有三种枚举变体,它们与我们在前一章中展示的三种结构相呼应.没有数据的变体对应于单元式结构.元组变体的外观和功能就像元组结构一样.结构变体具有花括号和命名字段.单个枚举可以具有所有三种变体.

  1. enum RelationshipStatus {
  2. Single,
  3. InARelationship,
  4. ItsComplicated(Option<String>),
  5. ItsExtremelyComplicated {
  6. car: DifferentialEquation,
  7. cdr: EarlyModernistPoem
  8. }
  9. }

公有枚举的所有构造函数和字段都是自动公有的.

内存中的枚举(Enums in Memory)

在内存中,带有数据的枚举存储为一个小的整数 标记(tag) ,加上足够的内存来保存最大变体的所有字段. 标记字段用于Rust的内部使用.它告诉哪个构造函数创建了值,因此它创建了哪些字段.

从Rust 1.17开始,RoughTime适合8个字节,如图10-1所示.

图10-1. 内存中的RoughTime值.

但是,Rust没有对枚举布局做出任何承诺,以便为将来的优化留出空间.在某些情况下,可以比图中所示更有效地打包枚举.我们将在本章后面的内容中展示Rust如何针对某些枚举优化掉标记字段.

使用枚举的富数据结构(Rich Data Structures Using Enums)

枚举对于快速实现树状数据结构也很有用.例如,假设Rust程序需要使用任意JSON数据.在内存中,任何JSON文档都可以表示为此Rust类型的值:

  1. enum Json {
  2. Null,
  3. Boolean(bool),
  4. Number(f64),
  5. String(String),
  6. Array(Vec<Json>),
  7. Object(Box<HashMap<String, Json>>)
  8. }

用英语解释数据结构对Rust代码并没有太大改进.JSON标准指定可以出现在JSON文档中的各种数据类型:null,布尔值,数字,字符串,JSON值的数组以及具有字符串键和JSON值的对象.Json枚举简单地说明了这些类型.

这不是一个假想的例子.在serde_json(一个Rust结构的序列化库,它是crates.io上下载次数最多的crates之一)中可以找到一个非常相似的枚举.

表示ObjectHashMap周围的Box仅用于使所有Json值更紧凑.在内存中,Json类型的值占用四个机器字.StringVec值是三个字,Rust添加一个标记字节.NullBoolean值中没有足够的数据用尽所有空间,但所有Json值必须大小相同.额外的空间未使用.图10-2显示了Json值在内存中的实际样子的一些示例.

HashMap仍然要更大.如果我们不得不在每个Json值中为它留出空间,那么它们会非常大,八个字左右.但Box<HashMap>是一个单个字:它只是指向堆分配数据的指针.我们可以通过装箱更多的字段( boxing more fields)使Json更加紧凑.

图10-2. 内存中的Json值.

这里值得注意的是设置它是多么容易.在C++中,或许要为此编写一个类:

  1. class JSON {
  2. private:
  3. enum Tag {
  4. Null, Boolean, Number, String, Array, Object
  5. };
  6. union Data {
  7. bool boolean;
  8. double number;
  9. shared_ptr<string> str;
  10. shared_ptr<vector<JSON>> array;
  11. shared_ptr<unordered_map<string, JSON>> object;
  12. Data() {}
  13. ~Data() {}
  14. ...
  15. };
  16. Tag tag;
  17. Data data;
  18. public:
  19. bool is_null() const { return tag == Null; }
  20. bool is_boolean() const { return tag == Boolean; }
  21. bool get_boolean() const {
  22. assert(is_boolean());
  23. return data.boolean;
  24. }
  25. void set_boolean(bool value) {
  26. this->~JSON(); // clean up string/array/object value
  27. tag = Boolean;
  28. data.boolean = value;
  29. }
  30. ...
  31. };

在30行代码中,我们几乎没有开始工作.该类需要构造函数,析构函数和赋值运算符.另一种方法是创建一个具有基类JSON和子类JSONBoolean,JSONString等的类层次结构.无论哪种方式,当它完成时,我们的C++JSON库将拥有十几种方法.其他程序员需要一些阅读才能拿起并使用它.整个Rust枚举是八行代码.

泛型枚举(Generic Enums)

枚举可以是泛型的.以下两个示例来自标准库,是该语言中最常用的数据类型:

  1. enum Option<T> {
  2. None,
  3. Some(T)
  4. }
  5. enum Result<T, E> {
  6. Ok(T),
  7. Err(E)
  8. }

这些类型现在已经足够熟悉了,泛型枚举的语法与泛型结构相同.一个不明显的细节是,当类型TBox或其他一些智能指针类型时,Rust可以消除Option<T>的标记字段.Option<Box <i32 >>作为单个机器字存储在内存中,0表示None,非零表示Some包装值.

只需几行代码即可构建泛型数据结构:

  1. // An ordered collection of `T`s.
  2. enum BinaryTree<T> {
  3. Empty,
  4. NonEmpty(Box<TreeNode<T>>)
  5. }
  6. // A part of a BinaryTree.
  7. struct TreeNode<T> {
  8. element: T,
  9. left: BinaryTree<T>,
  10. right: BinaryTree<T>
  11. }

这几行代码定义了一个BinaryTree类型,可以存储任意数量的T类型的值.

这两个定义中包含了大量信息,因此我们将花时间将代码逐字翻译成英语.每个BinaryTree值都是EmptyNonEmpty.如果它为Empty,则它根本不包含任何数据.如果是NonEmpty,那么它有一个Box,指向堆分配的TreeNode的指针.

每个TreeNode值包含一个实际元素,以及另外两个BinaryTree值.这意味着树可以包含子树,因此NonEmpty树可以包含任意数量的后代.

一个BinaryTree<&str>类型值的草图如图10-3所示.与Option<Box<T>>一样,Rust删除了标记字段,因此BinaryTree值只是一个机器字.

图10-3. 一个含有6个字符串的BinaryTree.

在此树中构建任何特定节点都很简单:

  1. use self::BinaryTree::*;
  2. let jupiter_tree = NonEmpty(Box::new(TreeNode {
  3. element: "Jupiter",
  4. left: Empty,
  5. right: Empty
  6. }));

较大的树可以用较小的树构建:

  1. let mars_tree = NonEmpty(Box::new(TreeNode {
  2. element: "Mars",
  3. left: jupiter_tree,
  4. right: mercury_tree
  5. }));

当然,这个赋值将jupiter_nodemercury_node的所有权转移到它们的新的父节点.

树的其余部分遵循相同的模式.根节点与其他节点没有区别:

  1. let tree = NonEmpty(Box::new(TreeNode {
  2. element: "Saturn",
  3. left: mars_tree,
  4. right: uranus_tree
  5. }));

在本章的后面,我们将展示如何在BinaryTree类型上实现add方法,以便我们可以编写:

  1. let mut tree = BinaryTree::Empty;
  2. for planet in planets {
  3. tree.add(planet);
  4. }

无论你来自哪种语言,在Rust中创建像BinaryTree这样的数据结构都可能需要一些练习.首先放置Box的位置并不明显.找到一个可行的设计的一种方法是绘制一幅如图10-3所示的图片,显示你希望如何在内存中布置内容.然后从图片返回到代码.每个矩形集合都是一个结构或元组;每个箭头都是一个Box或其他智能指针.弄清楚每个字段的类型是一个难题,但是一个可管理的难题.解决难题的好处是控制程序的内存使用情况.

现在来到我们在介绍中提到的”代价(price)”.枚举的标记字段占用一点内存,在最坏的情况下最多8个字节,但这通常可以忽略不计.枚举的真正缺点(如果可以称之为)是Rust代码不能不顾一切然后尝试访问字段,无论它们是否实际存在于值中:

  1. let r = shape.radius; // error: no field `radius` on type `Shape`

访问枚举中数据的唯一方法是安全的方式:使用模式.

模式(Patterns)

回想一下本章前面我们的RoughTime类型的定义:

  1. enum RoughTime {
  2. InThePast(TimeUnit, u32),
  3. JustNow,
  4. InTheFuture(TimeUnit, u32)
  5. }

假设你有一个RoughTime值,并且你希望将其显示在网页上.你需要访问值内的TimeUnitu32字段.Rust不允许你通过编写rough_time.0rough_time.1直接访问它们,因为毕竟,值可能是RoughTime::JustNow,它没有字段.但是,你如何获得数据呢?

你需要一个match表达式:

  1. 1 fn rough_time_to_english(rt: RoughTime) -> String {
  2. 2 match rt {
  3. 3 RoughTime::InThePast(units, count) =>
  4. 4 format!("{} {} ago", count, units.plural()),
  5. 5 RoughTime::JustNow =>
  6. 6 format!("just now"),
  7. 7 RoughTime::InTheFuture(units, count) =>
  8. 8 format!("{} {} from now", count, units.plural())
  9. 9 }
  10. 10 }

match执行模式匹配;在此示例中, 模式(pattern) 是出现在第3,5和7行的=>符号之前的部分.与RoughTime值匹配的模式看起来就像用于创建RoughTime值的表达式.这不是巧合.表达式 产生(produce) 值,模式 消费(consume) 价值.这两个使用了很多相同的语法.

让我们逐步了解这个match表达式运行时会发生什么.假设rt是值RoughTime::InTheFuture(TimeUnit::Months, 1).Rust首先尝试将此值与第3行上的模式匹配.如图10-4所示,它不匹配.

图10-4. 一个RoughTime值和不匹配的模式.

枚举,结构或元组上的模式匹配就像Rust正在进行简单的从左到右扫描一样,检查模式的每个组件以查看该值是否与之匹配.如果没有,Rust继续进行下一个模式.

第3行和第5行的模式不匹配.但第7行的模式成功(图10-5).

图10-5. 一个成功的匹配.

当模式包含unitscount等简单标识符时,它们将成为模式后面的代码中的局部变量.将值中存在的任何内容复制或移动到新变量中.Rust将TimeUnit::Months存储在units,1存储在count中,运行第8行,并返回字符串"1 months from now".

该输出有一个小的语法问题,可以通过在match中添加另一个分支来修复:

  1. RoughTime::InTheFuture(unit, 1) =>
  2. format!("a {} from now", unit.singular()),

只有当count字段正好为1时,此分支才匹配.请注意,必须在第7行之前添加此新代码.如果我们在最后添加它,Rust将永远不会到达它,因为第7行上的模式匹配所有InTheFuture值.如果你犯了这种错误,Rust编译器会警告”unreachable pattern”.

不幸的是,即使使用新代码,RoughTime::InTheFuture(TimeUnit::Hours, 1)仍然存在问题:结果"a hour from now"并不完全正确.这是英语.这也可以通过向match中添加另一个分支来解决.

如此示例所示,模式匹配与枚举协同工作,甚至可以测试它们包含的数据,使match成为C的switch语句的强大,灵活的替代品.

到目前为止,我们只看到了与枚举值匹配的模式.除此之外还有更多.Rust模式它们自己就是小语言,如表10-1所示.我们用本章其余的大部分内容来讨论此表中显示的功能.

表10-1. 模式.

模式类型 示例 说明
字面量 100
"name"
匹配一个确切的值;const的名称也是允许的
范围(range) 0 ... 100
'a' ... 'k'
匹配范围内的任何值,包括结束值
通配符 _ 匹配任何值并忽略它
变量 name
mut count
_类似,但将值移动或复制到新的局部变量中
ref变量 ref field
ref mut field
借用对匹配值的引用,而不是移动或复制它
与子模式绑定 val @ 0 ... 99
ref circle @ Shape::Circle
使用左侧的变量名称匹配@右侧的模式
枚举模式 Some(value)
None
Pet::Orca
元组模式 (key, value)
(r, g, b)
结构模式 Color(r, g, b)
Point { x, y }
Card { suit: Clubs, rank: n }
Account { id, name, .. }
引用 &value
&(k, v)
仅匹配引用值
多种模式 `’a’ ‘A’` 仅在match中(在let等中无效)
守卫表达式(Guard expression) x if x * x <= r2 仅在match中(在let等中无效)

模式中的字面量,变量和通配符(Literals, Variables, and Wildcards in Patterns)

到目前为止,我们已经展示了使用枚举的match表达式.其他类型也可以匹配.当你需要类似Cswitch语句的内容时,请用整数值使用match,像01这样的整数字面量可以作为模式:

  1. match meadow.count_rabbits() {
  2. 0 => {} // nothing to say
  3. 1 => println!("A rabbit is nosing around in the clover."),
  4. n => println!("There are {} rabbits hopping about in the meadow", n)
  5. }

如果草地上没有兔子,则模式0匹配.如果只有一个,则1匹配.如果有两只或两只以上的兔子,我们会达到第三种模式,n.此模式只是一个变量名称.它可以匹配任何值,匹配的值被移动或复制到新的局部变量中.所以在这种情况下,meadow.count_rabbits()的值存储在一个新的局部变量n中,然后我们打印它.

其他字面量也可以用作模式,包括布尔值,字符甚至字符串:

  1. let calendar =
  2. match settings.get_string("calendar") {
  3. "gregorian" => Calendar::Gregorian,
  4. "chinese" => Calendar::Chinese,
  5. "ethiopian" => Calendar::Ethiopian,
  6. other => return parse_error("calendar", other)
  7. };

在此示例中,other用作捕获所有(catch-all)模式,如上例中的n.这些模式与switch语句中的default情况起着相同的作用,匹配与任何其他模式都不匹配的值.

如果你需要一个捕获所有模式,但你不关心匹配到的值,你可以使用单个下划线_作为模式, 通配符模式(wildcard pattern)

  1. let caption =
  2. match photo.tagged_pet() {
  3. Pet::Tyrannosaur => "RRRAAAAAHHHHHH",
  4. Pet::Samoyed => "*dog thoughts*",
  5. _ => "I'm cute, love me"// generic caption, works for any pet
  6. };

通配符模式匹配任何值,但不将其存储在任何位置.由于Rust要求每个match表达式都要处理所有可能的值,因此最后通常需要使用通配符.即使你非常确定剩下的情况不会发生,你也必须至少添加一个恐慌的后备分支:

  1. // There are many Shapes, but we only support "selecting"
  2. // either some text, or everything in a rectangular area.
  3. // You can't select an ellipse or trapezoid.
  4. match document.selection() {
  5. Shape::TextSpan(start, end) => paint_text_selection(start, end),
  6. Shape::Rectangle(rect) => paint_rect_selection(rect),
  7. _ => panic!("unexpected selection type")
  8. }

值得注意的是,现有变量不能用于模式中.假设我们正在实现一个六边形空间的棋盘游戏,玩家只需点击移动一块.要确认点击是有效的,我们可能会尝试这样的事情:

  1. fn check_move(current_hex: Hex, click: Point) -> game::Result<Hex> {
  2. match point_to_hex(click) {
  3. None =>
  4. Err("That's not a game space."),
  5. Some(current_hex) => // try to match if user clicked the current_hex
  6. // (it doesn't work: see explanation below)
  7. Err("You are already there! You must click somewhere else."),
  8. Some(other_hex) =>
  9. Ok(other_hex)
  10. }
  11. }

这会失败,因为模式中的标识符引入了 变量.这里的模式Some(current_hex)创建一个新的局部变量current_hex,它遮蔽参数current_hex.Rust发出了关于此代码的几个警告—特别是,match的最后一个分支是无法到达的.要修复它,请使用if表达式:

  1. Some(hex) =>
  2. if hex == current_hex {
  3. Err("You are already there! You must click somewhere else")
  4. } else {
  5. Ok(hex)
  6. }

在接下来的几页中,我们将介绍守卫(guard),它提供了解决此问题的另一种方法.

元组和结构模式(Tuple and Struct Patterns)

元组模式匹配元组.只要你想在单个match中获得多个数据,它们就很有用:

  1. fn describe_point(x: i32, y: i32) -> &'staticstr {
  2. use std::cmp::Ordering::*;
  3. match (x.cmp(&0), y.cmp(&0)) {
  4. (Equal, Equal) => "at the origin",
  5. (_, Equal) => "on the x axis",
  6. (Equal, _) => "on the y axis",
  7. (Greater, Greater) => "in the first quadrant",
  8. (Less, Greater) => "in the second quadrant",
  9. _ => "somewhere else"
  10. }
  11. }

结构模式使用花括号,就像结构表达式一样.它们包含每个字段的子模式:

  1. match balloon.location {
  2. Point { x: 0, y: height } =>
  3. println!("straight up {} meters", height),
  4. Point { x: x, y: y } =>
  5. println!("at ({}m, {}m)", x, y)
  6. }

在此示例中,如果第一个分支匹配,则balloon.location.y存储在新的局部变量height中.

假设balloon.locationPoint { x: 30, y: 40 }.与往常一样,Rust依次检查每个模式的每个组件(图10-6).

图10-6. 使用结构的模式匹配.

第二分支匹配,因此输出将为”at (30m, 40m)“.

Point { x: x, y: y }这样的模式在匹配结构时很常见,冗余名称是视觉上的混乱,所以Rust有一个简写:Point {x, y}.意思是一样的.此模式仍然将点x的字段存储在新的局部(变量)x中,将y字段存储在新的局部(变量)y中.

即使使用简写,当我们只关心几个字段时,匹配大型结构也很麻烦:

  1. match get_account(id) {
  2. ...
  3. Some(Account {
  4. name, language, // <--- the 2 things we care about
  5. id: _, status: _, address: _, birthday: _, eye_color: _,
  6. pet: _, security_question: _, hashed_innermost_secret: _,
  7. is_adamantium_preferred_customer: _ }) =>
  8. language.show_custom_greeting(name)
  9. }

要避免这种情况,请使用..告诉Rust你不关心任何其他字段:

  1. Some(Account { name, language, .. }) =>
  2. language.show_custom_greeting(name)

引用模式(Reference Patterns)

Rust模式支持两个用于处理引用的功能.ref模式借用匹配值的部分.&模式匹配引用.我们将首先介绍ref模式.

匹配不可复制的值会移动该值.继续使用帐户(account)示例,此代码无效:

  1. match account {
  2. Account { name, language, .. } => {
  3. ui.greet(&name, &language);
  4. ui.show_settings(&account); // error: use of moved value `account`
  5. }
  6. }

这里,字段account.nameaccount.language被移动到局部变量namelanguage中.account的其余部分被删除.这就是我们之后无法用account调用方法的原因.

如果namelanguage都是可复制值,Rust会复制字段而不是移动它们,这段代码就可以了.但假设这些是String.我们可以做什么?

我们需要一种模式, 借用(borrow) 匹配的值而不是移动它们.ref关键字就是这样做的:

  1. match account {
  2. Account { ref name, ref language, .. } => {
  3. ui.greet(name, language);
  4. ui.show_settings(&account); // ok
  5. }
  6. }

现在,局部变量namelanguage是对account中相应字段的引用.由于account只是被借用而不被消费,因此可以继续在其上调用方法.

你可以使用ref mut借用mut引用:

  1. match line_result {
  2. Err(ref err) => log_error(err), // `err` is &Error (shared ref)
  3. Ok(ref mut line) => { // `line` is &mut String (mut ref)
  4. trim_comments(line); // modify the String in place
  5. handle(line);
  6. }
  7. }

模式Ok(ref mut line)匹配任何成功结果,并借用成功值的mut引用存储在其中.

相反类型的引用模式是&模式.以&开始的模式匹配一个引用.

  1. match sphere.center() {
  2. &Point3d { x, y, z } => ...
  3. }

在此示例中,假设sphere.center()返回对sphere的私有字段的引用,这是Rust中的常见模式.返回的值是Point3d的地址.如果中心位于原点,则sphere.center()返回&Point3d {x: 0.0, y: 0.0, z: 0.0 }.

因此模式匹配如图10-7所示进行.

图10-7. 使用引用的模式匹配.

这有点棘手,因为Rust在这里跟随一个指针,我们通常与*运算符关联,而不是&运算符.要记住的是模式和表达式是天生的对立面.表达式(x, y)将两个值组成一个新元组,但模式(x, y)则相反:它匹配一个元组并将两个值分开.与&相同.在表达式中,&创建引用.在模式中,&匹配引用.

匹配引用符合我们所期望的所有规则.生命周期强制执行.你无法通过共享引用获取mut访问.并且你不能从引用中移动一个值,即使是mut引用.当我们匹配&Point3d { x, y, z }时,变量x,yz接收坐标的副本,使原始的Point3d值保持不变.它起作用,因为这些字段是可复制的.如果我们在具有不可复制字段的结构上尝试相同的操作,我们将收到错误:

  1. match friend.borrow_car() {
  2. Some(&Car { engine, .. }) => // error: can't move out of borrow
  3. ...
  4. None => {}
  5. }

为零件报废一辆借来的汽车并不好,而Rust也不能容忍它.你可以使用ref模式借用对零件的引用.你就是不能拥有它.

  1. Some(&Car { ref engine, .. }) => // ok, engine is a reference

让我们看一个&模式的另一个例子.假设我们在字符串中的字符上有一个迭代器chars,并且它有一个方法chars.peek()它返回一个Option<&char>:对下一个字符的引用(如果有的话).(Peekable迭代器确实会返回一个Option<&ItemType>,我们将在第15章中看到.)

程序可以使用&模式来获取指向的字符:

  1. match chars.peek() {
  2. Some(&c) => println!("coming up: {:?}", c),
  3. None => println!("end of chars")
  4. }

匹配多种可能性(Matching Multiple Possibilities)

垂直条(|)可用于在单个匹配分支中组合多个模式:

  1. let at_end =
  2. match chars.peek() {
  3. Some(&'\r') | Some(&'\n') | None => true,
  4. _ => false
  5. };

在表达式中,|是按位OR运算符,但在这里它更像是正则表达式中的|符号.如果chars.peek()匹配三种模式中的任何一种,则at_end设置为true.

使用...来匹配整个值范围.范围模式包括开始和结束值,因此'0' ... '9'匹配所有ASCII数字:

  1. match next_char {
  2. '0' ... '9' =>
  3. self.read_number(),
  4. 'a' ... 'z' | 'A' ... 'Z' =>
  5. self.read_word(),
  6. ' ' | '\t' | '\n' =>
  7. self.skip_whitespace(),
  8. _ =>
  9. self.handle_punctuation()
  10. }

模式中的范围是 包容性的(inclusive) ,因此'0''9'都匹配模式'0' ... '9'.相比之下,范围表达式(用两个点写成,如for n in 0..100中)是半开的,或者是 独占的(exclusive) (覆盖0但不覆盖100).不一致的原因很简单,独占范围对于循环和切片更有用,但是包含范围在模式匹配中更有用.

模式守卫(Pattern Guards)

使用if关键字添加一个 守卫(guard)match分支.只有当守卫求值为true时,匹配才会成功:

  1. match robot.last_known_location() {
  2. Some(point) if self.distance_to(point) < 10 =>
  3. short_distance_strategy(point),
  4. Some(point) =>
  5. long_distance_strategy(point),
  6. None =>
  7. searching_strategy()
  8. }

如果一个模式移动任何值,你就不能对它放置守卫.守卫可能会求值为false,然后Rust会继续下一个模式.但是,如果你已将位移出匹配值,则无法做到这一点.因此,前面的代码仅在point是可复制的时才起作用,如果不是,我们会收到错误:

  1. error[E0008]: cannot bind by-move into a pattern guard
  2. --> enums_move_into_guard.rs:19:18
  3. |
  4. 19 | Some(point) if self.distance_to(point) < 10 =>
  5. | ^^^^^ moves value into pattern guard

然后,解决方法是将模式更改为借用point而不是移动它:`Some(ref point).

@模式(@ patterns)

最后, x @ pattern 与给定pattern完全匹配,但是在成功时,它不是为匹配值的部分创建变量,而是创建单个变量x并将整个值移动或复制到其中.例如,假设你有以下代码:

  1. match self.get_selection() {
  2. Shape::Rect(top_left, bottom_right) =>
  3. optimized_paint(&Shape::Rect(top_left, bottom_right)),
  4. other_shape =>
  5. paint_outline(other_shape.get_outline()),
  6. }

请注意,第一种情况是解包Shape::Rect值,只是为了在下一行重建相同的Shape::Rect值.这可以重写为使用@模式:

  1. rect @ Shape::Rect(..) =>
  2. optimized_paint(&rect),

@模式对范围也很有用:

  1. match chars.next() {
  2. Some(digit @ '0' ... '9') => read_number(digit, chars),
  3. ...
  4. }

允许模式的地方(Where Patterns Are Allowed)

尽管模式在match表达式中最为突出,但它们也可以在其他几个地方使用,通常代替标识符.含义总是相同的:Rust不是仅仅将值存储在单个变量中.而是使用模式匹配来将值分开.

这意味着模式可用于…

  1. // ...unpack a struct into three new local variables
  2. let Track { album, track_number, title, .. } = song;
  3. // ...unpack a function argument that's a tuple
  4. fn distance_to((x, y): (f64, f64)) -> f64 { ... }
  5. // ...iterate over keys and values of a HashMap
  6. for (id, document) in &cache_map {
  7. println!("Document #{}: {}", id, document.title);
  8. }
  9. // ...automatically dereference an argument to a closure
  10. // (handy because sometimes other code passes you a reference
  11. // when you'd rather have a copy)
  12. let sum = numbers.fold(0, |a, &num| a + num);

每个都节省了两到三行样板代码.在其他一些语言中存在相同的概念:在JavaScript中,它被称为 解构(destructuring) ;在Python中,解包(unpacking) .

请注意,在所有四个示例中,我们使用保证匹配的模式.Point3d {x, y, z}模式匹配Point3d结构类型的每个可能值;(x, y)匹配任何(f64, f64)对;等等.始终匹配的模式在Rust中是特殊的.它们被称为 无可辩驳的模式(irrefutable patterns) ,它们是这里显示的四个地方(在let之后,在函数参数中,在for之后和在闭包参数中)中允许的唯一模式.

可辩驳的模式(refutable pattern) 是可能不匹配的模式,如Ok(x)它与错误结果不匹配,或者'0' ... '9',它与字符'Q'不匹配.可以在match分支中使用可辩驳模式,因为match是为它们设计的:如果一个模式不匹配,很清楚接下来会发生什么.上面的四个示例是Rust程序中模式可以很方便,但语言不允许匹配失败的地方.

如果letwhile let表达式也可以使用可辩驳模式,这可以用于…

  1. // ...handle just one enum variant specially
  2. if let RoughTime::InTheFuture(_, _) = user.date_of_birth() {
  3. user.set_time_traveler(true);
  4. }
  5. // ...run some code only if a table lookup succeeds
  6. if let Some(document) = cache_map.get(&id) {
  7. return send_cached_response(document);
  8. }
  9. // ...repeatedly try something until it succeeds
  10. while let Err(err) = present_cheesy_anti_robot_task() {
  11. log_robot_attempt(err);
  12. // let the user try again (it might still be a human)
  13. }
  14. // ...manually loop over an iterator
  15. while let Some(_) = lines.peek() {
  16. read_paragraph(&mut lines);
  17. }

有关这些表达式的详细信息,请参阅第129页的”if let”和第130页的”Loops”.

填充二叉树(Populating a Binary Tree)

之前我们承诺将展示如何实现一个方法BinaryTree::add(),它将一个节点添加到这种类型的BinaryTree中:

  1. enum BinaryTree<T> {
  2. Empty,
  3. NonEmpty(Box<TreeNode<T>>)
  4. }
  5. struct TreeNode<T> {
  6. element: T,
  7. left: BinaryTree<T>,
  8. right: BinaryTree<T>
  9. }

你现在对模式有了足够的了解,可以编写此方法.二叉搜索树的解释超出了本书的范围,但对于已经熟悉该主题的读者来说,值得看看它在Rust中的表现.

  1. 1 impl<T: Ord> BinaryTree<T> {
  2. 2 fn add(&mut self, value: T) {
  3. 3 match *self {
  4. 4 BinaryTree::Empty =>
  5. 5 *self = BinaryTree::NonEmpty(Box::new(TreeNode {
  6. 6 element: value,
  7. 7 left: BinaryTree::Empty,
  8. 8 right: BinaryTree::Empty
  9. 9 })),
  10. 10 BinaryTree::NonEmpty(refmut node) =>
  11. 11 if value <= node.element {
  12. 12 node.left.add(value);
  13. 13 } else {
  14. 14 node.right.add(value);
  15. 15 }
  16. 16 }
  17. 17 }
  18. 18 }

第1行告诉Rust我们在有序类型的BinaryTree上定义了一个方法.这与我们用于在泛型结构上定义方法的语法完全相同,在”使用impl定义方法(Defining Methods with impl)”(第198页)中进行了解释.

如果现有树*self为空,那就很容易了.第5-9行运行,将Empty树更改为NonEmpty.对Box::new()的调用在这里分配一个新的TreeNode.当我们完成时,树包含一个元素.它的左右子树都是Empty.

如果*self不为空,我们匹配第10行的模式:

  1. BinaryTree::NonEmpty(ref mut node) =>

这个模式借用了对Box<TreeNode<T>>的可变引用,因此我们可以访问和修改该树节点中的数据.该引用被命名为node,它在第11行到第15行的范围内.由于此节点中已有一个元素,因此代码必须递归调用.add()以将新元素添加到左子树或右子树.

新方法可以像这样使用:

  1. let mut tree = BinaryTree::Empty;
  2. tree.add("Mercury");
  3. tree.add("Venus");
  4. ...

大局(The Big Picture)

Rust的枚举可能对于系统编程来说是新东西,但它们并不是一个新想法.以各种各样听起来像是学术的名字,比如 代数数据类型(algebraic data types) ,它们已经在函数式编程语言中使用了40多年.目前还不清楚为什么在C传统中的其他几种语言都没有.也许只是对于编程语言设计者来说,将变体,引用,可变性和内存安全性结合起来是极具挑战性的.函数式编程语言免除了可变性.相比之下,Cunion有变体,指针和可变性—但是非常不安全,即使在C中,它们也是最后的选择.Rust的借用检查器是神奇的,可以将所有四个结合起来而不妥协.

编程是数据处理.将数据转换为正确的形状可能是一个小巧,快速,优雅的程序与缓慢,庞大混乱的管道胶带和虚拟方法调用之间的区别.

这是空间枚举地址的问题.它们是用于将数据转换为正确形状的设计工具.对于值可能是一件事或其他事物,或者根本没有事物的情况,枚举优于每个轴上的类层次结构:更快,更安全,更少代码,更易于文档化.

限制因素是灵活性.枚举的最终用户无法扩展它以添加新变体.只能通过更改枚举声明来添加变体.当发生这种情况时,现有代码会中断.每个单独匹配枚举的每个变体的match表达式都必须被修正—它需要一个新的分支来处理新变体.在某些情况下,用灵活性换取简单性是很有意义的.毕竟,JSON的结构体不会改变.在某些情况下,修正枚举的所有用法正是我们想要的.例如,当在编译器中使用enum来表示编程语言的各种运算符时,添加新运算符 应该(should) 涉及触及处理运算符的所有代码.

但有时需要更多的灵活性.对于这些情况,Rust有traits,这是我们下一章的主题.