枚举
枚举就是允许我们列举所有可能的值来定义一个类型(概念上与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;
}
从图中可以看出,第一行定义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"), // 要放在最后面,且如果不加则会报错
}
}