Rust模式匹配

模式匹配

模式匹配是从函数式编程语言(例如:Haskell,Lisp)吸收而来的,用于为复杂的类型系统提供一个轻松的解构能力。rust使用match来提供模式匹配的功能。mathc类似于其它编程语言中的switch-case,但是远比switch-case强大。match的通用模式如下所示。

match target {
    模式1 => 表达式1,
    模式2 => {
        语句1;
        语句2;
        表达式2
    },
    _ => 表达式3
}

该形式清晰的说明了何为模式,何为模式匹配:将模式与 target 进行匹配,即为模式匹配,而模式匹配不仅仅局限于 match,还有if let。一个实际的例子如下所示:

fn main() { enum Coin { Penny,
        Nickel,
        Dime,
        Quarter,
    }
    fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!");
                1
            },
            Coin::Nickel => 5,
            Coin::Dime => 10,
            Coin::Quarter => 25,
        }
    }
    let coin = Coin::Dime;
    let x = value_in_cents(coin);
    println!("{}", x);
}
  • match 的匹配必须要穷举出所有可能,因此这里用 _ 来代表未列出的所有可能性。当我们不想使用通配模式获取的值时,请使用 _ ,这是一个特殊的模式,可以匹配任意值而不绑定到该值。这告诉 Rust 我们不会使用这个值,所以 Rust 也不会警告我们存在未使用的变量。
  • match 的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同
  • match 的模式之间可以使用X | Y,类似逻辑或,代表该分支可以匹配 X 也可以匹配 Y,只要满足一个即可

    match表达式在执行时,将目标值coin按照顺序依次与每一个分支的模式相比较,如果模式匹配了这个值,那么模式之后的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支。每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 match 表达式的返回值。如果分支有多行代码,那么需要用 {} 包裹,同时最后一行代码需要是一个表达式。

    使用match表达式赋值

    #![allow(unused)]
    enum IpAddr { Ipv4,
       Ipv6
    }
    fn main() { let ip1 = IpAddr::Ipv6;
        let ip_str = match ip1 { IpAddr::Ipv4 => "127.0.0.1",
            _ => "::1",
        };
        println!("{}", ip_str);
    }
    

    通过match表达式,给ip_str赋值(绑定)了一个Ipv6的地址::1(环回地址)。

    模式匹配取出值

    模式匹配的另外一个重要功能是从模式中取出绑定的值。例如:

    enum Action { Say(String),
        MoveTo(i32, i32),
        ChangeColorRGB(u16, u16, u16),
    }
    fn main() { let actions = [
            Action::Say("Hello Rust".to_string()),
            Action::MoveTo(1,2),
            Action::ChangeColorRGB(255,255,0),
        ];
        for action in actions { match action { Action::Say(s) => { //当action匹配Action::Say的时候,s将会取出取出其中的值
                    println!("{}", s);
                },
                Action::MoveTo(x, y) => { // 也可以取出多个值
                    println!("point from (0, 0) move to ({}, {})", x, y);
                },
                Action::ChangeColorRGB(r, g, _) => { println!("change color into '(r:{}, g:{}, b:0)', 'b' has been ignored",
                        r, g,
                    );
                }
            }
        }
    }
    

    运行后输出如下所示:

    Hello Rust
    point from (0, 0) move to (1, 2)    
    change color into '(r:255, g:255, b:0)', 'b' has been ignored
    

    通配符(_)

    当我们不想在匹配时列出所有值的时候,可以使用 Rust 提供的一个特殊模式,例如,u8 可以拥有 0 到 255 的有效的值,但是我们只关心 1、3、5 和 7 这几个值,不想列出其它的 0、2、4、6、8、9 一直到 255 的值。例如:

    let some_u8_value = 0u8;
    match some_u8_value { 1 => println!("one"),
        3 => println!("three"),
        5 => println!("five"),
        7 => println!("seven"),
        _ => (),
    }
    

    通过将 _ 其放置于其他分支后,_ 将会匹配所有遗漏的值。() 表示返回单元类型与所有分支返回值的类型相同(因为println!宏返回()),所以当匹配到 _ 后,什么也不会发生。

    if let匹配

    在某些场景下,我们其实只关心某一个值是否存在,此时 match 就显得过于啰嗦。例如:

    let v = Some(3u8);
        match v { Some(3) => println!("three"),
            _ => (),
        }
    

    我们只想要对 Some(3) 模式进行匹配, 不想处理任何其他 Some 值或 None 值。但是为了满足 match 表达式(穷尽性)的要求,写代码时必须在处理完这唯一的成员后加上 _ => (),这样会增加不少无用的代码。为此,rust提供了更加简洁的if-let匹配,上面的例子可以改写为如下所示:

    if let Some(3) = v { println!("three");
    }
    

    matches!宏

    Rust 标准库中提供了一个非常实用的宏:matches!,它可以将一个表达式跟模式进行匹配,然后返回匹配的结果 true or false。例如:

    let foo = 'f';
    assert!(matches!(foo, 'A'..='Z' | 'a'..='z'));
    let bar = Some(4);
    assert!(matches!(bar, Some(x) if x > 2));
    

    解构Option

    之前在枚举类型中,遗留的一个问题是:“一个变量要么有值:Some(T), 要么为空:None”。当时没有取出Some中的值,现在有了模式匹配,我们来实现取出值。例如:

    let num = Some(123i32);
    let num1: i32;
    num1 = match num { Some(x) => x,
        None => 0,
    };
    println!("{}", num1);
    

    这个例子中,我们只是对数值类型做了取出操作,对其它类型也是类似的。例如:

    let s = Some("123".to_string());
    let s1:String;
    s1 = match s { Some(x) => x,
        None => " ".to_string(),
    };
    println!("{}", s1);
    

    只不过对于String这种存储在堆内存上的数据类型而言,这会导致所有权的转移,从而导致s在模式匹配之后无法使用。

    模式匹配无处不在

    在rust中,模式匹配无处不在。除了match,if-let之外。还有while let, for循环,let语句,函数参数等都是模式匹配。

    while let

    while let条件循环,它的作用是只要模式匹配,循环就能一直进行。下面是一个例子。

    // Vec是动态数组
    let mut stack = Vec::new();
    // 向数组尾部插入元素
    stack.push(1);
    stack.push(2);
    stack.push(3);
    // stack.pop从数组尾部弹出元素
    while let Some(top) = stack.pop() { println!("{}", top);
    }
    

    pop 方法取出动态数组的最后一个元素并返回 Some(value),如果动态数组是空的,将返回 None。如果返回了None,那么while循环将会结束。

    for循环

    let v = vec!['a', 'b', 'c'];
    for (index, value) in v.iter().enumerate() { println!("{} is at index {}", value, index);
    }
    

    像这样的for循环,本质上也是模式匹配。迭代器每次迭代会返回一个 (索引,值) 形式的元组,然后用 (index,value) 来匹配。

    let语句

    实际上let x = 3,这也是一种模式绑定,代表将匹配的值绑定到变量 x 上。更复杂一些的比如元组。let (x, y, z) = (1, 2, 3)。因此,在 Rust 中变量名也是一种模式。

    函数参数

    fn print_coordinates(&(x, y): &(i32, i32)) { println!("Current location: ({}, {})", x, y);
    }
    fn main() { let point = (3, 5);
        print_coordinates(&point);
    }
    

    在函数中,参数也是一种模式匹配。&(3, 5) 会匹配模式 &(x, y),因此 x 得到了 3,y 得到了 5。

    可驳模式和不可驳模式

    在rust中,模式匹配可以分为两类,一类是可驳模式,另一类是不可驳模式。像let, for, match这类属于不可驳模式匹配,它们要求必须完全覆盖匹配;而if-let, while-let这种属于可驳模式,它们允许忽略其余的模式。

    通过…=匹配值的范围

    let x = 3;
    match x { 1..=3 => println!("one through three"),
        4..8 => println!("four through seven")
        _ => println!("something else"),
    }
    

    ..= 语法允许你匹配一个闭区间序列内的值。序列只允许用于数字或字符类型,原因是:它们可以连续,同时编译器在编译期可以检查该序列是否为空,字符和数字值是 Rust 中仅有的可以用于判断是否为空的类型。

    let x = 'c';
    match x { 'a'..='j' => println!("early ASCII letter"),
        'k'..='z' => println!("late ASCII letter"),
        _ => println!("something else"),
    }
    

    Rust 知道 ‘c’ 位于第一个模式的序列内,所以会打印出 early ASCII letter。

    解构结构体

    struct Point { x: i32,
        y: i32,
    }
    fn main() { let p = Point { x: 0, y: 7 };
        let Point { x: a, y: b } = p;
        assert_eq!(0, a);
        assert_eq!(7, b);
    }
    

    这段代码创建了变量 a 和 b 来匹配结构体 p 中的 x 和 y 字段,这个例子展示了模式中的变量名不必与结构体中的字段名一致。不过通常希望变量名与字段名一致以便于理解变量来自于哪些字段。这时候只需要将let Point { x: x, y: y } = p;简写为let Point { x, y } = p;即可。

    也可以使用字面值作为结构体模式的一部分进行解构,而不是为所有的字段创建变量。这允许我们测试一些字段为特定值的同时创建其他字段的变量。

    fn main() { let p = Point { x: 0, y: 7 };
        match p { Point { x, y: 0 } => println!("On the x axis at {}", x),
            Point { x: 0, y } => println!("On the y axis at {}", y),
            Point { x, y } => println!("On neither axis: ({}, {})", x, y),
        }
    }
    

    在这个例子中,首先是 match 第一个分支,指定匹配 y 为 0 的 Point; 然后第二个分支在第一个分支之后,匹配 y 不为 0,x 为 0 的 Point; 最后一个分支匹配 x 不为 0,y 也不为 0 的 Point。

    解构枚举

    enum Message { Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }
    fn main() { let msg = Message::ChangeColor(0, 160, 255);
        match msg { Message::Quit => { println!("The Quit variant has no data to destructure.")
            }
            Message::Move { x, y } => { println!(
                    "Move in the x direction {} and in the y direction {}",
                    x,
                    y
                );
            }
            Message::Write(text) => println!("Text message: {}", text),
            Message::ChangeColor(r, g, b) => { println!(
                    "Change the color to red {}, green {}, and blue {}",
                    r,
                    g,
                    b
                )
            }
        }
    }
    

    模式匹配一样要类型相同,因此匹配 Message::Move{1,2} 这样的枚举值,就必须要用 Message::Move{x,y} 这样的同类型模式才行。

    这段代码会打印出 Change the color to red 0, green 160, and blue 255。尝试改变 msg 的值来观察其他分支代码的运行。

    对于像 Message::Quit 这样没有任何数据的枚举成员,不能进一步解构其值。只能匹配其字面值 Message::Quit,因此模式中没有任何变量。

    对于另外两个枚举成员,就用相同类型的模式去匹配出对应的值即可。

    解构嵌套的结构体和枚举

    #![allow(unused)]
    enum Color { Rgb(i32, i32, i32),
       Hsv(i32, i32, i32),
    }
    enum Message { Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(Color),
    }
    fn main() { let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
        match msg { Message::ChangeColor(Color::Rgb(r, g, b)) => { println!(
                    "Change the color to red {}, green {}, and blue {}",
                    r,
                    g,
                    b
                )
            }
            Message::ChangeColor(Color::Hsv(h, s, v)) => { println!(
                    "Change the color to hue {}, saturation {}, and value {}",
                    h,
                    s,
                    v
                )
            }
            _ => ()
        }
    }
    

    模式匹配非常强大,像这样嵌套的结构体和枚举,它也能进行解构,取出嵌套在其中的值。下面是一个更复杂的例子:

    #![allow(unused)]
    fn main() {struct Point { x: i32,
         y: i32,
     }
    let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });
    }
    

    结构体和元组嵌套在元组中,rust依旧可以将这种复杂类型分解匹配,从而让我们取出感兴趣的值。

    解构数组

    对于数组,我们可以用类似元组的方式解构,分为两种情况:

    • 定长数组
      let arr: [u16; 2] = [114, 514];
      let [x, y] = arr;
      assert_eq!(x, 114);
      assert_eq!(y, 514);
      
      • 不定长数组
        let arr: &[u16] = &[114, 514];
        if let [x, ..] = arr { assert_eq!(x, &114);
        }
        if let &[.., y] = arr { assert_eq!(y, 514);
        }
        let arr: &[u16] = &[];
        assert!(matches!(arr, [..]));
        assert!(!matches!(arr, [x, ..]));
        

        ..是用来忽略剩余值的,后续会介绍。

        忽略模式中的值

        有时忽略模式中的一些值是很有用的,比如在 match 中的最后一个分支使用 _ 模式匹配所有剩余的值。 你也可以在另一个模式中使用 _ 模式,使用一个以下划线开始的名称,或者使用 … 忽略所剩部分的值。

        使用 _ 忽略整个值

        虽然 _ 模式作为 match 表达式最后的分支特别有用,但是它的作用还不限于此。例如可以将其用于函数参数中:

        fn foo(_: i32, y: i32) { println!("This code only uses the y parameter: {}", y);
        }
        fn main() { foo(3, 4);
        }
        

        此时编译器就不会警告说存在未使用的函数参数,就跟使用命名参数一样。

        使用嵌套的 _ 忽略部分值

        let mut setting_value = Some(5);
        let new_setting_value = Some(10);
        match (setting_value, new_setting_value) { (Some(_), Some(_)) => { println!("Can't overwrite an existing customized value");
            }
            _ => { setting_value = new_setting_value;
            }
        }
        println!("setting is {:?}", setting_value);
        

        第一个匹配分支,我们不关心里面的值,只关心元组中两个元素的类型,因此对于 Some 中的值,直接进行忽略。 剩下的形如 (Some(),None),(None, Some()), (None,None) 形式,都由第二个分支 _ 进行分配。还可以在一个模式中的多处使用下划线来忽略特定值,如下所示,这里忽略了一个五元元组中的第二和第四个值:

        let numbers = (2, 4, 8, 16, 32);
        match numbers { (first, _, third, _, fifth) => { println!("Some numbers: {}, {}, {}", first, third, fifth)
            },
        }
        

        用 … 忽略剩余值

        前文的不定长数组的模式中出现了..,用来忽略除开头以外的值,或者是除结尾以外的值。当然了,..也可以忽略中间的某些值。例如:

        fn main() { let numbers = (2, 4, 8, 16, 32);
            match numbers { (first, .., last) => { println!("Some numbers: {}, {}", first, last);
                },
            }
        }
        

        这里用 first 和 last 来匹配第一个和最后一个值。… 将匹配并忽略中间的所有值。然而使用 … 必须是无歧义的。如果期望匹配和忽略的值是不明确的,Rust 会报错。下面代码展示了一个带有歧义的 … 例子,因此不能编译:

        fn main() { let numbers = (2, 4, 8, 16, 32);
            match numbers { (.., second, ..) => { println!("Some numbers: {}", second)
                },
            }
        }
        

        Rust 无法判断,second 应该匹配 numbers 中的第几个元素,因此无法通过编译。

        匹配守卫

        匹配守卫(match guard)是一个位于 match 分支模式之后的额外 if 条件,它能为分支模式提供更进一步的匹配条件。

        这个条件可以使用模式中创建的变量:

        let num = Some(4);
        match num { Some(x) if x < 5 => println!("less than five: {}", x),
            Some(x) => println!("{}", x),
            None => (),
        }
        

        这个例子会打印出 less than five: 4。当 num 与模式中第一个分支匹配时,Some(4) 可以与 Some(x) 匹配,接着匹配守卫检查 x 值是否小于 5,因为 4 小于 5,所以第一个分支被选择。模式中无法提供类如 if x < 5 的表达能力,我们可以通过匹配守卫的方式来实现。

        match 表达式的模式中新建了一个变量而不是使用 match 之外的同名变量。内部变量覆盖了外部变量,意味着此时不能够使用外部变量的值,下面代码展示了如何使用匹配守卫修复这个问题。

        fn main() { let x = Some(5);
            let y = 10;
            match x { Some(50) => println!("Got 50"),
                Some(n) if n == y => println!("Matched, n = {}", n),
                _ => println!("Default case, x = {:?}", x),
            }
            println!("at the end: x = {:?}, y = {}", x, y);
        }
        

        匹配守卫 if n == y 并不是一个模式所以没有引入新变量。这个 y 正是 外部的 y 而不是新的覆盖变量 y,这样就可以通过比较 n 和 y 来表达寻找一个与外部 y 相同的值的概念了。

        也可以在匹配守卫中使用 或 运算符 | 来指定多个模式,同时匹配守卫的条件会作用于所有的模式。下面代码展示了匹配守卫与 | 的优先级。这个例子中看起来好像 if y 只作用于 6,但实际上匹配守卫 if y 作用于 4、5 和 6 ,在满足 x 属于 4 | 5 | 6 后才会判断 y 是否为 true:

        let x = 4;
        let y = false;
        match x { 4 | 5 | 6 if y => println!("yes"),
            _ => println!("no"),
        }
        

        这个匹配条件表明此分支只匹配 x 值为 4、5 或 6 同时 y 为 true 的情况。

        @绑定

        @允许为一个字段绑定另外一个变量。下面例子中,我们希望测试 Message::Hello 的 id 字段是否位于 3…=7 范围内,同时也希望能将其值绑定到 id_variable 变量中以便此分支中相关的代码可以使用它。

        enum Message { Hello { id: i32 },
        }
        let msg = Message::Hello { id: 5 };
        match msg { Message::Hello { id: id_variable @ 3..=7 } => { println!("Found an id in range: {}", id_variable)
            },
            Message::Hello { id: 10..=12 } => { println!("Found an id in another range")
            },
            Message::Hello { id } => { println!("Found some other id: {}", id)
            },
        }
        

        上例会打印出 Found an id in range: 5。通过在 3…=7 之前指定 id_variable @,我们捕获了任何匹配此范围的值并同时将该值绑定到变量 id_variable 上。

        使用 @ 还可以在绑定新变量的同时,对目标进行解构:

        #[derive(Debug)]
        struct Point { x: i32,
            y: i32,
        }
        fn main() { // 绑定新变量 `p`,同时对 `Point` 进行解构
            let p @ Point {x: px, y: py } = Point {x: 10, y: 23};
            println!("x: {}, y: {}", px, py);
            println!("{:?}", p);
            let point = Point {x: 10, y: 5};
            if let p @ Point {x: 10, y} = point { println!("x is 10 and y is {} in {:?}", y, p);
            } else { println!("x was not 10 :(");
            }
        }
        

        @的新特性(rust1.53新增)

        fn main() { match 1 { num @ (1 | 2) => { println!("{}", num);
                }
                _ => {}
            }
        }
        

        参考资料

        1. Rust语言圣经
        2. Rust程序设计语言