Rust 从 0 到 1 (流程控制,匹配模式,方法)

流程控制

在所有的编程语言都有 if else 流程控制语句,如果没有请告诉我!

if else

Rust 中的 if else 语法如下:

1
2
3
4
5
6
let condition = true;
if condition {
// run A
} else {
// run B
}

读作:若 condition 的值为 true,则执行 A 代码,否则执行 B 代码。

在 Rust 中 if else 还有一种用法是:

1
2
3
4
5
6
7
8
let condition = true;
let number = if condition {
5
} else {
6
};

println!("The value of number is: {}", number);

这里的代码解释为:如果 condition 为真,那么 number 的值为 5 否做为 6。这种用法需要注意的是:返回类型要一致,如果条件不一致就不能通过编译。

else if

if else 可以和 else if 组合实现复杂的条件分支判断,如:

1
2
3
4
5
6
7
8
9
10
11
let n = 6;

if n % 4 == 0 {
println!("number is divisible by 4");
} else if n % 3 == 0 {
println!("number is divisible by 3");
} else if n % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}

有一点要注意,就算有多个分支能匹配,也只有第一个匹配的分支会被执行!

循环控制

在 Rust 语言中有三种循环方式:for、while 和 loop,其中 for 循环比较重要。

for 循环

Rust 中的 for 循环的书写格式:

1
2
3
for 元素 in 集合 {
// do something
}

for 循环使用的是集合的引用,集合的所有权会被转移(move) 到 for 语句块中,后续就没有办法在使用该集合了,但是对于实现了 copy 特质的数组,则不存在所有权转移的问题。
我们也可以使用 mut 关键字修改元素:

1
2
3
for item in &mut collection {
// do something
}

for 循环的具体使用方式总结如下:

使用方法 等价使用方式 所有权
for item in collection for item in IntoIterator::into_iter(collection) 转移所有权
for item in &collection for item in collection.iter() 不可变借用
for item in &mut collection for item in collection.iter_mut() 可变借用

如果你只想单独的循环执行某些事情,用不到元素可以使用如下方法:

1
2
3
for _ in 0..10 {
// ...
}

在 Rust 中 _ 的含义是忽略该值或者类型的意思

continue

使用 continue 可以跳过当前当次的循环,开始下次的循环,如:

1
2
3
4
5
6
for i in 1..4 {
if i == 2 {
continue;
}
println!("{}", i);
}

break

使用 break 可以直接跳出当前整个循环,如:

1
2
3
4
5
6
for i in 1..4 {
if i == 2 {
break;
}
println!("{}", i);
}

while 循环

使用一个条件来循环,当该条件为 true 时,继续循环,条件为 false,跳出循环,如:

1
2
3
4
5
6
7
8
let mut n = 0;

while n <= 5 {
println!("{}!", n);

n = n + 1;
}
println!("i jump!")

当 n 小于等于 5 时,会执行 n = n + 1,当 n 大于 5 时,则会跳出循环。

loop 循环

loop 是一个简单的无限循环,需要在内部设置条件,当条件满足时就跳出循环,如果内部逻辑出现错误,你将得到一个死循环。如:

1
2
3
loop {
println!("again!");
}

正确的用法是,loop 与 break 配合使用,如

1
2
3
4
5
6
7
8
9
10
11
let mut counter = 0;

let result = loop {
counter += 1;

if counter == 10 {
break counter * 2;
}
};

println!("The result is {}", result);

解释:当 counter 递增到 10 时,就会通过 break 返回一个 counter * 2 的值,最后赋值给 reuslt 并打印出来。

这里需要注意的是:

  • break 可以单独使用,也可以带一个返回值,有些类似 return
  • loop 是一个表达式,因此可以返回一个值

匹配模式

match 匹配

在 Rust 中,匹配模式最常用的就是 match 和 if let,首先 match 的通用形式:

1
2
3
4
5
6
7
8
9
match target {
模式1 => 表达式1,
模式2 => {
语句1;
语句2;
表达式2
},
_ => 表达式3
}

看来一个 match 的例子,对于经常挖 src 的小伙伴而言,我们在提交漏洞后审核会给我们的漏洞评级,那么来看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Vul {
Rce,
Sqli,
Xss,
Unauth,
}

fn main() {
let vul = Vul::Rce;
match vul {
Vul::Rce => println!("高危"),
Vul::Sqli | Vul::Unauth => println!("低位"),
_ => println!("忽略!")
}
}

这里我们想去匹配 vul 对应的枚举类型,因此使用三个 match 分支来覆盖漏洞的等级,需要注意的是:

  • match 的匹配必须要穷举出所有可能,因此这里用 _ 来代表未列出的所有可能性
  • match 的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同
  • X | Y,类似逻辑运算符 或,代表该分支可以匹配 X 也可以匹配 Y,只要满足一个即可

match 跟其他语言中的 switch 非常像,_ 类似于 switch 中的 default。

使用 match 赋值

mathc 本身是一个表达式,因此它可以用来赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}

模式绑定

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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) => {
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,
);
}
}
}
}

上面的代码中,在匹配 Action::Say(String) 模式时,我们把内部的存储的值绑定到了 s 变量上,因此 s 变量对应的就是 String 类型。

穷尽匹配

match 的匹配必须穷尽所有情况,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Vul {
Rce,
Sqli,
Xss,
Unauth,
}

fn main() {
let vul = Vul::Rce;
match vul {
Vul::Rce => println!("高危"),
Vul::Sqli | Vul::Unauth => println!("低位"),
}
}

在这里我们没有处理 Xss 的情况,因此在编译时就会报错:

_ 通配符

当我们不想在匹配时列出所有值的时候,可以使用 Rust 提供的一个特殊模式,我们可以使用 _ 代替未列出的值,如:

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

