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>
类型是枚举;这样的值可以是包含String
的Ok
值,也可以是包含io::Error
的Err
值.这超过了C++和C#枚举的功能.它更像是一个Cunion
--但与联合体不同,Rust枚举是类型安全的.
只要值可能是一个或另一个值,枚举就很有用.使用它们的"代价"是你必须使用模式匹配安全地访问数据,这是本章后半部分的主题.
如果您在Python中使用解包或在JavaScript中进行解构,那么模式也可能很熟悉,但Rust会进一步采用模式. Rust模式有点像所有数据的正则表达式.它们用于测试值是否具有特定的所需形状.他们可以一次从结构或元组中提取几个字段到局部变量.和正则表达式一样,它们很简洁,通常在一行代码中完成所有操作.
简单地,C风格枚举很直接:
enum Ordering {
Less,
Equal,
Greater
}
这声明了一个带有三个可能值的类型Ordering
,称为 变体(variants) 或 构造函数(constructors) :Ordering::Less
,Ordering::Equal
和Ordering::Greater
.这个特殊的枚举是标准库的一部分,因此Rust代码可以自己导入它:
use std::cmp::Ordering;
fn compare(n: i32, m: i32) -> Ordering {
if n < m {
Ordering::Less
} else if n > m {
Ordering::Greater
} else {
Ordering::Equal
}
}
或者用其所有构造函数:
use std::cmp::Ordering;
use std::cmp::Ordering::*; // `*` to import all children
fn compare(n: i32, m: i32) -> Ordering {
if n < m {
Less
} else if n > m {
Greater
} else {
Equal
}
}
在导入构造函数之后,我们可以编写Less
而不是Ordering::Less
等等,但由于这不太明确,因此通常认为更好的方式是 不(not) 导入它们,除非它使代码更具可读性.
要导入当前模块中声明的枚举的构造函数,请使用self
导入:
enum Pet {
Orca,
Giraffe,
...
}
use self::Pet::*;
在内存中,C风格枚举的值存储为整数.有时,告诉Rust使用哪个整数是有用的:
enum HttpStatus {
Ok = 200,
NotModified = 304,
NotFound = 404,
...
}
否则Rust将为你分配数字,从0开始.
默认情况下,Rust使用可容纳它们的最小内置整数类型存储C风格的枚举.大多数适合单个字节.
use std::mem::size_of;
assert_eq!(size_of::<Ordering>(), 1);
assert_eq!(size_of::<HttpStatus>(), 2); // 404 doesn't fit in a u8
你可以通过向枚举添加#[repr]
属性来覆盖Rust对内存中表示的选择.有关详细信息,请参阅第21章.
允许将C风格枚举转换为整数:
assert_eq!(HttpStatus::Ok as i32, 200);
但是,另一个方向的转换(从整数到枚举)不可以.与C和C++不同,Rust保证枚举值只是enum
声明中拼写的值之一.从整数类型到枚举类型的未经检查的强制转换可能会破坏此保证,因此不允许这样做.你可以编写自己的受检查转换:
fn http_status_from_u32(n: u32) -> Option<HttpStatus> {
match n {
200 => Some(HttpStatus::Ok),
304 => Some(HttpStatus::NotModified),
404 => Some(HttpStatus::NotFound),
...
_ => None
}
}
或使用enum_primitive
crate.它包含一个为你自动生成此类转换代码的宏.
与结构一样,编译器会为你实现类似==
运算符的功能,但你必须要求.
#[derive(Copy, Clone, Debug, PartialEq)]
enum TimeUnit {
Seconds, Minutes, Hours, Days, Months, Years
}
枚举可以有方法,就像结构一样:
impl TimeUnit {
/// Return the plural noun for this time unit.
fn plural(self) -> &'staticstr {
match self {
TimeUnit::Seconds => "seconds",
TimeUnit::Minutes => "minutes",
TimeUnit::Hours => "hours",
TimeUnit::Days => "days",
TimeUnit::Months => "months",
TimeUnit::Years => "years"
}
}
/// Return the singular noun for this time unit.
fn singular(self) -> &'static str {
self.plural().trim_right_matches('s')
}
}
C风格的枚举就到这里.更有趣的Rust枚举是包含数据的那种.
有些程序总是需要显示毫秒级的完整日期和时间,但对于大多数应用程序来说,使用粗略的近似值更加用户友好,比如"两个月前(two months ago)".我们可以写一个枚举来帮助它:
// A timestamp that has been deliberately rounded off, so our program
/// says "6 months ago" instead of "February 9, 2016, at 9:49 AM".
#[derive(Copy, Clone, Debug, PartialEq)]
enum RoughTime {
InThePast(TimeUnit, u32),
JustNow,
InTheFuture(TimeUnit, u32)
}
此枚举中的两个变体,InThePast
和InTheFuture
采用了参数.这些被称为 元组变体(tuple variants) .与元组结构一样,这些构造函数是创建新RoughTime
值的函数.
let four_score_and_seven_years_ago =
RoughTime::InThePast(TimeUnit::Years, 4*20 + 7);
let three_hours_from_now =
RoughTime::InTheFuture(TimeUnit::Hours, 3);
枚举也可以包含 结构变体(struct variants) ,其中包含命名字段,就像普通结构一样:
enum Shape {
Sphere { center: Point3d, radius: f32 },
Cuboid { corner1: Point3d, corner2: Point3d }
}
let unit_sphere = Shape::Sphere { center: ORIGIN, radius: 1.0 };
总而言之,Rust有三种枚举变体,它们与我们在前一章中展示的三种结构相呼应.没有数据的变体对应于单元式结构.元组变体的外观和功能就像元组结构一样.结构变体具有花括号和命名字段.单个枚举可以具有所有三种变体.
enum RelationshipStatus {
Single,
InARelationship,
ItsComplicated(Option<String>),
ItsExtremelyComplicated {
car: DifferentialEquation,
cdr: EarlyModernistPoem
}
}
公有枚举的所有构造函数和字段都是自动公有的.
在内存中,带有数据的枚举存储为一个小的整数 标记(tag) ,加上足够的内存来保存最大变体的所有字段. 标记字段用于Rust的内部使用.它告诉哪个构造函数创建了值,因此它创建了哪些字段.
从Rust 1.17开始,RoughTime
适合8个字节,如图10-1所示.
图10-1. 内存中的RoughTime值.
但是,Rust没有对枚举布局做出任何承诺,以便为将来的优化留出空间.在某些情况下,可以比图中所示更有效地打包枚举.我们将在本章后面的内容中展示Rust如何针对某些枚举优化掉标记字段.
枚举对于快速实现树状数据结构也很有用.例如,假设Rust程序需要使用任意JSON数据.在内存中,任何JSON文档都可以表示为此Rust类型的值:
enum Json {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec<Json>),
Object(Box<HashMap<String, Json>>)
}
用英语解释数据结构对Rust代码并没有太大改进.JSON标准指定可以出现在JSON文档中的各种数据类型:null
,布尔值,数字,字符串,JSON值的数组以及具有字符串键和JSON值的对象.Json
枚举简单地说明了这些类型.
这不是一个假想的例子.在serde_json
(一个Rust结构的序列化库,它是crates.io上下载次数最多的crates之一)中可以找到一个非常相似的枚举.
表示Object
的HashMap
周围的Box
仅用于使所有Json
值更紧凑.在内存中,Json
类型的值占用四个机器字.String
和Vec
值是三个字,Rust添加一个标记字节.Null
和Boolean
值中没有足够的数据用尽所有空间,但所有Json
值必须大小相同.额外的空间未使用.图10-2显示了Json
值在内存中的实际样子的一些示例.
HashMap
仍然要更大.如果我们不得不在每个Json
值中为它留出空间,那么它们会非常大,八个字左右.但Box<HashMap>
是一个单个字:它只是指向堆分配数据的指针.我们可以通过装箱更多的字段( boxing more fields)使Json
更加紧凑.
图10-2. 内存中的Json值.
这里值得注意的是设置它是多么容易.在C++中,或许要为此编写一个类:
class JSON {
private:
enum Tag {
Null, Boolean, Number, String, Array, Object
};
union Data {
bool boolean;
double number;
shared_ptr<string> str;
shared_ptr<vector<JSON>> array;
shared_ptr<unordered_map<string, JSON>> object;
Data() {}
~Data() {}
...
};
Tag tag;
Data data;
public:
bool is_null() const { return tag == Null; }
bool is_boolean() const { return tag == Boolean; }
bool get_boolean() const {
assert(is_boolean());
return data.boolean;
}
void set_boolean(bool value) {
this->~JSON(); // clean up string/array/object value
tag = Boolean;
data.boolean = value;
}
...
};
在30行代码中,我们几乎没有开始工作.该类需要构造函数,析构函数和赋值运算符.另一种方法是创建一个具有基类JSON
和子类JSONBoolean
,JSONString
等的类层次结构.无论哪种方式,当它完成时,我们的C++JSON
库将拥有十几种方法.其他程序员需要一些阅读才能拿起并使用它.整个Rust枚举是八行代码.
枚举可以是泛型的.以下两个示例来自标准库,是该语言中最常用的数据类型:
enum Option<T> {
None,
Some(T)
}
enum Result<T, E> {
Ok(T),
Err(E)
}
这些类型现在已经足够熟悉了,泛型枚举的语法与泛型结构相同.一个不明显的细节是,当类型T
是Box
或其他一些智能指针类型时,Rust可以消除Option<T>
的标记字段.Option<Box <i32 >>
作为单个机器字存储在内存中,0表示None
,非零表示Some
包装值.
只需几行代码即可构建泛型数据结构:
// An ordered collection of `T`s.
enum BinaryTree<T> {
Empty,
NonEmpty(Box<TreeNode<T>>)
}
// A part of a BinaryTree.
struct TreeNode<T> {
element: T,
left: BinaryTree<T>,
right: BinaryTree<T>
}
这几行代码定义了一个BinaryTree
类型,可以存储任意数量的T
类型的值.
这两个定义中包含了大量信息,因此我们将花时间将代码逐字翻译成英语.每个BinaryTree
值都是Empty
或NonEmpty
.如果它为Empty
,则它根本不包含任何数据.如果是NonEmpty
,那么它有一个Box
,指向堆分配的TreeNode
的指针.
每个TreeNode
值包含一个实际元素,以及另外两个BinaryTree
值.这意味着树可以包含子树,因此NonEmpty
树可以包含任意数量的后代.
一个BinaryTree<&str>
类型值的草图如图10-3所示.与Option<Box<T>>
一样,Rust删除了标记字段,因此BinaryTree
值只是一个机器字.
图10-3. 一个含有6个字符串的BinaryTree.
在此树中构建任何特定节点都很简单:
use self::BinaryTree::*;
let jupiter_tree = NonEmpty(Box::new(TreeNode {
element: "Jupiter",
left: Empty,
right: Empty
}));
较大的树可以用较小的树构建:
let mars_tree = NonEmpty(Box::new(TreeNode {
element: "Mars",
left: jupiter_tree,
right: mercury_tree
}));
当然,这个赋值将jupiter_node
和mercury_node
的所有权转移到它们的新的父节点.
树的其余部分遵循相同的模式.根节点与其他节点没有区别:
let tree = NonEmpty(Box::new(TreeNode {
element: "Saturn",
left: mars_tree,
right: uranus_tree
}));
在本章的后面,我们将展示如何在BinaryTree
类型上实现add
方法,以便我们可以编写:
let mut tree = BinaryTree::Empty;
for planet in planets {
tree.add(planet);
}
无论你来自哪种语言,在Rust中创建像BinaryTree
这样的数据结构都可能需要一些练习.首先放置Box
的位置并不明显.找到一个可行的设计的一种方法是绘制一幅如图10-3所示的图片,显示你希望如何在内存中布置内容.然后从图片返回到代码.每个矩形集合都是一个结构或元组;每个箭头都是一个Box
或其他智能指针.弄清楚每个字段的类型是一个难题,但是一个可管理的难题.解决难题的好处是控制程序的内存使用情况.
现在来到我们在介绍中提到的"代价(price)".枚举的标记字段占用一点内存,在最坏的情况下最多8个字节,但这通常可以忽略不计.枚举的真正缺点(如果可以称之为)是Rust代码不能不顾一切然后尝试访问字段,无论它们是否实际存在于值中:
let r = shape.radius; // error: no field `radius` on type `Shape`
访问枚举中数据的唯一方法是安全的方式:使用模式.
回想一下本章前面我们的RoughTime
类型的定义:
enum RoughTime {
InThePast(TimeUnit, u32),
JustNow,
InTheFuture(TimeUnit, u32)
}
假设你有一个RoughTime
值,并且你希望将其显示在网页上.你需要访问值内的TimeUnit
和u32
字段.Rust不允许你通过编写rough_time.0
和rough_time.1
直接访问它们,因为毕竟,值可能是RoughTime::JustNow
,它没有字段.但是,你如何获得数据呢?
你需要一个match
表达式:
1 fn rough_time_to_english(rt: RoughTime) -> String {
2 match rt {
3 RoughTime::InThePast(units, count) =>
4 format!("{} {} ago", count, units.plural()),
5 RoughTime::JustNow =>
6 format!("just now"),
7 RoughTime::InTheFuture(units, count) =>
8 format!("{} {} from now", count, units.plural())
9 }
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. 一个成功的匹配.
当模式包含units
和count
等简单标识符时,它们将成为模式后面的代码中的局部变量.将值中存在的任何内容复制或移动到新变量中.Rust将TimeUnit::Months
存储在units
,1
存储在count
中,运行第8行,并返回字符串"1 months from now"
.
该输出有一个小的语法问题,可以通过在match
中添加另一个分支来修复:
RoughTime::InTheFuture(unit, 1) =>
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'` |
守卫表达式(Guard expression) | x if x * x <= r2 |
仅在match 中(在let 等中无效) |
到目前为止,我们已经展示了使用枚举的match
表达式.其他类型也可以匹配.当你需要类似Cswitch
语句的内容时,请用整数值使用match
,像0
和1
这样的整数字面量可以作为模式:
match meadow.count_rabbits() {
0 => {} // nothing to say
1 => println!("A rabbit is nosing around in the clover."),
n => println!("There are {} rabbits hopping about in the meadow", n)
}
如果草地上没有兔子,则模式0
匹配.如果只有一个,则1
匹配.如果有两只或两只以上的兔子,我们会达到第三种模式,n
.此模式只是一个变量名称.它可以匹配任何值,匹配的值被移动或复制到新的局部变量中.所以在这种情况下,meadow.count_rabbits()
的值存储在一个新的局部变量n
中,然后我们打印它.
其他字面量也可以用作模式,包括布尔值,字符甚至字符串:
let calendar =
match settings.get_string("calendar") {
"gregorian" => Calendar::Gregorian,
"chinese" => Calendar::Chinese,
"ethiopian" => Calendar::Ethiopian,
other => return parse_error("calendar", other)
};
在此示例中,other
用作捕获所有(catch-all)模式,如上例中的n
.这些模式与switch
语句中的default
情况起着相同的作用,匹配与任何其他模式都不匹配的值.
如果你需要一个捕获所有模式,但你不关心匹配到的值,你可以使用单个下划线_
作为模式, 通配符模式(wildcard pattern) :
let caption =
match photo.tagged_pet() {
Pet::Tyrannosaur => "RRRAAAAAHHHHHH",
Pet::Samoyed => "*dog thoughts*",
_ => "I'm cute, love me"// generic caption, works for any pet
};
通配符模式匹配任何值,但不将其存储在任何位置.由于Rust要求每个match
表达式都要处理所有可能的值,因此最后通常需要使用通配符.即使你非常确定剩下的情况不会发生,你也必须至少添加一个恐慌的后备分支:
// There are many Shapes, but we only support "selecting"
// either some text, or everything in a rectangular area.
// You can't select an ellipse or trapezoid.
match document.selection() {
Shape::TextSpan(start, end) => paint_text_selection(start, end),
Shape::Rectangle(rect) => paint_rect_selection(rect),
_ => panic!("unexpected selection type")
}
值得注意的是,现有变量不能用于模式中.假设我们正在实现一个六边形空间的棋盘游戏,玩家只需点击移动一块.要确认点击是有效的,我们可能会尝试这样的事情:
fn check_move(current_hex: Hex, click: Point) -> game::Result<Hex> {
match point_to_hex(click) {
None =>
Err("That's not a game space."),
Some(current_hex) => // try to match if user clicked the current_hex
// (it doesn't work: see explanation below)
Err("You are already there! You must click somewhere else."),
Some(other_hex) =>
Ok(other_hex)
}
}
这会失败,因为模式中的标识符引入了 新 变量.这里的模式Some(current_hex)
创建一个新的局部变量current_hex
,它遮蔽参数current_hex
.Rust发出了关于此代码的几个警告--特别是,match
的最后一个分支是无法到达的.要修复它,请使用if
表达式:
Some(hex) =>
if hex == current_hex {
Err("You are already there! You must click somewhere else")
} else {
Ok(hex)
}
在接下来的几页中,我们将介绍守卫(guard),它提供了解决此问题的另一种方法.
元组模式匹配元组.只要你想在单个match
中获得多个数据,它们就很有用:
fn describe_point(x: i32, y: i32) -> &'staticstr {
use std::cmp::Ordering::*;
match (x.cmp(&0), y.cmp(&0)) {
(Equal, Equal) => "at the origin",
(_, Equal) => "on the x axis",
(Equal, _) => "on the y axis",
(Greater, Greater) => "in the first quadrant",
(Less, Greater) => "in the second quadrant",
_ => "somewhere else"
}
}
结构模式使用花括号,就像结构表达式一样.它们包含每个字段的子模式:
match balloon.location {
Point { x: 0, y: height } =>
println!("straight up {} meters", height),
Point { x: x, y: y } =>
println!("at ({}m, {}m)", x, y)
}
在此示例中,如果第一个分支匹配,则balloon.location.y
存储在新的局部变量height
中.
假设balloon.location
是Point { 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
中.
即使使用简写,当我们只关心几个字段时,匹配大型结构也很麻烦:
match get_account(id) {
...
Some(Account {
name, language, // <--- the 2 things we care about
id: _, status: _, address: _, birthday: _, eye_color: _,
pet: _, security_question: _, hashed_innermost_secret: _,
is_adamantium_preferred_customer: _ }) =>
language.show_custom_greeting(name)
}
要避免这种情况,请使用..
告诉Rust你不关心任何其他字段:
Some(Account { name, language, .. }) =>
language.show_custom_greeting(name)
Rust模式支持两个用于处理引用的功能.ref
模式借用匹配值的部分.&
模式匹配引用.我们将首先介绍ref
模式.
匹配不可复制的值会移动该值.继续使用帐户(account)示例,此代码无效:
match account {
Account { name, language, .. } => {
ui.greet(&name, &language);
ui.show_settings(&account); // error: use of moved value `account`
}
}
这里,字段account.name
和account.language
被移动到局部变量name
和language
中.account
的其余部分被删除.这就是我们之后无法用account
调用方法的原因.
如果name
和language
都是可复制值,Rust会复制字段而不是移动它们,这段代码就可以了.但假设这些是String
.我们可以做什么?
我们需要一种模式, 借用(borrow) 匹配的值而不是移动它们.ref
关键字就是这样做的:
match account {
Account { ref name, ref language, .. } => {
ui.greet(name, language);
ui.show_settings(&account); // ok
}
}
现在,局部变量name
和language
是对account
中相应字段的引用.由于account
只是被借用而不被消费,因此可以继续在其上调用方法.
你可以使用ref mut
借用mut
引用:
match line_result {
Err(ref err) => log_error(err), // `err` is &Error (shared ref)
Ok(ref mut line) => { // `line` is &mut String (mut ref)
trim_comments(line); // modify the String in place
handle(line);
}
}
模式Ok(ref mut line)
匹配任何成功结果,并借用成功值的mut
引用存储在其中.
相反类型的引用模式是&
模式.以&
开始的模式匹配一个引用.
match sphere.center() {
&Point3d { x, y, z } => ...
}
在此示例中,假设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
,y
和z
接收坐标的副本,使原始的Point3d
值保持不变.它起作用,因为这些字段是可复制的.如果我们在具有不可复制字段的结构上尝试相同的操作,我们将收到错误:
match friend.borrow_car() {
Some(&Car { engine, .. }) => // error: can't move out of borrow
...
None => {}
}
为零件报废一辆借来的汽车并不好,而Rust也不能容忍它.你可以使用ref
模式借用对零件的引用.你就是不能拥有它.
Some(&Car { ref engine, .. }) => // ok, engine is a reference
让我们看一个&
模式的另一个例子.假设我们在字符串中的字符上有一个迭代器chars
,并且它有一个方法chars.peek()
它返回一个Option<&char>
:对下一个字符的引用(如果有的话).(Peekable迭代器确实会返回一个Option<&ItemType>
,我们将在第15章中看到.)
程序可以使用&
模式来获取指向的字符:
match chars.peek() {
Some(&c) => println!("coming up: {:?}", c),
None => println!("end of chars")
}
垂直条(|
)可用于在单个匹配分支中组合多个模式:
let at_end =
match chars.peek() {
Some(&'\r') | Some(&'\n') | None => true,
_ => false
};
在表达式中,|
是按位OR运算符,但在这里它更像是正则表达式中的|
符号.如果chars.peek()
匹配三种模式中的任何一种,则at_end
设置为true
.
使用...
来匹配整个值范围.范围模式包括开始和结束值,因此'0' ... '9'
匹配所有ASCII数字:
match next_char {
'0' ... '9' =>
self.read_number(),
'a' ... 'z' | 'A' ... 'Z' =>
self.read_word(),
' ' | '\t' | '\n' =>
self.skip_whitespace(),
_ =>
self.handle_punctuation()
}
模式中的范围是 包容性的(inclusive) ,因此'0'
和'9'
都匹配模式'0' ... '9'
.相比之下,范围表达式(用两个点写成,如for n in 0..100
中)是半开的,或者是 独占的(exclusive) (覆盖0
但不覆盖100
).不一致的原因很简单,独占范围对于循环和切片更有用,但是包含范围在模式匹配中更有用.
使用if
关键字添加一个 守卫(guard) 到match
分支.只有当守卫求值为true
时,匹配才会成功:
match robot.last_known_location() {
Some(point) if self.distance_to(point) < 10 =>
short_distance_strategy(point),
Some(point) =>
long_distance_strategy(point),
None =>
searching_strategy()
}
如果一个模式移动任何值,你就不能对它放置守卫.守卫可能会求值为false
,然后Rust会继续下一个模式.但是,如果你已将位移出匹配值,则无法做到这一点.因此,前面的代码仅在point
是可复制的时才起作用,如果不是,我们会收到错误:
error[E0008]: cannot bind by-move into a pattern guard
--> enums_move_into_guard.rs:19:18
|
19 | Some(point) if self.distance_to(point) < 10 =>
| ^^^^^ moves value into pattern guard
然后,解决方法是将模式更改为借用point
而不是移动它:`Some(ref point).
最后, x @ pattern
与给定pattern
完全匹配,但是在成功时,它不是为匹配值的部分创建变量,而是创建单个变量x
并将整个值移动或复制到其中.例如,假设你有以下代码:
match self.get_selection() {
Shape::Rect(top_left, bottom_right) =>
optimized_paint(&Shape::Rect(top_left, bottom_right)),
other_shape =>
paint_outline(other_shape.get_outline()),
}
请注意,第一种情况是解包Shape::Rect值
,只是为了在下一行重建相同的Shape::Rect
值.这可以重写为使用@
模式:
rect @ Shape::Rect(..) =>
optimized_paint(&rect),
@
模式对范围也很有用:
match chars.next() {
Some(digit @ '0' ... '9') => read_number(digit, chars),
...
}
尽管模式在match
表达式中最为突出,但它们也可以在其他几个地方使用,通常代替标识符.含义总是相同的:Rust不是仅仅将值存储在单个变量中.而是使用模式匹配来将值分开.
这意味着模式可用于...
// ...unpack a struct into three new local variables
let Track { album, track_number, title, .. } = song;
// ...unpack a function argument that's a tuple
fn distance_to((x, y): (f64, f64)) -> f64 { ... }
// ...iterate over keys and values of a HashMap
for (id, document) in &cache_map {
println!("Document #{}: {}", id, document.title);
}
// ...automatically dereference an argument to a closure
// (handy because sometimes other code passes you a reference
// when you'd rather have a copy)
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程序中模式可以很方便,但语言不允许匹配失败的地方.
如果let
和while let
表达式也可以使用可辩驳模式,这可以用于...
// ...handle just one enum variant specially
if let RoughTime::InTheFuture(_, _) = user.date_of_birth() {
user.set_time_traveler(true);
}
// ...run some code only if a table lookup succeeds
if let Some(document) = cache_map.get(&id) {
return send_cached_response(document);
}
// ...repeatedly try something until it succeeds
while let Err(err) = present_cheesy_anti_robot_task() {
log_robot_attempt(err);
// let the user try again (it might still be a human)
}
// ...manually loop over an iterator
while let Some(_) = lines.peek() {
read_paragraph(&mut lines);
}
有关这些表达式的详细信息,请参阅第129页的"if let"和第130页的"Loops".
之前我们承诺将展示如何实现一个方法BinaryTree::add()
,它将一个节点添加到这种类型的BinaryTree
中:
enum BinaryTree<T> {
Empty,
NonEmpty(Box<TreeNode<T>>)
}
struct TreeNode<T> {
element: T,
left: BinaryTree<T>,
right: BinaryTree<T>
}
你现在对模式有了足够的了解,可以编写此方法.二叉搜索树的解释超出了本书的范围,但对于已经熟悉该主题的读者来说,值得看看它在Rust中的表现.
1 impl<T: Ord> BinaryTree<T> {
2 fn add(&mut self, value: T) {
3 match *self {
4 BinaryTree::Empty =>
5 *self = BinaryTree::NonEmpty(Box::new(TreeNode {
6 element: value,
7 left: BinaryTree::Empty,
8 right: BinaryTree::Empty
9 })),
10 BinaryTree::NonEmpty(refmut node) =>
11 if value <= node.element {
12 node.left.add(value);
13 } else {
14 node.right.add(value);
15 }
16 }
17 }
18 }
第1行告诉Rust我们在有序类型的BinaryTree
上定义了一个方法.这与我们用于在泛型结构上定义方法的语法完全相同,在"使用impl定义方法(Defining Methods with impl)"(第198页)中进行了解释.
如果现有树*self
为空,那就很容易了.第5-9行运行,将Empty
树更改为NonEmpty
.对Box::new()
的调用在这里分配一个新的TreeNode
.当我们完成时,树包含一个元素.它的左右子树都是Empty
.
如果*self
不为空,我们匹配第10行的模式:
BinaryTree::NonEmpty(ref mut node) =>
这个模式借用了对Box<TreeNode<T>>
的可变引用,因此我们可以访问和修改该树节点中的数据.该引用被命名为node
,它在第11行到第15行的范围内.由于此节点中已有一个元素,因此代码必须递归调用.add()
以将新元素添加到左子树或右子树.
新方法可以像这样使用:
let mut tree = BinaryTree::Empty;
tree.add("Mercury");
tree.add("Venus");
...
Rust的枚举可能对于系统编程来说是新东西,但它们并不是一个新想法.以各种各样听起来像是学术的名字,比如 代数数据类型(algebraic data types) ,它们已经在函数式编程语言中使用了40多年.目前还不清楚为什么在C传统中的其他几种语言都没有.也许只是对于编程语言设计者来说,将变体,引用,可变性和内存安全性结合起来是极具挑战性的.函数式编程语言免除了可变性.相比之下,Cunion
有变体,指针和可变性--但是非常不安全,即使在C中,它们也是最后的选择.Rust的借用检查器是神奇的,可以将所有四个结合起来而不妥协.
编程是数据处理.将数据转换为正确的形状可能是一个小巧,快速,优雅的程序与缓慢,庞大混乱的管道胶带和虚拟方法调用之间的区别.
这是空间枚举地址的问题.它们是用于将数据转换为正确形状的设计工具.对于值可能是一件事或其他事物,或者根本没有事物的情况,枚举优于每个轴上的类层次结构:更快,更安全,更少代码,更易于文档化.
限制因素是灵活性.枚举的最终用户无法扩展它以添加新变体.只能通过更改枚举声明来添加变体.当发生这种情况时,现有代码会中断.每个单独匹配枚举的每个变体的match
表达式都必须被修正--它需要一个新的分支来处理新变体.在某些情况下,用灵活性换取简单性是很有意义的.毕竟,JSON的结构体不会改变.在某些情况下,修正枚举的所有用法正是我们想要的.例如,当在编译器中使用enum
来表示编程语言的各种运算符时,添加新运算符 应该(should) 涉及触及处理运算符的所有代码.
但有时需要更多的灵活性.对于这些情况,Rust有traits,这是我们下一章的主题.