The match
Control Flow Operator
+ match
控制流运算符
+++ch06-02-match.md +
+
+commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d
Rust 有一个叫做match
的极为强大的控制流运算符,
diff --git a/docs/ch06-01-defining-an-enum.html b/docs/ch06-01-defining-an-enum.html index 543f26d..f665615 100644 --- a/docs/ch06-01-defining-an-enum.html +++ b/docs/ch06-01-defining-an-enum.html @@ -150,7 +150,126 @@ let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V6(String::from("::1"));
我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。
-使用枚举而不是结构体还有另外一个优势:每个成员可以处理不同类型和数量的数据。
+使用枚举而不是结构体还有另外一个优势:每个成员可以处理不同类型和数量的数据。IPv4 版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将V4
地址储存为四个u8
值而V6
地址仍然表现为一个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 地址实在是太常见了以致标准库提供了一个可供使用的定义!让我们看看标准库如何定义IpAddr
的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员种的地址数据嵌入到了两个不同形式的结构体中,他们对不同的成员的定义是不同的:
struct Ipv4Addr {
+ // details elided
+}
+
+struct Ipv6Addr {
+ // details elided
+}
+
+enum IpAddr {
+ V4(Ipv4Addr),
+ V6(Ipv6Addr),
+}
+
+这些代码展示了可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你可能设想出来的要复杂多少。
+注意虽然标准库中包含一个IpAddr
的定义,仍然可以创建和使用我们自己的定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。第七章会讲到如何导入类型。
来看看列表 6-2 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型:
+ +这个枚举有四个含有不同类型的成员:
+Quit
没有关联任何数据。Move
包含一个匿名结构体Write
包含单独一个String
。ChangeColor
包含三个i32
。定义一个像列表 6-2 中的枚举类似于定义不同类型的结构体,除了枚举不使用struct
关键字而且所有成员都被组合在一起位于Message
下。如下这些结构体可以包含与之前枚举成员中相同的数据:
struct QuitMessage; // unit struct
+struct MoveMessage {
+ x: i32,
+ y: i32,
+}
+struct WriteMessage(String); // tuple struct
+struct ChangeColorMessage(i32, i32, i32); // tuple struct
+
+不过如果我们使用不同的结构体,他们都有不同的类型,将不能轻易的定义一个获取任何这些信息类型的函数,正如可以使用列表 6-2 中定义的Message
枚举那样因为他们是一个类型的。
结构体和枚举还有另一个相似点:就像可以使用impl
来为结构体定义方法那样,也可以在枚举上定义方法。这是一个定义于我们Message
枚举上的叫做call
的方法:
# enum Message {
+# Quit,
+# Move { x: i32, y: i32 },
+# Write(String),
+# ChangeColor(i32, i32, i32),
+# }
+#
+impl Message {
+ fn call(&self) {
+ // method body would be defined here
+ }
+}
+
+let m = Message::Write(String::from("hello"));
+m.call();
+
+方法体使用了self
来获取调用方法的值。这个例子中,创建了一个拥有类型Message::Write("hello")
的变量m
,而且这就是当m.call()
运行时call
方法中的self
的值。
让我们看看标准库中的另一个非常常见和实用的枚举:Option
。
Option
枚举和其相对空值的优势在之前的部分,我们看到了IpAddr
枚举如何利用 Rust 的类型系统编码更多信息而不单单是程序中的数据。这一部分探索一个Option
的案例分析,它是标准库定义的另一个枚举。Option
类型应用广泛因为它编码了一个非常普遍的场景,就是一个值可能是某个值或者什么都不是。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。
编程语言的设计经常从其包含功能的角度考虑问题,但是从不包含的功能的角度思考也很重要。Rust 并没有很多其他语言中有的空值功能。空值(Null )是一个值它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
+在“Null References: The Billion Dollar Mistake”中,Tony Hoare,null 的发明者,曾经说到:
+++我称之为我万亿美元的错误。当时,我在在一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的应有都应该是绝对安全的。不过我未能抗拒引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数以万计美元的苦痛和伤害。
+
空值的为题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性是无处不在的,非常容易出现这类错误。
+然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。
+问题不在于实际的概念而在于具体的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是Option<T>
,而且它定义于标准库中,如下:
enum Option<T> {
+ Some(T),
+ None,
+}
+
+Option<T>
是如此有用以至于它甚至被包含在了 prelude 之中:不需要显式导入它。另外,它的成员也是如此:可以不需要Option::
前缀来直接使用Some
和None
。即便如此Option<T>
也仍是常规的枚举,Some(T)
和None
仍是Option<T>
的成员。
<T>
语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数,第十章会更详细的讲解泛型。目前,所有你需要知道的就是<T>
意味着Option
枚举的Some
成员可以包含任意类型的数据。这里是一些包含数字类型和字符串类型Option
值的例子:
let some_number = Some(5);
+let some_string = Some("a string");
+
+let absent_number: Option<i32> = None;
+
+如果使用None
而不是Some
,需要告诉 Rust Option<T>
是什么类型的,因为编译器只通过None
值无法推断出Some
成员的类型。
当有一个Some
值时,我们就知道存在一个值,而这个值保存在Some
中。当有个None
值时,在某种意义上它跟空值是相同的意义:并没有一个有效的值。那么,Option<T>
为什么就比空值要好呢?
简而言之,因为Option<T>
和T
(这里T
可以是任何类型)是不同的类型,编译器不允许像一个被定义的有效的类型那样使用Option<T>
。例如,这些代码不能编译,因为它尝试将Option<i8>
与i8
相比:
let x: i8 = 5;
+let y: Option<i8> = Some(5);
+
+let sum = x + y;
+
+如果运行这些代码,将得到类似这样的错误信息:
+error[E0277]: the trait bound `i8: std::ops::Add<std::option::Option<i8>>` is
+not satisfied
+ -->
+ |
+7 | let sum = x + y;
+ | ^^^^^
+ |
+
+哇哦!事实上,错误信息意味着 Rust 不知道该如何将Option<i8>
与i8
相加。当在 Rust 中拥有一个像i8
这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需判空。只有当使用Option<i8>
(或者任何用到的类型)是需要担心可能没有一个值,而编译器会确保我们在使用值之前处理为空的情况。
换句话说,在对Option<T>
进行T
的运算之前必须转为T
。通常这能帮助我们捕获空值最常见的问题之一:假设某值不为空但实际上为空。
无需担心错过非空值的假设(和处理)让我们对代码更加有信心,为了拥有一个可能为空的值,必须显式的将其放入对应类型的Option<T>
中。接着,当使用这个值时,必须明确的处理值为空的情况。任何地方一个值不是Option<T>
类型的话,可以安全的假设它的值不为空。这是 Rust 的一个有意为之的设计选择,来限制空值的泛滥和增加 Rust 代码的安全性。
那么当有一个Option<T>
的值时,如何从Some
成员中取出T
的值来使用它呢?Option<T>
枚举拥有大量用于各种情况的方法:你可以查看相关代码。熟悉Option<T>
的方法将对你的 Rust 之旅提供巨大的帮助。
总的来说,为了使用Option<T>
值,需要编写处理每个成员的代码。我们想要一些代码只当拥有Some(T)
值时运行,这些代码允许使用其中的T
。也希望一些代码当在None
值时运行,这些代码并没有一个可用的T
值。match
表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。
match
Control Flow Operatormatch
控制流运算符++ch06-02-match.md +
+
+commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d
Rust 有一个叫做match
的极为强大的控制流运算符,
我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。
-使用枚举而不是结构体还有另外一个优势:每个成员可以处理不同类型和数量的数据。
-match
Control Flow Operator使用枚举而不是结构体还有另外一个优势:每个成员可以处理不同类型和数量的数据。IPv4 版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将V4
地址储存为四个u8
值而V6
地址仍然表现为一个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 地址实在是太常见了以致标准库提供了一个可供使用的定义!让我们看看标准库如何定义IpAddr
的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员种的地址数据嵌入到了两个不同形式的结构体中,他们对不同的成员的定义是不同的:
struct Ipv4Addr {
+ // details elided
+}
+
+struct Ipv6Addr {
+ // details elided
+}
+
+enum IpAddr {
+ V4(Ipv4Addr),
+ V6(Ipv6Addr),
+}
+
+这些代码展示了可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你可能设想出来的要复杂多少。
+注意虽然标准库中包含一个IpAddr
的定义,仍然可以创建和使用我们自己的定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。第七章会讲到如何导入类型。
来看看列表 6-2 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型:
+ +这个枚举有四个含有不同类型的成员:
+Quit
没有关联任何数据。Move
包含一个匿名结构体Write
包含单独一个String
。ChangeColor
包含三个i32
。定义一个像列表 6-2 中的枚举类似于定义不同类型的结构体,除了枚举不使用struct
关键字而且所有成员都被组合在一起位于Message
下。如下这些结构体可以包含与之前枚举成员中相同的数据:
struct QuitMessage; // unit struct
+struct MoveMessage {
+ x: i32,
+ y: i32,
+}
+struct WriteMessage(String); // tuple struct
+struct ChangeColorMessage(i32, i32, i32); // tuple struct
+
+不过如果我们使用不同的结构体,他们都有不同的类型,将不能轻易的定义一个获取任何这些信息类型的函数,正如可以使用列表 6-2 中定义的Message
枚举那样因为他们是一个类型的。
结构体和枚举还有另一个相似点:就像可以使用impl
来为结构体定义方法那样,也可以在枚举上定义方法。这是一个定义于我们Message
枚举上的叫做call
的方法:
# enum Message {
+# Quit,
+# Move { x: i32, y: i32 },
+# Write(String),
+# ChangeColor(i32, i32, i32),
+# }
+#
+impl Message {
+ fn call(&self) {
+ // method body would be defined here
+ }
+}
+
+let m = Message::Write(String::from("hello"));
+m.call();
+
+方法体使用了self
来获取调用方法的值。这个例子中,创建了一个拥有类型Message::Write("hello")
的变量m
,而且这就是当m.call()
运行时call
方法中的self
的值。
让我们看看标准库中的另一个非常常见和实用的枚举:Option
。
Option
枚举和其相对空值的优势在之前的部分,我们看到了IpAddr
枚举如何利用 Rust 的类型系统编码更多信息而不单单是程序中的数据。这一部分探索一个Option
的案例分析,它是标准库定义的另一个枚举。Option
类型应用广泛因为它编码了一个非常普遍的场景,就是一个值可能是某个值或者什么都不是。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。
编程语言的设计经常从其包含功能的角度考虑问题,但是从不包含的功能的角度思考也很重要。Rust 并没有很多其他语言中有的空值功能。空值(Null )是一个值它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
+在“Null References: The Billion Dollar Mistake”中,Tony Hoare,null 的发明者,曾经说到:
+++我称之为我万亿美元的错误。当时,我在在一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的应有都应该是绝对安全的。不过我未能抗拒引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数以万计美元的苦痛和伤害。
+
空值的为题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性是无处不在的,非常容易出现这类错误。
+然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。
+问题不在于实际的概念而在于具体的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是Option<T>
,而且它定义于标准库中,如下:
enum Option<T> {
+ Some(T),
+ None,
+}
+
+Option<T>
是如此有用以至于它甚至被包含在了 prelude 之中:不需要显式导入它。另外,它的成员也是如此:可以不需要Option::
前缀来直接使用Some
和None
。即便如此Option<T>
也仍是常规的枚举,Some(T)
和None
仍是Option<T>
的成员。
<T>
语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数,第十章会更详细的讲解泛型。目前,所有你需要知道的就是<T>
意味着Option
枚举的Some
成员可以包含任意类型的数据。这里是一些包含数字类型和字符串类型Option
值的例子:
let some_number = Some(5);
+let some_string = Some("a string");
+
+let absent_number: Option<i32> = None;
+
+如果使用None
而不是Some
,需要告诉 Rust Option<T>
是什么类型的,因为编译器只通过None
值无法推断出Some
成员的类型。
当有一个Some
值时,我们就知道存在一个值,而这个值保存在Some
中。当有个None
值时,在某种意义上它跟空值是相同的意义:并没有一个有效的值。那么,Option<T>
为什么就比空值要好呢?
简而言之,因为Option<T>
和T
(这里T
可以是任何类型)是不同的类型,编译器不允许像一个被定义的有效的类型那样使用Option<T>
。例如,这些代码不能编译,因为它尝试将Option<i8>
与i8
相比:
let x: i8 = 5;
+let y: Option<i8> = Some(5);
+
+let sum = x + y;
+
+如果运行这些代码,将得到类似这样的错误信息:
+error[E0277]: the trait bound `i8: std::ops::Add<std::option::Option<i8>>` is
+not satisfied
+ -->
+ |
+7 | let sum = x + y;
+ | ^^^^^
+ |
+
+哇哦!事实上,错误信息意味着 Rust 不知道该如何将Option<i8>
与i8
相加。当在 Rust 中拥有一个像i8
这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需判空。只有当使用Option<i8>
(或者任何用到的类型)是需要担心可能没有一个值,而编译器会确保我们在使用值之前处理为空的情况。
换句话说,在对Option<T>
进行T
的运算之前必须转为T
。通常这能帮助我们捕获空值最常见的问题之一:假设某值不为空但实际上为空。
无需担心错过非空值的假设(和处理)让我们对代码更加有信心,为了拥有一个可能为空的值,必须显式的将其放入对应类型的Option<T>
中。接着,当使用这个值时,必须明确的处理值为空的情况。任何地方一个值不是Option<T>
类型的话,可以安全的假设它的值不为空。这是 Rust 的一个有意为之的设计选择,来限制空值的泛滥和增加 Rust 代码的安全性。
那么当有一个Option<T>
的值时,如何从Some
成员中取出T
的值来使用它呢?Option<T>
枚举拥有大量用于各种情况的方法:你可以查看相关代码。熟悉Option<T>
的方法将对你的 Rust 之旅提供巨大的帮助。
总的来说,为了使用Option<T>
值,需要编写处理每个成员的代码。我们想要一些代码只当拥有Some(T)
值时运行,这些代码允许使用其中的T
。也希望一些代码当在None
值时运行,这些代码并没有一个可用的T
值。match
表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。
match
控制流运算符++ch06-02-match.md +
+
+commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d
Rust 有一个叫做match
的极为强大的控制流运算符,
if let