_ 放在最后是为让 _ 匹配所有遗漏的值。当然你不想用 _ 通配符也是可以的,可以使用变量来接收,如:

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

if let 匹配

在某些场景下只关心一个值是否存在,如果用 match 就要写成这样子:

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

比如只想对 Some(3) 模式进行匹配,不想处理其他的值,由于 match 表达式穷尽性的要求,需要加上 _ = (),这样会增加一些无用的代码。好在 Rust 为了我们提供了简便的方法 if let:

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

那么如何选择:当你只要匹配一个条件,且忽略其他条件时就用 if let ,否则都用 match。

matches!宏

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

1
2
3
4
5
6
7
8
enum MyEnum {
Foo,
Bar
}

fn main() {
let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
}

现在需要对 v 进行过滤,只保留 MyEnum::Foo 的元素,使用 matches!的写法:

1
v.iter().filter(|x| matches!(x, MyEnum::Foo));

变量遮蔽

无论是 match 还是 if let,这里都是一个新的代码块,而且这里的绑定相当于新变量,如果你使用同名变量,会发生变量遮蔽,因此我们需要注意。

解构 Option

在 Rust 中使用 Option 枚举来解决变量中是否有值的问题,Option 枚举的定义如下:

1
2
3
4
enum Option<T> {
Some(T),
None,
}

简单解释就是:一个变量要么有值:Some(T), 要么为空:None。

匹配 Option

首先来看代码:

1
2
3
4
5
6
7
8
9
10
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);

plus_one 接受一个 Option 类型的参数,同时返回一个 Option 类型的值,如果传入的是一个 None ,则返回一个 None 且不做任何处理;如果传入的是一个 Some(i32),则通过模式绑定,把其中的值绑定到变量 i 上,然后返回 i+1 的值,同时用 Some 进行包裹。

模式使用场景

模式是 Rust 中的特殊语法,它用来匹配类型中的结构和数据,它往往和 match 表达式联用,以实现强大的模式匹配能力。模式一般由以下内容组合而成:

  • 字面值
  • 解构的数组、枚举、结构体或者元组
  • 变量
  • 通配符
  • 占位符

所有可能用到模式的地方

  • match 分支
  • if let 分支
  • while let 条件循环
  • for 循环
  • let 语句
  • 函数参数
  • let 和 if let

完整的列表可以查看 全模式列表

方法 Method

Rust 使用 impl 来定义方法:

1
2
3
4
5
impl 结构体{
fn 方法名(&self,参数列表) 返回值 {
//方法体
}
}

例如现在需要计算一个矩形的面积,则有一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}

fn main() {
let rect1 = Rectangle { width: 30, height: 50 };

println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}

首先定义了一个 Rectangle 结构体,并且在其上定义了一个 area 方法,用于计算该矩形的面积。

impl Rectangle {} 表示为 Rectangle 实现方法(impl 是实现 implementation 的缩写),这样的写法表明 impl 语句块中的一切都是跟 Rectangle 相关联的。

self、&self 和 &mut self

在 area 的签名中,我们使用 &self 替代 rectangle: &Rectangle,&self 其实是 self: &Self 的简写(注意大小写)。在一个 impl 块内,Self 指代被实现方法的结构体类型,self 指代此类型的实例,换句话说,self 指代的是 Rectangle 结构体实例,这样的写法会让我们的代码简洁很多。需要注意的是, self 依然有所有权的概念:

  • self 表示 Rectangle 的所有权转移到该方法中,这种形式用的较少
  • &self 表示该方法对 Rectangle 的不可变借用
  • &mut self 表示可变借用

self 的使用就跟函数参数一样,要严格遵守 Rust 的所有权规则。

使用方法代替函数有以下好处:

  • 不用在函数签名中重复书写 self 对应的类型
  • 代码的组织性和内聚性更强,对于代码维护和阅读来说,好处巨大

方法名跟结构体字段名相同

在 Rust 中,允许方法名跟结构体的字段名相同,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

if rect1.width() {
println!("The rectangle has a nonzero width; it is {}", rect1.width);
}
}

使用 rect1.width() 时,Rust 知道我们调用的是它的方法,如果使用 rect1.width,则是访问它的字段。

带有多个参数的方法

方法和函数一样,可以使用多个参数,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}

fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}

fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 10, height: 40 };
let rect3 = Rectangle { width: 60, height: 45 };

println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

关联函数

定义在 impl 中且没有 self 的函数被称之为关联函数: 因为它没有 self,不能用 f.read() 的形式调用,因此它是一个函数而不是方法,它又在 impl 中,与结构体紧密关联,因此称为关联函数。

Rust 中有一个约定俗成的规则,使用 new 来作为构造器的名称,出于设计上的考虑,Rust 特地没有用 new 作为关键字。

为之前的 Rectangle 创建一个关联函数,如:

1
2
3
4
5
6
7
8
9
impl Rectangle {
fn new(w: u32, h: u32) -> Rectangle {
Rectangle { width: w, height: h }
}
}

fn main() {
let rectangle = Rectangle::new(3, 5);
}

因为是函数,所以不能用 . 的方式来调用,我们需要用 :: 来调用,例如 let sq = Rectangle::new(3, 3);。这个方法位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间。

多个 impl 定义

Rust 允许我们为一个结构体定义多个 impl 块,目的是提供更多的灵活性和代码组织性, 如:

1
2
3
4
5
6
7
8
9
10
11
12
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}

impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}

为枚举实现方法

我们也可以为枚举实现方法,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#![allow(unused)]
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

impl Message {
fn call(&self) {
// 在这里定义方法体
}
}

fn main() {
let m = Message::Write(String::from("hello"));
m.call();
}

Ref

Rust语言圣经(Rust Course)