Skip to content

Enum

枚举类型类似于结构体,可以同时具备多种数据,但最终使用时只能具备一种数据。

声明

rust
enum IpAddrKind {  // 使用 enum 关键字声明枚举,IpAddrKind 是枚举体名称
    V4,  // 枚举体中可能包含的元素,但最终只能选择 1 个
    V6,
}

使用:

rust
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

使用枚举体生成的变量类型仍然是枚举,可以定义一个函数,接收枚举类型的参数,上述定义的变量可以传入:

rust
fn route(ip_type: IpAddrKind) {}

route(four);
route(six);

可以在声明的时候初始化值:

rust
enum IpAddrKind2 {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddrKind2::V4(127, 0, 0, 1);
let loopback = IpAddrKind2::V6(String::from("::1"));

枚举在声明时也可以包含多个类型:

rust
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u32, u32, u32),
}

和结构体一样,也支持定义枚举类型的方法:

rust
impl Message {
    fn call(&self) {}  // 第一个参数也是 self
}

Option<T>

Option 类型在 Rust 中比较经典,为枚举类型,其可能有 2 种值,一种是 None ,也就是空,可以理解为 Python 中的 None 等,另一种是自定义的值。Option 类型的主要目的是为了显式表达一个值可能存在,也可能不存在的情况,避免使用不安全的 null 值,从而减少空指针错误。

rust
let some_number = Some(5);
let some_string = Some("a string");
// absent_number 是一个 Option<i32> 类型,而不是 i32 类型
let absent_number: Option<i32> = None; // 这里允许为 None ,但必须给定类型注解为 Option<i32>

使用 Some 表示 Option 中的另一种非空的可能取值,这里的 Some(5) ,即表示另一种的取值类型只可能是 i32 类型(除非显式定义其他类型如 u8 )。

当定义一个 Option 类型为 None 时,需要显式声明类型注解,因为 Rust 中本身不存在 None ,且同时说明当前变量除了可能取值为 None 外,另一种可能的取值为 i32(上述)。

Option 类型定义的变量的类型是 Option 还是具体的值的类型? 如上述的 some_number 的类型是 Option<i32> 还是具体的 i32 ?答案是 Option<i32> ,使用 Option 定义的变量的类型永远都是 Option ,不会是具体值的类型,不是说当值变为 None 时,其类型就从原本的 i32 变为了 None

match

match 是 rust 中的模式匹配,他根据传入的枚举体类型的数据,返回相应的值:

rust
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            // 在返回值之前也可以进行一些处理
            println!("This is a penny!");
            1
        }  // 当定义的 Coin 枚举类型的变量为 Penny 时,返回其值为 1
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

枚举体中也可以嵌套枚举体:

rust
#[derive(Debug)] // 为了后续可以打印
enum UsState {
    Alabama,
    Alaska,
}

enum Coin2 {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}
fn value_in_cents2(coin: Coin2) -> u8 {
    // match 是 rust 中的模式匹配,他根据传入的 Coin 类型的数据,返回相应的值
    match coin {
        Coin2::Penny => 1,
        Coin2::Nickel => 5,
        Coin2::Dime => 10,
        Coin2::Quarter(state) => {
            println!("Now state value is {:?}", state);
            25
        }
    }
}

let coin = Coin2::Quarter(UsState::Alaska); // 🟢 这是阿拉斯加州的 25 美分硬币
let value = value_in_cents2(coin);
println!("Value in cents: {}", value);  // 输出:Value in cents: 25

枚举体 Coin2 中嵌套了一个枚举体 UsState ,在后面的 match 处理时,传入的 state 就是一个 UsState 枚举体类型,当然,这里并没有使用具体的 state 变量本身,最终返回值为 25

后面定义了 coin 变量是 Coin2 枚举体中的 Quarter 数据,则应传入其需要的枚举体,这里传入的是 UsState 中的 Alaska ,最终,在调用 value_in_cents2 函数时,match 将传入的 coin 识别到 Coin2::Quarter(state) 中,最终返回 25 ,因此最终输出 value 的值为 25

