Rust学习教程17 - 枚举enum
Posted 孙飞 Sunface
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Rust学习教程17 - 枚举enum相关的知识,希望对你有一定的参考价值。
本文节选自<<Rust语言圣经>>一书
欢迎大家加入Rust编程学院,一起学习交流:
QQ群:1009730433
枚举
枚举(enum或enumeration)允许你通过列举可能的成员来定义一个**枚举类型
**,例如扑克牌花色:
enum PokerSuit
Clubs,
Spades,
Diamonds,
Hearts,
如果在此之前你没有在其它语言中使用过枚举,那么可能需要花费一些时间来理解这些概念,一旦上手,就会发现枚举的强大,甚至对它爱不释手,枚举虽好,可不要滥用哦。
再回到之前创建的PokerSuit
,扑克总共有四种花色,而这里我们枚举出所有的可能值,这也正是枚举
名称的由来。
任何一张扑克,它的花色肯定会落在四种花色中,而且也只会落在其中一个花色上,这种特性非常适合枚举的使用,因为**枚举值
**只可能是其中一个成员。抽象来看,四种花色尽管是不同的花色,但是它们都是扑克花色这个概念,因此当某个函数处理扑克花色时,可以把它们当作相同的类型进行传参。
细心的读者应该注意到,我们对之前的枚举类型
和枚举值
进行了重点标注,这是因为对于新人来说容易混淆相应的概念,总而言之:
枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实现。
枚举值
现在来创建PokerSuit
枚举类型的两个成员实例:
let heart = PokerSuit::Hearts;
let diamond = PokerSuit::Diamonds;
我们通过::
操作符来访问PokerSuit
下的具体成员,从代码可以清晰看出,heart
和diamond
都是PkerSuit
枚举类型的,接着可以定义一个函数来使用它们:
fn main()
let heart = PokerSuit::Hearts;
let diamond = PokerSuit::Diamonds;
print_suit(heart);
print_suit(diamond);
fn print_suit(card: PokerSuit)
println!(":?",card);
print_suit
函数的参数类型是PokerSuit
,因此我们可以把heart
和diamond
传给它,虽然heart
是基于PokerSuit
下的Hearts
成员实例化的,但是它是货真价实的PokerSuit
枚举类型。
接下来,我们想给扑克牌变得更加实用,那么需要给每张牌赋予一个值:A
(1)-K
(13),这样再加上花色,就是一张真实的扑克牌了,例如红心A。
目前来说,枚举值还不能带有值,因此先用结构体来实现:
enum PokerSuit
Clubs,
Spades,
Diamonds,
Hearts,
struct PokerCard
suit: PokerSuit,
value: u8
fn main()
let c1 = PokerCard
suit: PokerSuit::Clubs,
value: 1,
;
let c2 = PokerCard
suit: PokerSuit::Diamonds,
value: 12,
;
这段代码很好的完成了它的使命,通过结构体PokerCard
来代表一张牌,结构体的suit
字段表示牌的花色,类型是PokerSuit
枚举类型,value
字段代表扑克牌的值。
可以吗?可以!好吗?说实话,不咋地,因为还有简洁的多的方式来实现:
enum PokerCard
Clubs(u8),
Spades(u8),
Diamonds(u8),
Hearts(u8),
fn main()
let c1 = PokerCard::Spades(5);
let c2 = PokerCard::Diamonds(13);
直接将数据信息关联到枚举成员上,直接省去近一半的代码,这种实现漂亮不?
而且不仅仅如此,同一个枚举类型下的不同成员还能持有不同的类型,例如让部分花色打印1-13的字样,另外花色打印上A-K的字样:
enum PokerCard
Clubs(u8),
Spades(u8),
Diamonds(char),
Hearts(char),
fn main()
let c1 = PokerCard::Spades(5);
let c2 = PokerCard::Diamonds('A');
回想一下,遇到这种不同类型的情况,再用我们之前的结构体实现方式,可行吗?也许可行,但是会复杂很多。
再来看一个来自标准库中的例子:
struct Ipv4Addr
// --snip--
struct Ipv6Addr
// --snip--
enum IpAddr
V4(Ipv4Addr),
V6(Ipv6Addr),
该例子跟我们之前的扑克牌很像,只不过枚举成员包含的类型更复杂了,变成了结构体:分别通过Ipv4Addr
和Ipv4Addr
来定义两种不同的IP数据。
从这些例子可以看出,任何类型的数据都可以放入枚举成员中: 例例如字符串、数值、结构体甚至另一个枚举。
增加一些挑战?先看以下代码:
enum Message
Quit,
Move x: i32, y: i32 ,
Write(String),
ChangeColor(i32, i32, i32),
fn main()
let m1 = Message::Quit;
let m2 = Message::Movex:1,y:1;
let m3 = Message::ChangeColor(255,255,0);
该枚举类型代表一条消息,它包含四个不同的成员:
Quit
没有任何关联数据Move
包含一个匿名结构体Write
包含一个String
字符串ChangeColor
包含三个i32
当然,我们也可以用结构体的方式来定义这些消息:
struct QuitMessage; // 元结构体
struct MoveMessage
x: i32,
y: i32,
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体
由于每个结构体都有自己的类型,因此我们无法在需要同一类型的地方进行使用,例如某个函数它的功能是接受消息并进行发送,那么用枚举的方式,就可以接收不同的消息,但是用结构体,该函数无法接受4个不同的结构体作为参数。
而且从代码规范角度来看,枚举的实现更简洁,代码内聚性更强,不像结构体的实现,分散在各个地方。
最后,再用一个实际项目中的设计考虑,来结束枚举类型的语法学习。
例如我们有一个web服务,需要接受用户的长连接,假设连接有两种:TcpStream和TlsStream,但是我们希望对这两个连接的处理流程相同,也就是用同一个函数来处理这两个连接,代码如下:
func new (stream: TcpStream)
let mut s = stream;
if tls
s = negotiate_tls(stream)
// websocket是一个WebSocket<TcpStream>或者
// WebSocket<native_tls::TlsStream<TcpStream>>类型
websocket = WebSocket::from_raw_socket(
stream, ......)
此时,枚举类型就能帮上大忙:
enum Websocket
Tcp(Websocket<TcpStream>),
Tls(Websocket<native_tls::TlsStream<TcpStream>>),
Option枚举用于处理空值
在其它编程语言中,往往都有一个null
关键字,该关键字用于表明一个变量当前的值为空(不是零值,例如整形的零值是0),也就是不存在值。当你对这些null
进行操作时,例如调用一个方法,就会直接抛出异常,导致程序的崩溃,因此我们在编程时需要格外的小心去处理这些null
空值。
Tony Hoare,null的发明者,曾经说过有非常有名的话
我称之为我十亿美元的错误。当时,我在使用一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过在设计过程中,我未能抵抗住诱惑,引入了空引用的概念,因为它非常容易实现。就是因为这个决策,引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。
然后空值的表达依然非常有意义,因为空值表示当前时刻变量的值是缺失的。因此,Rust吸取了众多教训,决定抛弃null
,而改为使用Option
枚举变量来表述这种结果:
**一个变量要么有值:Some(T)
, 要么为空: None
,定义如下:
enum Option<T>
Some(T),
None,
其中T
是泛型参数,Some(T)
表示该枚举成员的数据类型是T
, 换句话说,Some
可以包含任何类型的数据。
Option<T>
枚举是如此有用以至于它甚至被包含在了prelude
(Rust会将最常用的类型、函数等提前引入进来,避免我们再手动引入)之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要Option::
前缀来直接使用Some
和 None
。即便如此Option<T>
也仍是常规的枚举,Some(T)
和 None
仍是 Option<T>
的成员。
再来看以下代码:
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<i8>
(Option<T>
) 与 i8
(T
) 相加:
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
-->
|
5 | let sum = x + y;
| ^ no implementation for `i8 + std::option::Option<i8>`
|
很好!事实上,错误信息意味着 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
的大致模样,在模式匹配中,我们会详细讲解:
fn plus_one(x: Option<i32>) -> Option<i32>
match x
None => None,
Some(i) => Some(i + 1),
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
plus_one
通过match
来处理不同Option
的情况。
以上是关于Rust学习教程17 - 枚举enum的主要内容,如果未能解决你的问题,请参考以下文章