21人参与 • 2025-06-26 • rust
在本文中,首先,我们将定义和使用枚举。接下来,我们将探讨一个特别有用的枚举,称为 option。然后,我们将了解 match 表达式中的模式匹配。最后,我们将介绍 if let 构造。
结构体提供了一种将相关字段和数据分组在一起的方法,而枚举则提供了一种说明一个值是一组可能值中的一个的方法。
任何 ip 地址都可以是 ipv4 或者 ipv6,但不能同时是这两个地址。ip 地址的这个属性使得枚举数据结构非常合适,因为枚举值只能是它的一个变体。
定义 ipaddrkind 枚举:
enum ipaddrkind { v4, v6, }
ipaddrkind 是一个自定义数据类型。
我们可以像这样创建 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);
枚举可以作为结构体的字段:
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"), };
但是,仅使用枚举表示相同的概念更为简洁:我们可以将数据直接放入每个枚举变体中,而不是在结构体中使用枚举。这个枚举的新定义表明,v4 和 v6 的实例将具有相关的 string 值:
enum ipaddr { v4(string), v6(string), } let home = ipaddr::v4(string::from("127.0.0.1")); let loopback = ipaddr::v6(string::from("::1"));
我们直接将数据附加到枚举的每个变体上,因此不需要额外的结构体。我们定义的每个枚举变体的名称也成为构造枚举实例的函数。也就是说,ipaddr::v4() 是一个函数调用,它接受一个 string 参数并返回一个 ipaddr 类型的实例。
使用 enum 而不是 struct 还有另一个好处:每个变量可以有不同的关联数据类型和数量。如果我们想要将 v4 地址存储为四个 u8 值,但仍然将 v6 地址表示为一个 string 值,那么我们将无法使用结构体。枚举可以轻松处理这种情况:
enum ipaddr { v4(u8, u8, u8, u8), v6(string), } let home = ipaddr::v4(127, 0, 0, 1); let loopback = ipaddr::v6(string::from("::1"));
让我们来看看标准库是如何定义 ipaddr 的:两个不同结构体的形式将地址数据嵌入到变量中,每个变量的定义不同。
struct ipv4addr { // --snip-- } struct ipv6addr { // --snip-- } enum ipaddr { v4(ipv4addr), v6(ipv6addr), }
注意,即使标准库包含了 ipaddr 的定义,我们仍然可以创建和使用我们自己的定义而不会产生冲突,因为我们没有将标准库的定义引入我们的作用域。
我们也可以使用 impl 在枚举上定义方法:
impl message { fn call(&self) { // method body would be defined here } } let m = message::write(string::from("hello")); m.call();
方法的主体会使用 self 来获取我们调用该方法的值。在这个例子中,我们创建了一个变量 m,它的值是 message::write(string::from(“hello”)),这就是 m.call() 运行时调用方法体中的 self 的值。
option 是标准库定义的另一个枚举。option 类型编码了一种非常常见的场景,在这种场景中,值可以是什么东西,也可以是空(什么都没有)。
rust没有许多其他语言所具有的 null 特性。null 是一个表示没有值的值。在带有 null 的语言中,变量总是处于两种状态之一:null 或非 null。
空值的问题是,如果尝试将空值用作非空值,将得到某种错误。然而,null 试图表达的概念仍然是有用的:null 是由于某种原因当前无效或不存在的值。
问题不在于概念,而在于具体的实现。因此,rust 没有空值,但它有一个枚举,可以编码值存在或不存在的概念。该 enum 为 option<t>,由标准库定义如下:
enum option<t> { none, some(t), }
可以直接使用 some 和 none,而不使用 option:: 前缀。some(t) 和 none 是 option<t> 类型的变体。
<t> 语法是 rust 的一个我们还没有讨论的特性。它是一个泛型类型参数,意味着 option 枚举的某些变体可以保存任何类型的数据。
示例:
let some_number = some(5); let some_char = some('e'); let absent_number: option<i32> = none;
some_number 的类型为 option<i32>,some_char 的类型是 option<char>。对于 none,rust 要求必须提供具体的 option 类型。
当我们有一个 none 值时,在某种意义上它和 null 的意思是一样的:我们没有一个有效值。那么为什么 option<t> 比 null 好呢?因为 option<t> 和 t (t 可以是任何类型)是不同的类型。
例如,这段代码无法编译,因为它试图将 i8 添加到 option<i8>:
let x: i8 = 5; let y: option<i8> = some(5); let sum = x + y;
rust 不理解如何添加 i8 和 option<i8>,因为它们是不同的类型。
须先将 option<t> 转换为 t,然后才能对其执行 t 操作。一般来说,这有助于抓住 null 最常见的问题之一:假设某些东西不是空的,而实际上是空的。为了拥有一个可能为空的值,必须显式地将该值的类型设置为 option<t>。然后,当使用该值时,需要显式地处理该值为空的情况。只要值的类型不是 option<t>,就可以放心地假设该值不为空。
这是 rust 经过深思熟虑的设计决策,目的是限制 null 的普遍性,提高 rust 代码的安全性。
match 表达式是一个控制流结构,当与枚举一起使用时,它就是这样做的:它将运行不同的代码,这取决于它拥有的枚举的哪个变体,并且该代码可以使用匹配值中的数据。
我们可以编写一个函数,它接受一枚未知的美国硬币,并以与计数机类似的方式确定它是哪一枚硬币,并返回其以美分为单位的值:
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, } }
当匹配表达式执行时,它按顺序将结果值与每个模式进行比较。如果模式匹配该值,则执行与该模式关联的代码。如果该模式与值不匹配,则继续执行。
match 的的另一个有用特性是:它们可以绑定到与模式匹配的值部分。这就是从枚举变量中提取值的方法。
作为一个例子,让我们修改一个枚举变量,使其包含数据。
#[derive(debug)] // so we can inspect the state in a minute enum usstate { alabama, alaska, // --snip-- } enum coin { penny, nickel, dime, quarter(usstate), }
在这段代码的匹配表达式中,我们将一个名为 state 的变量添加到匹配变量 coin::quarter 值的模式中。当一个 coin::quarter 匹配时,状态变量将绑定到该 quarter 的状态值。然后我们可以在代码中使用 state,如下所示:
fn value_in_cents(coin: coin) -> u8 { match coin { coin::penny => 1, coin::nickel => 5, coin::dime => 10, coin::quarter(state) => { println!("state quarter from {state:?}!"); 25 } } }
如果我们调用 value_in_cents(coin::quarter(usstate::alaska)),coin 将是 coin::quarter(usstate::alaska)。当我们将该值与每个匹配进行比较时,在到达 coin::quarter(state) 之前,它们都不匹配。此时,州的绑定将是值 usstate::alaska。然后我们可以使用 println! 打印该值。
我们还可以使用 match 来处理 option<t>。
编写一个函数,它接受 option<i32>,如果里面有一个值,则将该值加 1。如果里面没有值,函数应该返回 none 值,并且不尝试执行任何操作。
fn plus_one(x: option<i32>) -> option<i32> { match x { none => none, some(i) => some(i + 1), } } let five = some(5); let six = plus_one(five); let none = plus_one(none);
match 中的模式必须涵盖所有可能性。
考虑一下这个版本的 plus_one 函数:
fn plus_one(x: option<i32>) -> option<i32> { match x { some(i) => some(i + 1), } }
报错:error[e0004]: non-exhaustive patterns: `none` not covered。
我们没有处理 none 的情况,所以无法编译。
rust 中的匹配是详尽的:为了使代码有效,我们必须穷尽每一种可能性。特别是在 option<t> 的情况下,当 rust 防止我们忘记显式处理 none 情况时,它保护我们避免在可能为 null 的情况下假设我们有一个值。
使用枚举,我们还可以对一些特定的值采取特殊的操作,但对所有其他值采取默认操作。
let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), other => move_player(other), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn move_player(num_spaces: u8) {}
最后一个模式 other 将匹配所有没有明确列出的值,other 必须放在最后。
当我们想要捕获所有值,但又不想在捕获所有值的模式中使用值时可以使用 _
。这是一个特殊的模式,它匹配任何值,并且不绑定到该值。这告诉 rust 我们不打算使用这个值,所以 rust 不会警告我们一个未使用的变量。
let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => reroll(), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn reroll() {}
这个例子也满足穷竭性要求,因为我们显式地忽略了所有的其他值。
还可以有另外一种写法:
let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => (), } fn add_fancy_hat() {} fn remove_fancy_hat() {}
使用 () 作为与 _
匹配时的动作。这将告诉 rust:不使用任何不匹配先前模式的值,并且不想在这种情况下运行任何代码。
if let 语法以一种更简洁的方式来处理匹配一个模式的值,同时忽略其他模式。
let config_max = some(3u8); match config_max { some(max) => println!("the maximum is configured to be {max}"), _ => (), }
如果值是 some,我们通过将值绑定到模式中的变量 max 来打印出 some 变量中的值。不对 none 值做任何事情。
每次都要写 _ => () 确实很烦,可以用 if let 语法进行简化:
let config_max = some(3u8); if let some(max) = config_max { println!("the maximum is configured to be {max}"); }
if let 的语法接受一个模式和一个用等号分隔的表达式。它的工作方式与匹配相同,将表达式提供给匹配,而模式是它的第一个臂。在本例中,模式是 some(max),并且 max 绑定到 some 内部的值。if let 块中的代码仅在值与模式匹配时运行。
使用 if let 意味着更少的输入、更少的缩进和更少的样板代码。但是,失去了 match 强制执行的详尽检查。
换句话说,可以将 if let 视为匹配的语法糖,当值匹配一个模式时运行代码,然后忽略所有其他值。
我们可以在 if 语句中包含 else 语句,该代码块相当于 if let 和 else。
match 写法:
let mut count = 0; match coin { coin::quarter(state) => println!("state quarter from {state:?}!"), _ => count += 1, }
if let…else 写法:
let mut count = 0; if let coin::quarter(state) = coin { println!("state quarter from {state:?}!"); } else { count += 1; }
let else 语法在左侧接受一个模式,在右侧接受一个表达式(变量),这与 if let 非常相似,但它没有 if 分支,只有 else 分支。如果模式匹配,它将在外部作用域中绑定来自模式的值。如果模式不匹配,程序将进入 else。
一种常见的模式是当值存在时执行一些计算,否则返回默认值。
fn describe_state_quarter(coin: coin) -> option<string> { if let coin::quarter(state) = coin { if state.existed_in(1900) { some(format!("{state:?} is pretty old, for america!")) } else { some(format!("{state:?} is relatively new.")) } } else { none } }
我们还可以利用表达式生成的值来从 if let 中生成状态或提前返回。
用 if let 来写:
fn describe_state_quarter(coin: coin) -> option<string> { let state = if let coin::quarter(state) = coin { state } else { return none; }; if state.existed_in(1900) { some(format!("{state:?} is pretty old, for america!")) } else { some(format!("{state:?} is relatively new.")) } }
用 let else 来写,会更简单:
fn describe_state_quarter(coin: coin) -> option<string> { let coin::quarter(state) = coin else { return none; }; if state.existed_in(1900) { some(format!("{state:?} is pretty old, for america!")) } else { some(format!("{state:?} is relatively new.")) } }
到此这篇关于rust中枚举与模式匹配的使用的文章就介绍到这了,更多相关rust 枚举与模式匹配内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论