match 的应用

rust
fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        // Option 有两种情况,使用 match 需要全部考虑到,少一种无法通过编译
        None => None,
        Some(i) => Some(i + 1),
    }
}

let x = Some(5);
let six = plus_one(x);  // x 是 Option<i32> 类型,经过 match 判定为符合 Some(i) ,返回 Some(i + 1)
let seven = plus_one(six);
let none: Option<i32> = plus_one(None);

如上述代码注释种所述,match 的处理不仅限于 Option 类型,对于任何可能取多种值的类型都可以使用 match 判断,如 i32u8 等,只是这些类型可能的取值非常多,如 u8 可能取值有 0~255 ,有 256 种,且 match 必须考虑到这些取值的所有情况。不可能将这 255 种情况一一列举,因此有以下处理方法:

rust
let dice_roll: u8 = 9;
match dice_roll {
    1 => test_func(),
    ot => println!("This was not 1: {}", ot), // 如果最终的变量 ot 没有被使用到,可以使用下划线 _ 表示
}

fn test_func() {}

最终使用一个变量兜底。

match 和所有权

观察以下代码示例:

rust
let opt: Option<String> = Some(String::from("hello world"));

match opt {
    Some(_) => println!("Some!"), // 由于是下划线表示,没有使用到这个变量,因此所有权不发生移动,后续仍可继续访问 opt
    None => println!("None!"),
}
println!("{:?}", opt);

// match opt {
//     Some(t) => println!("{:?}", t), // 因为 t 被使用了,所有权发生了移动,后续 opt 不可再使用
//     None => println!("None!"),
// }
// println!("{:?}", opt);  ❌

// 如果不希望所有权移动,只需使用引用表示
match &opt {
    Some(t) => println!("{:?}", t),
    None => println!("None!"),
}
println!("{:?}", opt);
  • 当在 match 中使用下划线时,变量的所有权不会发生转移,后续仍可继续使用该变量
  • 当使用到了该变量,则所有权发生转移,转移到了 match 中的变量中
  • 不希望所有权移动,只需使用引用,这样传入 match 中的数据是原本变量的引用,内部的变量 t 也是一个引用,不会发生所有权转移

if let

对比 matchif let 用于只处理可能的多种取值的变量中的某一种情况,如果使用 match ,即使使用变量兜底,也得将这个语句写出来表示(或者说占位),使用 if let 可以直接处理这种情况。

假设当前只需要处理 Option 类型变量 a 会取值的情况:

rust
let a = Some(5);
match a {
    Some(i) => println!("This situation needs to be dealt with"),
    _ => println!(
        "Even if this case does not need to be handled, it needs to be handled with an '_'"
    ),
}
if let Some(i) = a {
    // 语法说明:只处理是 Some(i) 的情况,处理的对象是变量 a
    println!("Matched, value: {}", i);
} else {
    println!("None");
}

使用 match 需要使用下划线占位,但使用 if let 只需一次处理,且后续可以继续跟 else 增加处理其他的情况。相比于 match 更方便一些。

本节全部代码

