rust学习——枚举(enums)基本概念

定义枚举

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 枚举是如此有用,以至于它甚至都包含在序言中; 您无需将其明确纳入范围。 此外,它的变体也是如此:您可以直接使用Some和None,而无需使用Option ::前缀。 Option 枚举仍然只是常规枚举,而Some(T)和None仍然是Option 类型的变体。

是一个泛型类型参数,表示Option枚举的Some变体可以容纳任何类型的数据。 以下是一些使用Option值保存数字类型和字符串类型的示例:

fn main() {
    let some_number = Some(5);
    let some_string = Some("a string");

    let absent_number: Option = None;
}

如果我们使用None而不是Some,则需要告诉Rust我们拥有哪种Option 类型,因为编译器无法通过仅查看None值来推断Some变量将持有的类型。

当我们有一个Some值时,我们知道存在一个值并将该值保存在Some中。 从某种意义上讲,当我们使用None值时,它与null含义相同:我们没有有效的值。 那么为什么拥有Option 比拥有null更好呢?

简而言之,由于Option 和T(其中T可以是任何类型)是不同的类型,因此编译器不会让我们使用Option 值,就好像它绝对是有效值一样。 例如,此代码无法编译,因为它试图将i8添加到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 ,因为它们的类型不同。当我们在Rust中具有类似i8的类型的值时,编译器将确保我们始终具有有效值。我们可以放心地进行操作,而不必在使用该值之前检查null。只有当我们拥有Option (或我们正在使用的任何类型的值)时,我们才不必担心可能没有值,并且编译器将确保我们在使用值之前先处理这种情况。
换句话说,必须先将Option 转换为T,然后才能对其执行T操作。通常,这有助于解决最常见的null问题之一:假设某件事实际上并非为null。*

不必担心错误地假设非空值可以帮助我们对代码更有信心。为了具有可能为空的值,必须通过使该值的类型为Option 来显式选择加入。然后,当使用该值时,要求显式处理该值为null的情况。任何值的类型都不是Option 的位置,我们可以放心地假定该值不为null。这是Rust的一项故意设计决策,旨在限制null的普遍性并提高Rust代码的安全性。

那么,当我们拥有类型为Option 的值时,如何从Some变量中获取T值,以便可以使用该值? Option 枚举具有许多在各种情况下有用的方法;例如,可以在其文档中查看它们。在使用Rust的过程中,熟悉Option 上的方法将非常有用。

通常,为了使用Option 值,我们需要具有可处理每个变体的代码。需要一些仅在具有Some(T)值时才运行的代码,并且允许该代码使用内部T。如果具有None值并且该代码没有T值可用的情况,match表达式是一个控制流构造,与enum一起使用时会执行此操作:它将运行不同的代码,具体取决于它具有enum的哪个变体,并且该代码可以使用匹配值内的数据。

你可能感兴趣的:(rust)