rust入坑小记-03-函数与控制流
4 函数、语句与表达式
4.1 函数
函数的定义如下:
1 | fn function(a:i32,b:i32) -> f64 { |
就这么简单。
与C或python不同的是,你无需关心函数定义在哪个位置(在前还是在后),rust中的函数都可以调用到。
4.2 语句与表达式
下面介绍语句与表达式。一般语句指的是以分号结尾的一些操作,在函数中,语句并不会返回值,比如:
1 | let a = 3; |
而表达式则是指没有分号的,并且会在求值后返回一个值,比如:
1 | let x = 5; // 语句 |
在函数中,函数体包括一些语句+最后一行的零个或一个表达式。语句与表达式在写法上就差一个分号,表达式不能包含分号,因此,在函数结尾需要返回值的时候不能带分号,否则它就会变成一条语句,不会返回值。最后,如果不返回任何值,则会隐式地返回一个 ()。
当然,你也可以在函数体中使用return来返回值(return带分号和不带分号都可以):
1 | fn foo() -> i32 { |
4.3 发散函数!
发散函数(diverging functions),返回值类型为特殊的!,表示该函数永不返回,一般用于导致程序崩溃的函数:
1 | fn forever() -> ! { |
4.4 函数与所有权
当看到这个标题,相信你已经明白了,函数的参数传递也会进行所有权的转移。当然,实现了Copy特征的基本类型是通过拷贝进行的,所以没有所有权的转移,下面的代码不会报错:
1 | fn main() { |
如果是复合类型,没有实现Copy特征,情况就不同了:
1 | fn main() { |
上面的代码会报错,说明在传递参数的时候,进行了所有权转移,a不再拥有值hello,而是转移到了value,在函数执行完毕后,函数的{}作用域结束,value就被drop掉了,因此第四行再次打印a就会报错。
解决方法也很简单,我们传递参数时使用引用即可:
1 | fn main() { |
因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。这样,程序就可以正常运行了。
5 流程控制
5.1 条件控制
通过if和else:
1 | fn main() { |
5.2 循环控制
rust存在三种循环。
for
首先是for:
1 | fn main() { |
其中,0..=10的含义是生成从0到10的连续序列,即[0,10]这样的闭区间,如果不加=号0..10,则是左闭右开的区间[0,10)。
对于for来说,有一点需要注意,那就是所有权的问题。
首先,对于实现了Copy特征的数组来说,使用for i in 数组并不会将所有权转移,而是进行了内存拷贝,比如:
1 | fn main() { |
但是对于复杂的类型,则会发生所有权转移:
1 | fn main() { |
解决方法就是使用引用:
1 | fn main() { |
引用默认是不可变的,因此你无法修改元素:
1 | fn main() { |
需要使用mut 来解决:
1 | fn main() { |
如果你使用过python,那么一定很熟悉enumerate获取可迭代对象的索引,在rust中是这样写的:
1 | fn main() { |
iter()方法会将a转化为迭代器,再使用enumerate()即可在for循环中获取索引。
while
第二种是while,即条件循环,满足某个条件就进行循环,直到不满足条件为止:
1 | fn main() { |
loop
第三种是loop,即无条件循环:
1 | fn main() { |
这个代码会无限循环下去。因此,对于循环,我们还需要一些可以约束循环的关键字:continue和break:
1 | fn main() { |
若操作返回一个值,则可能需要将其传递给代码的其余部分:将该值放在 break 之后,它就会被 loop 表达式返回。
1 | fn main() { |
5.3 循环标签
一个循环表达式可以选择设置一个标签。这类标签被标记为循环表达式之前的生命周期(标签),如:
1 | 'foo: loop { break 'foo; } |
如果循环存在标签,则嵌套在该循环中的带此标签的 break表达式和 continue表达式可以退出此标签标记的循环层或将控制流返回至此标签标记的循环层的头部。
比如:
1 | fn main() { |
同理可以用于continue:
1 | fn main() { |
6 模式匹配
6.1 match
在枚举一节的最后,我们提到了取Some(T)的方法,这里用到的就是模式匹配。首先通过一个例子来介绍match
1 | enum Animal{ |
首先创建了一个枚举类型和一个枚举成员的实例,接下来对这个实例cat进行模式匹配,使用match去匹配它对应的类型。下面是match的一些特性:
在
match内部我们需要将所有的可能都列出来。如果你不列出来,编译器会报错:=>的左边,是我们的匹配条件,也叫做模式,右边是匹配成功后执行的代码,也叫做针对该模式进行处理的代码。使用
|表示逻辑或,也就是说只要有一个匹配上,就算匹配成功。最后的
_代表没有匹配成功的默认匹配,和C/C++/typescript中的switch语句内的default很像,作为兜底选项存在。还需要注意一点就是
=>右边可以也可以有多行代码,需要用{}包裹,但无论是单行代码还是多行代码,最后一行一定是一个表达式。1
2
3
4
5
6
7
8
9match something {
case1 => do_something, // 表达式
case2 => { // 可以是多行代码
balabala; // 语句
balabala; // 语句
expression // 表达式
},
_ => println!("...")
}match本身也是一个表达式,因此你可以这样写,将match匹配到的值绑定到a上:1
2
3let a = match {
...
}模式匹配从上到下按顺序执行,如果模式匹配了这个值,那么模式之后的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支。
模式匹配还有一个比较好用的功能,就是获取模式中绑定的值:
1 | enum Animal{ |
这里的name就是绑定到枚举成员上的值。
另外,match同样涉及到所有权转移,还是上面的例子:
1 |
|
根据所有权转移的规则,由于String不是基本类型,没有实现Copy特征,它的所有权先移动到cat,然后通过模式匹配,所有权转移到name,在离开{}的作用域之后,name被drop掉了,再次打印cat时,就会报错。
当然,rust编译器贴心地给出了提示,通过引用&来避免所有权转移:
1 | match &cat { |
当然,也可以通过ref,在通过 let 绑定来进行模式匹配或解构时,ref 关键字可用来创建结构体/元组的字段的引用:
1 | match cat { |
6.2 if let
在某些场景下,会遇到只有一个模式的值需要被处理,其它值直接忽略的场景,这时使用match就显得很复杂
1 | fn main() { |
我们只想要对 Some(3) 模式进行匹配, 不想处理任何其他 Some<u8> 值或 None 值。但是为了满足 match 表达式(穷尽性)的要求,写代码时必须在处理完这唯一的成员后加上 _ => (),这样会增加不少无用的代码。
我们完全可以用 if let 的方式来实现,在这种只有两个情况的场景下会简洁很多:
1 | fn main() { |
if let语法格式如下,当然else是可选的:
1 | if let 匹配值 = 原变量 { |
你可以使用else if来增加判断项:
1 | fn main() { |
6.3 while let
和 if let 类似,while let 也可以把别扭的 match 改写得好看一些。考虑下面这段使 i 不断增加的代码:
1 | // 将 `optional` 设为 `Option<i32>` 类型 |
使用 while let:
1 | fn main() { |
6.4 matches!
matches!是一个宏,它的作用是将一个表达式跟模式进行匹配,如果匹配成功,结果是 true 否则是 false。
1 | let foo = 'f'; |