前端学Rust - 枚举

2,780 阅读5分钟

枚举

枚举就是允许我们列举所有可能的值来定义一个类型(概念上与TS的枚举基本没差别,实际使用会有不同)

定义枚举

IP地址枚举

enum IpAddrKind{
    V4,
    V6
}

enum关键字 + 枚举名 + 大括号 + 变体 (枚举里所有可能的值叫枚举的变体)

创建枚举值

enum IpAddrKind{
    V4,
    V6
};
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

枚举的变体都位于标识符的命名空间下,使用 :: 符号进行分割。

将数据附加到枚举的变体中

在rust中,允许将数据直接附加到枚举的变体中, 且附加的数据可以是任意类型。 定义方式

enum IpAddrKind{
    V4(String),
    V6(String)
}

优点

  • 不需要使用额外的struct
  • 每个变体都可以拥有不同的类型及关联的数据量

假设我们不使用数据附加到枚举的变体中的方式去定义一个IpAddr,示例:

#[derive(Debug)]
enum IpAddrKind{
    V4,
    V6
}

#[derive(Debug)]
struct IpAddr {      // 需要单独定义一个struct
    kind:IpAddrKind,
    address:String 
} 

fn main() {
    let home = IpAddr {
        kind:IpAddrKind::V4,
        address:String::from("127.0.0.1")
    };

    let loopback = IpAddr {
        kind:IpAddrKind::V6,
        address:String::from("::1")
    };
    println!("{:?},{:?}",home,loopback)
}

将数据直接附加到枚举的变体中,实际表达内容是一致的。示例:

#[derive(Debug)]
enum IpAddr {
    V4(u8,u8,u8,u8),
    V6(String)
}

fn main() {
    let home = IpAddr::V4(127,0,0,1);
    let loopback = IpAddr::V6(String::from("::1"));
    println!("{:?},{:?}",home,loopback)
}

为枚举定义方法

和struct一样,使用impl关键字,示例:

#[derive(Debug)]
enum Message {
    Quit,
    Move {x:i32,y:i32},
    Write(String),
    ChangeColor(i32,i32,i32),
}

impl Message {
    fn call(&self) {
        println!("{:?}",self)
    }
}

fn main(){
    let q = Message::Quit;
    let m = Message::Move{ x:12,y:24 };
    let w = Message::Write(String::from("hello"));
    let c = Message::ChangeColor(0,255,255);

    m.call()
}

其余可参考struct相关文章(还没写…)中的定义方法的模块,基本一致。

枚举与模式匹配

Option枚举

Option枚举直接定义在标准库的预导入(Prelude)模块中。 该枚举用于描述: 某个值可能存在(可能是某种类型)或可能不存在。(额…好像有点绕)

那为什么会存在一个这么绕的枚举? 那是因为 Rust没有Null ,在很多其它语言中,都存在Null,它是一个表示”没有值“的值。但是Null的创作者曾经说过,Null引用是一个:billion-dollar mistake。所以Rust摒弃了Null类型,但是尽管Null存在很多问题,但是Null的概念还是很有用的,语言还是需要描述:因为某种原因而变为无效或缺失的值。所以Rust提供了一个类似Null概念的枚举,它就是 Option(泛型,前端玩家直接参考TS)。

在标准库中的定义

enum Option<T>{
    Some(T),
    None,
}

因为它是在预导入模块中的,所以在使用时是不需要 Option::Some(T) 或者 Option::None 的形式去写的,直接使用 Some<T>None 即可,示例:

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

    let absent_number: Option<i32> = None;
}

image.png image.png

从图中可以看出,第一行定义Some(5),Rust的编译器是可以推断出当前的 T 是i32类型,但是最后一行,如果不手动指定 Option<i32> 类型,仅仅赋值None这个变体,编译器就会无法推断当前值的类型,就会编译报错,所以定义None时,必须手动指定类型。

Option 和 Null 相比的优势

  • 在Rust中 Option<T>T 是两种不同的类型,不可以把Option<T>直接当成T来使用
  • 若想使用 Option<T> 中的 T 则必须将它转换为 T ,这就避免了None值泛滥的情况

错误示例:

fn main(){
    let x:i8 = 5;
    let y:Option<i8> = Some(5);

    let some = x + y; // 这样写就会报错:cannot add `std::option::Option<i8>` to `i8`
}

正确用例:

fn main(){
    let x:i8 = 5;
    let y:Option<i8> = Some(5);

    let some = x + y.unwrap(); // 转换为i8类型

    println!("{}",some)
}

前端瞎叨叨: 在js其实也有类似的问题,尽管因为隐式类型转换基本减少了运算时出现的问题,但是当我们在使用类似__a.b__这样的语法时,也会存在VM370:1 Uncaught TypeError: Cannot read properties of null (reading 'b') 这种错误。从TS借鉴到标准里的可选链就是为了解决这类问题的,但是这毕竟还是需要从”人“的维度来避免。 Rust从前面的基础学习中就不难发现,他就是从”规范”的维度来避免的,如果你的写法是可能会出现错误的,那就在编译阶段卡住,借助这种方式来实现语言的安全。

控制流运算符 - match

match允许一个值与一系列模式进行匹配,并执行匹配的模式对应的代码。模式可以是字面值、变量名、通配符等等。示例:

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

fn value_in_coins(coin:Coin)->u8{
    match coin{
        Coin::Penny => {
            println!("penny");
            1
        },
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main(){
    println!("{:?}",value_in_coins(Coin::Penny))
}

匹配的分支可以绑定到被配置对象的部分值,借此我们可以从enum变体中取值,示例:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_coins(coin:Coin)->u8{
    match coin{
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            print!("{:?}",state);
            25
        },
    }
}

fn main(){
    println!("{:?}",value_in_coins(Coin::Quarter(UsState::Alabama)))
}

既然我们知道match的使用,且匹配分支可以从enum变体中取值,那我们回到Option<T>类型的使用上:通过match匹配Option<T>,示例:

fn main(){
    let num = Some(3);
    let num_plus_five = plus_five(num);
    println!("{:?}",num_plus_five)
}

fn plus_five(x:Option<i32>)->Option<i32>{
    match x{
        None=>None,
        Some(num)=>Some(num+5)
    }
}

match匹配必须穷举所有的可能性,如果表达式可能性太多,可以使用:_通配符来代替没有写出来的值,示例:

fn main(){
    let v:u8 = 0;
    match v {
        1 => print!("1"),
        3 => print!("3"),
        5 => print!("5"),
        _ => print!("other"), // 要放在最后面,且如果不加则会报错
    }
}