定义枚举
rust中的枚举跟js中虽然不一样但是类似,使用起来也累死。比如我们想要定义一个枚举,用于获取ip地址的版本,是v4的还是v6的,我们就可以这样去定义一个枚举类型的示例。
#[derive(Debug)]
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {
println!("{:?}", ip_kind)
}
cargo run
warning: unused variable: `four`
--> src\main.rs:8:9
|
8 | let four = IpAddrKind::V4;
| ^^^^ help: if this is intentional, prefix it with an underscore: `_four`
|
= note: `#[warn(unused_variables)]` on by default
warning: unused variable: `six`
--> src\main.rs:9:9
|
9 | let six = IpAddrKind::V6;
| ^^^ help: if this is intentional, prefix it with an underscore: `_six`
warning: 2 warnings emitted
Finished dev [unoptimized + debuginfo] target(s) in 0.46s
Running `target\debug\cargo_learn.exe`
V4
V6
枚举值
我们可以像这样创建IpAddrKind的两个变体的每一个的实例:
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
注意,枚举的变体在其标识符下命名空间,并且我们使用双冒号将两者分开。 之所以有用,是因为现在值IpAddrKind::V4和IpAddrKind::V6都具有相同的类型:IpAddrKind。 例如,我们然后可以定义一个使用任何IpAddrKind的函数:
fn route(ip_kind: IpAddrKind) {}
我们可以使用任一变体来调用此函数:
route(IpAddrKind::V4);
route(IpAddrKind::V6);
使用枚举具有更多优势。目前,我们需要更多地考虑我们的IP地址类型,因此我们无法存储实际的IP地址数据; 我们只知道那是什么。可以参考struct来解决该问题:
fn main() {
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,它具有两个字段:一个类型字段,类型字段为IpAddrKind(我们之前定义的枚举),另一个地址类型为String。 我们有此结构的两个实例。 第一个为home,其值为IpAddrKind :: V4,具有关联的地址数据127.0.0.1。 第二个实例回送具有IpAddrKind的另一个变体作为其种类值V6,并具有与:: 1关联的地址。 我们使用了一种将种类和地址值捆绑在一起的结构,因此现在该变体已与该值相关联。
通过将数据直接放入每个枚举变量中,我们可以仅使用枚举而不是结构内部的枚举以更简洁的方式表示相同的概念。 IpAddr枚举的新定义表明,V4和V6变体都将具有关联的String值:
fn main() {
#[derive(Debug)]
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
println!("{:?}", home);
println!("{:?}", loopback);
}
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.63s
Running `target\debug\cargo_learn.exe`
V4("127.0.0.1")
V6("::1")
我们将数据直接附加到枚举的每个变体,因此不需要额外的结构。
使用枚举而不是结构还有另一个优势:每个变体可以具有不同类型和数量的关联数据。 版本4类型的IP地址将始终具有四个数字部分,其值将介于0到255之间。如果我们想将V4地址存储为四个u8值,但仍将V6地址表示为一个字符串值,则无法使用结构来实现。 枚举可以轻松处理这种情况:
fn main() {
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:它具有我们已定义和使用的确切枚举和变体,但它以两种不同的结构形式将地址数据嵌入变体中,每种结构的定义都不同:
#![allow(unused_variables)]
fn main() {
struct Ipv4Addr {
// --snip--
}
struct Ipv6Addr {
// --snip--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
}
此代码说明可以将任何类型的数据放入枚举变量内:例如,字符串,数字类型或结构。甚至可以包含另一个枚举!而且,标准库类型通常不会比我们想像的复杂得多。
请注意,即使标准库包含IpAddr的定义,我们仍然可以创建和使用自己的定义而不会发生冲突,因为我们尚未将标准库的定义引入我们的使用范围。
让我们看另一个枚举示例:该枚举的变体中嵌入了多种类型。
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
//这个枚举有四个不同类型的变体:
//Quit 完全没有与之关联的数据。
//Move 内部包含一个匿名结构。
//Write 包括单个字符串。
//ChangeColor包含三个i32值。
用上述示例中的变量定义枚举类似于定义不同类型的结构定义,只是枚举不使用struct关键字,并且所有变量都在Message类型下分组在一起。 以下结构可以保存与前述枚举变量所保存的数据相同的数据:
struct QuitMessage; // 单位结构
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // 元组结构
struct ChangeColorMessage(i32, i32, i32); // 元组结构
但是,如果我们使用不同的结构,每个结构都有自己的类型,我们将无法像前面例子中定义的Message枚举那样轻松地定义一个函数来接收任何单独类型的message。
枚举和结构之间还有另外一个相似之处:正如我们可以使用impl在结构上定义方法一样,我们也可以在枚举上定义方法。 这是一个可以在Message枚举中定义的名为call的方法:
fn main() {
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) { //注意self并不是枚举本身,而是被调用枚举的其中一个实例,比如m调用传入的self就是`Write("hello")`
// 方法主体将在此处定义
}
}
let m = Message::Write(String::from("hello"));
m.call();
}
Option枚举及其相对于空值的优势
Option是标准库定义的另一个枚举。 Option类型在很多地方都使用,因为它对非常常见的情况进行编码,在这种情况下,值可以是某些值,也可以是空值。用类型系统表达这一概念意味着编译器可以检查是否已经处理了所有应该处理的情况。
Rust没有许多其他语言具有的null功能。Null是一个值,表示那里没有任何值。在具有null的语言中,变量始终可以处于以下两种状态之一:null或非null。
空值的问题在于,如果尝试将空值用作非空值,则会出现某种错误。 由于此null或not-null属性无处不在,因此很容易产生这种错误。
但是,null试图表达的概念仍然是一个有用的概念:null是当前由于某种原因而无效或不存在的值。
问题不在于概念,而在于具体的实现。 这样,Rust没有空值,但是它确实有一个枚举,该枚举可以对存在或不存在的值的概念进行编码。 该枚举是Option
#![allow(unused_variables)]
fn main() {
enum Option {
Some(T),
None,
}
}
Option
fn main() {
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option = None;
}
如果我们使用None而不是Some,则需要告诉Rust我们拥有哪种Option
当我们有一个Some值时,我们知道存在一个值并将该值保存在Some中。 从某种意义上讲,当我们使用None值时,它与null含义相同:我们没有有效的值。 那么为什么拥有Option
简而言之,由于Option
fn main() {
let x: i8 = 5;
let y: Option = Some(5);
let sum = x + y;
}
$ cargo run
|
5 | let sum = x + y;
| ^ no implementation for `i8 + std::option::Option`
|
= help: the trait `std::ops::Add>` is not implemented for `i8`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums`.
To learn more, run the command again with --verbose.
*实际上,此错误消息表示Rust不了解如何添加i8和Option
换句话说,必须先将Option
不必担心错误地假设非空值可以帮助我们对代码更有信心。为了具有可能为空的值,必须通过使该值的类型为Option
那么,当我们拥有类型为Option
通常,为了使用Option