10 KiB
流程控制
80后应该都对学校的小混混记忆犹新,在那个时代,小混混们往往都认为自己是地下王者,管控着地下事务的流程,在我看来,他们就像代码中的流程控制一样,无处不在,很显眼,但是又让人懒得重视。
言归正传,Rust程序是从上而下顺序执行的,在此过程中,我们可以引入循环、分支等流程控制方式,帮助我们的代码更好的实现相应的功能。
使用if来做分支控制
if else无处不在 -
鲁迅说
但凡你能找到一门编程语言没有if else
,那么一定更要反馈给鲁迅,反正不是我说的:) 总之,只要你拥有其它语言的编程经验,就一定会有以下认知:if else
表达式根据条件执行不同的代码分支:
if condition == true {
// A...
} else {
// B...
}
该代码读作:若condition
条件为true
,则执行A
代码,否则执行B
代码.
先看下面代码:
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
println!("The value of number is: {}", number);
}
以上代码有以下几点要注意:
if
语句块是表达式, 这里我们使用if
表达式的返回值来给number
进行赋值:number
的值是5
。- 用
if
来赋值时,要保证每个分支返回的类型一样(事实上,这种说法不完全准确,见这里),此处返回的5
和6
就是同一个类型,如果返回类型不一致就会报错
error[E0308]: if and else have incompatible types
--> src/main.rs:4:18
|
4 | let number = if condition {
| __________________^
5 | | 5
6 | | } else {
7 | | "six"
8 | | };
| |_____^ expected integer, found &str // 期望整数类型,但却发现&str字符串切片
|
= note: expected type `{integer}`
found type `&str`
使用else if来处理多重条件
可以将else if
与if
、else
组合在一起实现多种条件分支判断:
fn main() {
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");
}
}
程序执行时,会按照自上至下的顺序执行每一个分支判断,一旦成功,则跳出if
语句块,最终本程序会匹配执行else if n % 3 == 0
的分支,输出"number is divisible by 3"
。
有一点要注意,就算有多个分支能匹配,也只有第一个匹配的分支会被执行!
如果代码中有大量的else if
会让代码变得极其丑陋,不过不用担心,下一章的match
专门用以解决多分支模式匹配的问题。
循环控制
循环无处不在,上到数钱,下到数年,你能想象的很多场景都存在循环,因此它也是流程控制中最重要的组成部分之一.
在Rust语言中有三种循环方式:for
、while
和loop
,其中for
循环是Rust循环王冠上的明珠。
for循环
for
循环是Rust的大杀器:
fn main() {
for i in 1..=5 {
println!("{}",i);
}
}
以上代码循环输出一个从1到5的序列,简单粗暴,核心就在于for
和in
的联动,语义表达如下:
for 元素 in 集合 {
// 使用元素干一些你懂我不懂的事情
}
这个语法跟javascript
还蛮像,应该挺好理解。
注意,使用for
我们往往使用集合的引用形式,除非你不想在后面的代码中继续使用该集合(不使用引用的话,所有权会被转移到for
语句块中):
for item in &container {
// ...
}
如果想在循环中,修改该元素,使用mut
关键字:
for item in &mut collection {
// ...
}
总结如下:
使用方法 | 等价使用方式 | 所有权 |
---|---|---|
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() |
可变借用 |
如果想在循环中获取元素的索引:
fn main() {
let a = [4,3,2,1];
// `.iter()`方法把`a`数组变成一个迭代器
for (i,v) in a.iter().enumerate() {
println!("第{}个元素是{}",i+1,v);
}
}
有同学可能会想到,如果我们要用for
循环控制某个过程执行10次,但是又不关心那个计数值,该怎么写?
for _ in 0..10 {
// ...
}
可以用_
来替代i
用于for
循环中,在Rust中_
的含义是忽略该值或者类型的意思,如果不使用_
,那么编译器会给你一个变量未使用的
的警告.
两种循环方式优劣对比
以下代码,使用了两种循环方式:
// 第一种
let collection = [1, 2, 3, 4, 5];
for i in 0..collection.len() {
let item = collection[i];
// ...
}
// 第二种
for item in collection {
}
第一种方式是循环索引,然后通过索引下标去访问集合,第二种方式是直接循环集合中的元素,优劣如下:
- 性能:第一种使用方式中
collection[index]
的索引访问,会因为边界检查(bounds checking)导致运行时的性能损耗 - Rust会检查并确认index
是落在集合内,但是第二种直接迭代的方式就不会触发这种检查,因为编译器会在编译时就完成分析并证明这种访问是合法的 - 安全: 第一种方式里对
collection
的索引访问是非连续的,存在一定可能性在两次访问之间,collection
发生了变化,导致脏数据产生。而第二种直接迭代的方式是连续访问,因此不存在这种风险
由于for循环无需任何条件限制,也不需要通过索引来访问,因此是最安全也是最常用的,在下面的while
中,我们能看到为什么for
会更加安全。
continue
使用continue
可以跳过当前当次的循环,开始下次的循环:
for i in 1..4 {
if i == 2 {
continue;
}
println!("{}",i);
}
上面代码对1到3的序列进行迭代,且跳过值为2时的循环,输出如下:
1
3
while循环
如果你需要一个条件来循环,当该条件为true
时,继续循环,条件为false
,跳出循环,那么while
就非常适用:
fn main() {
let mut n = 0;
while n <= 5 {
println!("{}!", n);
n = n + 1;
}
println!("我出来了!");
}
该while
循环,只有当n
小于等于5
时,才执行,否则就立刻跳出循环,因此在上述代码中,它会先从0
开始,满足条件,进行循环,然后是1
,满足条件,进行循环,最终到6
的时候,不满足条件,跳出while
循环,执行我出来了
的打印,然后程序结束:
0!
1!
2!
3!
4!
5!
我出来了!
当然,你也可以用其它方式组合实现,例如loop
(无条件循环,将在下面介绍) + if
+ break
:
fn main() {
let mut n = 0;
loop {
if n > 5 {
break
}
println!("{}",n);
n+=1;
}
println!("我出来了!");
}
可以看出,在这种循环场景下,while
要简洁的多。
while vs for
我们也能用while
来实现for
的功能:
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index = index + 1;
}
}
这里,代码对数组中的元素进行计数。它从索引 0
开始,并接着循环直到遇到数组的最后一个索引(这时,index < 5
不再为真)。运行这段代码会打印出数组中的每一个元素:
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50
数组中的所有五个元素都如期被打印出来。尽管 index 在某一时刻会到达值 5,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。
但这个过程很容易出错;如果索引长度不正确会导致程序 panic。这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。
for
循环代码如下:
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
}
可以看出,for
并不会使用索引去访问数组,因此更安全也更简洁,同时避免运行时的边界检查
,性能更高。
loop循环
对于循环而言,loop
循环毋庸置疑,是适用面最高的,它可以适用于所有循环场景(虽然能用,但是在很多场景下,for
和while
才是最优选择),因为loop就是一个简单的无限循环,你可以在内部实现逻辑通过break
关键字来控制循环何时结束。
使用loop
循环一定要打起精神,否则你会写出下面的跑满你一个cpu核心的疯子代码:
fn main() {
loop {
println!("again!");
}
}
该循环会不停的在终端打印输出,直到你使用Ctrl-C
结束程序:
again!
again!
again!
again!
^Cagain!
注意,不要轻易尝试上述代码,如果你电脑配置不行,可能会死机!!!
因此,当使用loop
时,必不可少的伙伴是break
关键字,它能让循环在满足某个条件时跳出:
fn main() {
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
的值,最后赋给result
并打印出来。
这里有几点值得注意:
- break可以单独使用,也可以带一个返回值,有些类似
return
- loop是一个表达式,因此可以返回一个值