rust
fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    // four 和 six 都可以作为参数传入到 route() 函数中,说明 four 和 six 都是 IpAddrKind 类型
    route(four);
    route(six);

    // 枚举类型中可以直接给定值
    let home = IpAddrKind2::V4(127, 0, 0, 1);
    let loopback = IpAddrKind2::V6(String::from("::1"));

    // Option<T> 枚举,用于处理可能为 None 的情况
    let some_number = Some(5);
    let some_string = Some("a string");
    // absent_number 是一个 Option<i32> 类型,而不是 i32 类型
    let absent_number: Option<i32> = None; // 这里允许为 None ,但必须给定类型注解为 Option<i32>

    // match 控制流
    let coin = Coin2::Quarter(UsState::Alaska); // 🟢 这是阿拉斯加州的 25 美分硬币
    let value = value_in_cents2(coin);
    println!("Value in cents: {}", value);

    // 使用 Option<T>
    let x = Some(5);
    let six = plus_one(x);
    let seven = plus_one(six);
    let none: Option<i32> = plus_one(None);

    // match 必须穷尽所有可能的取值情况
    let dice_roll: u8 = 9;
    match dice_roll {
        1 => test_func(),
        ot => println!("This was not 1: {}", ot), // 如果最终的变量 ot 没有被使用到,可以使用下划线 _ 表示
    }

    // match 和所有权
    let opt: Option<String> = Some(String::from("hello world"));

    match opt {
        Some(_) => println!("Some!"), // 由于是下划线表示,没有使用到这个变量,因此所有权不发生移动,后续仍可继续访问 opt
        None => println!("None!"),
    }
    println!("{:?}", opt);

    // match opt {
    //     Some(t) => println!("{:?}", t), // 因为 t 被使用了,所有权发生了移动,后续 opt 不可再使用
    //     None => println!("None!"),
    // }
    // println!("{:?}", opt);  ❌

    // 如果不希望所有权移动,只需使用引用表示
    match &opt {
        Some(t) => println!("{:?}", t),
        None => println!("None!"),
    }
    println!("{:?}", opt);

    // 使用 if let 只处理一种情况
    // 如果使用 match ,如果当前变量有 2 种取值,即使只需要其某一种情况的取值,也需要处理另一种情况的取值
    let a = Some(5);
    match a {
        Some(i) => println!("This situation needs to be dealt with"),
        _ => println!(
            "Even if this case does not need to be handled, it needs to be handled with an '_'"
        ),
    }
    if let Some(i) = a {
        // 语法说明:只处理是 Some(i) 的情况,处理的对象是变量 a
        println!("Matched, value: {}", i);
    } else {
        println!("None");
    }
}

fn route(ip_type: IpAddrKind) {}

enum IpAddrKind {
    V4, // 枚举体中可能包含的元素,但最终只能选择 1 个
    V6,
}

enum IpAddrKind2 {
    V4(u8, u8, u8, u8),
    V6(String),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u32, u32, u32),
}

// 枚举也可以实现方法,和结构体类似,第一个参数也是 self
impl Message {
    fn call(&self) {}
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    // match 是 rust 中的模式匹配,他根据传入的 Coin 类型的数据,返回相应的值
    match coin {
        Coin::Penny => {
            // 在返回值之前也可以进行一些处理
            println!("This is a penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

#[derive(Debug)] // 为了后续可以打印
enum UsState {
    Alabama,
    Alaska,
}

enum Coin2 {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}
fn value_in_cents2(coin: Coin2) -> u8 {
    // match 是 rust 中的模式匹配,他根据传入的 Coin 类型的数据,返回相应的值
    match coin {
        Coin2::Penny => 1,
        Coin2::Nickel => 5,
        Coin2::Dime => 10,
        Coin2::Quarter(state) => {
            println!("Now state value is {:?}", state);
            25
        }
    }
}

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        // Option 有两种情况,使用 match 需要全部考虑到,少一种无法通过编译
        // match 表达式的分支,必须穷尽,不仅限于 Option
        // 即,match 的变量拥有的所有可能的取值,都应考虑到
        None => None,
        Some(i) => Some(i + 1),
    }
}

fn test_func() {}

练习

rust
enum Location {
    Point(i32),
    Range(i32, i32),
}

fn main() {
    // 问题:以下代码能否通过编译?
    let l: Location = Location::Range(0, 5);
    let n = match l {
        Location::Point(_) => -1,
        Location::Range(_, a) => a,
        Location::Range(0, _) => 0,
        _ => -2,
    };
    println!("{n}");
}

答案:

可以,输出 5

因为会在第二个分支被捕获,即 Location::Range(_, a) => a ,将 5 赋值给 a ,然后 match 最终返回的是 a(箭头后面指向的 a ),因此 n 的值为 a 的值,也就是 5 ,后续输出 n 即输出 5

即使将两个 a 改成 n ,也可以输出 5 ,因为 match 的返回值,即箭头指向的返回值 n 还是会给到 let n 中的 n ,后面输出的 n 就是 let n 中的 n ,输出 5