各种编程语言中,进行流程控制的结构无非是选择和循环,Go 和 Rust 亦是如此。在 Go 和 Rust 中,他们分别属于语句和表达式。Go 中的选择语句包括 if
语句和 switch
语句,Rust 中对应有 if
表达式、if let
表达式和 match
表达式;Go 中的循环语句只有一个 for
语句,Rust 中则有 loop
表达式、while
表达式、while let
表达式和 for
表达式。
条件结构
条件结构大都用 if
语句(或表达式)表示,它是最常见的选择结构。Go 和 Rust 中的 if
结构差不多。
下面先给出一个 Go 中的 if
语句示例:
if x := 11; x%3 == 0 {
fmt.Printf("%v 能被 3 整除!\n", x)
} else if x%2 == 0 {
fmt.Printf("%v 能被 2 整除!\n", x)
} else {
fmt.Printf("%v 不能被 2、3 整除!\n", x)
}
以下是用 Rust 实现上面相同的功能:
let x = 11;
if x % 3 == 0 {
println!("{} 能被 3 整除!", x)
} else if x % 2 == 0 {
println!("{} 能被 2 整除!", x)
} else {
println!("{} 不能被 2、3 整除!", x)
}
可以看出,两种语言的 if
结构看起来很相似。不过,可以在 Go 的 if
后面添加一个初始化语句(如上例中的 x := 11;
,该语句只能是简单语句,通常是短变量声明语句),Rust 却不能这么做。
另外,由于 if
结构在 Rust 中属于表达式,其使用要更加灵活:
let a = 27;
let b = 33;
let max = if a > b { a } else { b };
分支结构
分支(或称开关)是另外一种选择结构,它根据变量值或表达式,使控制流被导入到各个相应的分支。通过分支结构可以用更加简洁的方式表示 if
-else
-if
-else
链。
Go 的 switch
语句
Go 的 switch
语句可译作开关语句,它将一个 switch 表达式或类型与各个 case 表达式相对比,从而判断该执行哪个分支。存在两种形式的分支语句:表达式分支语句和类型分支语句。在表达式分支语句中,要对比的双方是 switch 表达式的值和各个 case 表达式的值;在类型分支语句中,要对比的双方是特别标注的 switch 表达式和各个 case 中的类型。
以下是一个表达式分支语句示例:
var x uint = 3
switch x {
default:
fmt.Println("x > 5")
case 0:
fmt.Println("x = 0")
case 1, 2:
fmt.Println("1 <= x <= 2")
case 3, 4, 5:
fmt.Println("3 <= x <= 5")
}
其中 x
是 switch 表达式,而 1, 2
和 3, 4, 5
则分别是逗号分割的多个 case 表达式的列表。其中 default
分支可以放在任何位置,但都只在其他分支没有执行时执行。每个分支中可以有多个语句,这些语句属于一个隐含的代码块,不需要用大括号括起来。
switch 表达式将被看作是一个有特定类型的值,即便该表达式的值本身是无类型常量,它将被隐式地转换为默认类型,因此该表达式不能是无类型的 nil
。switch 表达式的类型还必须是可比较的。另外,如果 case 表达式是无类型常量,它将被隐式地转换为 switch 表达式的类型;如果无法完成转换,将会报错。
switch
后面也可以没有 switch 表达式,这时该表达式相当于 true
,将匹配 case 表达式中的 true
。如下所示:
// TimePeriodName 函数根据输入的一天内的时间确定该时间点属于何种时间段,他将对应返回
// "早上"、"上午"、"晚上" 这些中文字符串,其中 hour 的整数部分取值范围应为 [0, 23),
// 小数部分对应一个小时内十进制的时间,如 8.75 对应的实际时间是 8:45。
func TimePeriodName(hour float64) string {
switch {
case hour >= 1 && hour < 5:
return "凌晨"
case hour >= 5 && hour < 8:
return "早上"
case hour >= 8 && hour < 11:
return "上午"
case hour >= 11 && hour < 13:
return "中午"
case hour >= 13 && hour < 17:
return "下午"
case hour >= 17 && hour < 19:
return "傍晚"
case hour >= 19 && hour < 23:
return "晚上"
case (hour >= 23 && hour <= 24) || (hour >= 0 && hour < 1):
return "子夜"
default:
return ""
}
}
如同 if
语句一样,switch
语句中也可以有个初始化语句,该语句必须是简单语句,通常是短变量声明语句。如下所示:
switch score := 82; {
default:
fmt.Println("无效的分数")
case score >= 0 && score < 60:
fmt.Println("不及格")
case score >= 60 && score <= 100:
fmt.Println("及格")
}
即便多个分支都满足要求,但由于 switch
语句不能向下穿透,它将只执行第一个满足要求的分支。要使某个分支向下穿透,需要添加 fallthrough
语句。如下所示:
switch score := 82; {
default:
fmt.Println("无效的分数")
case score >= 0 && score < 60:
fmt.Println("不及格")
case score >= 60 && score <= 100:
fmt.Println("及格")
fallthrough
case score >= 80 && score <= 100:
fmt.Println("优秀")
}
以上代码片段将分两行打印 及格
和 优秀
。
可以在 switch
语句的分支中使用 break
,这将终止 switch
的执行。
类型分支语句与表达式分支语句类似,不过它对类型进行比较。其 switch 表达式使用放在圆括号内的关键字 type
实现类型断言。以下是一个示例:
func main() {
findType(8)
findType(false)
findType(nil)
findType(5.0)
findType("hello")
}
func findType(x interface{}) {
switch v := x.(type) {
default:
fmt.Printf("未预测到的类型 %T\n", v) // %T 打印 v 的类型
case bool:
fmt.Printf("布尔类型 %v\n", v) // v 的类型为 bool
case int:
fmt.Printf("整数类型 %d\n", v) // v 的类型为 int
case *bool:
fmt.Printf("指向布尔类型值的指针 %v\n", *v) // v 的类型为 *bool
case *int:
fmt.Printf("指向整数类型值的指针 %d\n", *v) // v 的类型为 *int
case nil:
fmt.Printf("nil 值\n")
}
}
该段代码将打印:
整数类型 8
布尔类型 false
nil 值
未预测到的类型 float64
未预测到的类型 string
上例中的 x
必须是接口类型,并且不能是一个类型参数,每个 case
分支中列出的类型必须实现了 x
所属的接口,且所列出的每种类型必须是不同的。case
后面也可以是 nil
,但至多有一个这样的分支。以上 v := x.(type)
是一个短变量声明,在每个 case
分支中,v
被转换为对应的类型,而不再像 x
那样是一个接口值。在类型分支语句中不允许使用 fallthrough
语句。
Rust 的 match
表达式
Rust 中的 match
表达式可译作匹配表达式,它将一个检验对象(scrutinee)表达式与多个模式(pattern)相比较,从而判断该执行哪个分支。检验对象表达式和模式分别相当于 Go 的 switch
语句中的 switch
表达式和 case
表达式。在 Go 中,一般只是简单地判断 switch
表达式的值与 case
表达式的一个或多个值中的某个是否相等,但 Rust 则专门定义了模式用于判断两个事物是否匹配,它是一个非常有表现力的东西,使用模式能创建强大而简洁的代码。
模式基于给定数据结构去匹配值,并可选地将变量和这些结构中匹配到的值绑定起来。除了 match
分支,在其他许多位置都要用到模式匹配,如 if let
条件表达式、while let
条件循环、for
循环、let
语句、函数参数。例如,let
语句的形式实际上是:
let 模式 = 表达式;
具体示例如下:
let (x, y, z) = (3.5, 4.2, 10.0);
该行代码将一个元组与模式匹配,由于正好能够匹配,从而解构了该元组,即将元组元素与模式中的变量分别绑定起来。
模式有多种形式:字面量模式、标识符模式、通配符模式、剩余模式、区间模式、引用模式、结构体模式、元组结构体模式、元组模式、分组模式、切片模式、路径模式、或模式。通俗来讲,通过模式匹配,判断表达式的值和模式是否相等、是否存在、是否在某个范围内、是否为真等。
先看第一个例子:
let x: u32 = 3;
match x {
0 => println!("x = 0"),
1 | 2 => println!("1 <= x <= 2"),
3 | 4 | 5 => println!("3 <= x <= 5"),
_ => println!("x > 5"),
}
其中 0
模式只有一个字面量构成。1 | 2
和 3 | 4 | 5
属于或模式,它使用 |
将多个由字面量构成的模式组合起来,x
的值若匹配模式中的任何一个值,该分支将会被执行。最后一个 _
属于通配符模式,_
能与任何值匹配,因此相当于 Go 的 switch
中的 default
分支,但该分支必须放置在所有分支的最后。该段代码也可写为如下形式:
let x: u32 = 3;
match x {
0 => println!("x = 0"),
1 | 2 => println!("1 <= x <= 2"),
3 | 4 | 5 => println!("3 <= x <= 5"),
t => println!("x = {}, x > 5", t),
}
这里 t
是一个简单的标识符模式,由于检验对象 x
和模式 t
匹配,x
的值被赋值给模式中的变量 t
了。
再将前面给出的打印一天中时间段名称的 Go 代码转变为 Rust 代码,如下所示:
fn time_period_name(hour: f64) -> String {
match hour {
x if (1.0..5.0).contains(&x) => String::from("凌晨"),
x if (5.0..8.0).contains(&x) => String::from("早上"),
x if (8.0..11.0).contains(&x) => String::from("上午"),
x if (11.0..13.0).contains(&x) => String::from("中午"),
x if (13.0..17.0).contains(&x) => String::from("下午"),
x if (17.0..19.0).contains(&x) => String::from("傍晚"),
x if (19.0..23.0).contains(&x) => String::from("晚上"),
x if (23.0..24.0).contains(&x) || (0.0..1.0).contains(&x) => String::from("晚上"),
_ => String::from(""),
}
}
以上代码中的 x
同样属于标识符模式,一旦匹配,hour
的值将赋值给 x
。但 x
的后面还跟着 if (1.0..5.0).contains(&x)
形式的内容,这叫作匹配守卫(match guard)。匹配守卫是一个指定于 match
分支模式之后的额外 if
条件,它也必须被满足才能选择此分支。其中的 (1.0..5.0).contains(&x)
等价于 (x >= 1.0 && x < 5.0)
,有关 contains
方法的文档请参见此页。
再把前面成绩区间判断的 Go 代码转写为 Rust 代码:
let score: u32 = 82;
match score {
0..=59 => println!("不及格"),
60..=100 => println!("及格"),
_ => println!("无效的分数"),
}
其中的 0..=59
和 60..=100
属于区间模式,它匹配处于由给定的上下界构成的闭区间内的值。
同 Go 的 switch
一样,Rust 的 match
也是不能向下穿透的,但 Rust 中不存在类似 Go 中的 fallthrough
来强制向下穿透。实际上,Rust 也不提倡各个分支的模式相互重合,如以下代码:
let score: u32 = 82;
match score {
0..=60 => println!("不及格"),
60..=100 => println!("及格"),
_ => println!("无效的分数"),
}
它虽然能通过编译,但编译器会产生警告:“multiple patterns overlap on their endpoints”,因为第 1、2 个分支都包含 60。
另外,Rust 的 match
与 Go 的 switch
还存在一个重要差别,就是 match
表达式的匹配必须是穷尽的。例如:
let score: u32 = 82;
match score {
0..=59 => println!("不及格"),
60..=100 => println!("及格"),
// _ => println!("无效的分数"),
}
将会产生编译错误:“non-exhaustive patterns: 101_u32..=u32::MAX
not covered”。
Go 和 Rust 中都有 break
关键字。当 break
语句处在 Go 的 switch
语句中,且后面不跟标签时,它中断的是当前 switch
语句的执行;而当 break
表达式处在 Rust 的 match
表达式中,同样后面不跟标签时,它中断的是外层包括 match
表达式的循环表达式的执行。
match
表达式常用来检查枚举值,例如:
let x = Some(3);
match x {
Some(0) => println!("x = 0"),
Some(1) | Some(2) => println!("1 <= x <= 2"),
Some(3) | Some(4) | Some(5) => println!("3 <= x <= 5"),
Some(t) => println!("x = {}, x > 5", t),
None => println!("null value"),
}
关于以上代码,将在以后介绍枚举类型时再解释。
除了以上模式,还有更多种类的模式就不再一一介绍了,详情请见 Rust 参考手册。
借助模式语法,Rust 的 match
通常比 Go 的 switch
更简短,且能完成一些复杂的匹配,尤其是对元组、枚举、结构体等的解构十分方便。不过就简单的分支结构来说,Go 的 switch
通常更容易被掌握和理解。
Rust 的 if let
表达式
当只需要匹配一个模式而忽略其他模式时,可以用 if let
表达式代替 match
表达式,如下所示:
let score: u32 = 82;
match score {
60..=100 => println!("及格"),
_ => println!("不及格"),
}
这可改写为:
let score: u32 = 82;
if let 60..=100 = score {
println!("及格");
} else {
println!("不及格");
}
if let
后面放置通过等号分隔的一个模式和一个表达式,它是 match
的一个语法糖。
循环结构
循环结构可以使一段只出现一次的程序被连续多次执行,几乎所有编程语言都提供循环指令。Go 和 Rust 的循环结构虽然在使用的关键词、形式有区别,但能实现的功能却是一样的。
Go 的 for
语句
Go 中的循环语句只有一个,就是 for
语句。但该语句可以有多种形式,可以用于不同的情况。最完整的 for
语句形式如下:
for 初始化语句; 条件表达式; 后置语句 {
// 循环体代码
}
其中:
- 初始化语句一般是短变量声明语句,它最早执行,只执行一次;
- 条件表达式在每次迭代之前都被重新计算,如其值为
true
,则执行循环体代码,若为false
,则结束执行循环语句; - 后置语句在每次迭代执行完循环体代码后执行,它不能是短变量声明语句。
for
语句中的初始化语句、条件表达式、后置语句都可以被省略,这里直接通过示例说明。以下所有示例都是对整数类型切片 a
(如令 a := []int{0, 1, 2, 3, 4, 5}
)的遍历,他们是等价的:
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
for i := 0; i < len(a); {
fmt.Println(a[i])
i++
}
for i := 0; ; {
if i >= len(a) {
break
}
fmt.Println(a[i])
i++
}
for i := 0; ; i++ {
if i >= len(a) {
break
}
fmt.Println(a[i])
}
i := 0
for ; i < len(a); i++ {
fmt.Println(a[i])
}
a := []int{0, 1, 2, 3, 4, 5}
i := 0
for i < len(a) {
fmt.Println(a[i])
i++
}
i := 0
for {
if i >= len(a) {
break
}
fmt.Println(a[i])
i++
}
对于数组、指向数组的指针、切片、字符串、映射、从信道接收的值,也可以使用带 range
从句的 for
语句进行遍历。使用 for range
语句就不用担心数组或切片的索引越界,相对来说更简洁和安全。分别有如下示例:
// 索引和数值
for i, v := range a {
fmt.Println(i, v)
}
// 舍弃索引
for _, v := range a {
fmt.Println(v)
}
// 舍弃值
for i, _ := range a {
fmt.Println(a[i])
}
// 只要索引
for i := range a {
fmt.Println(a[i])
}
Rust 的 loop
表达式
loop
循环是无限循环表达式,相当于 Go 中没有初始化语句、条件表达式、后置语句的 for {}
语句。可以通过 if
和 break
表达式终止循环。如下所示:
let a = vec![0, 1, 2, 3, 4, 5];
let mut i: usize = 0;
loop {
if i >= a.len() {
break;
}
println!("{}", a[i]);
i += 1;
}
其中 a
是一个向量(Vector),i
表示该向量的索引,其为 usize
类型,但也可以省略此类型,而让编译器确定其类型。
可以使用 break
表达式从循环中返回一个值:
let mut x = 0.0;
let f = loop {
let v = 0.5 * x * x + 1.0;
if v > 100.0 {
break v;
}
x += 1.0;
};
Rust 的 while
表达式
以上给出的在 loop
循环中使用 if
、break
返回的模式非常常见,可以使用 while
表达式替换以上模式,该表达式自带一个条件,只有当条件为 true
才执行循环:
let a = vec![0, 1, 2, 3, 4, 5];
let mut i = 0;
while i < a.len() {
println!("{}", a[i]);
i += 1;
}
如同 if let
表达式一样,Rust 同样有一个 while let
表达式用于进行模式匹配,仅当匹配时才执行循环。对于下面的代码:
let a = vec![0, 1, 2, 3, 4, 5];
let mut i = 0;
while i < a.len() {
if let Some(v) = a.get(i) {
println!("{}", v);
}
i += 1;
}
也可以将其改写为:
let a = vec![0, 1, 2, 3, 4, 5];
let mut i = 0;
while let Some(v) = a.get(i) {
if i >= a.len() {
break;
}
println!("{}", v);
i += 1;
}
Rust 的 for
表达式
Rust 中同样有 for
表达式,但不像 Go 中的 for
语句那样灵活多样,它更像是 Go 中 for range
形式的语句,能更方便地对各种集合类型进行遍历。其形式为:
for 模式 in 迭代器表达式 {
// 循环体代码
}
这里的迭代器表达式(简称迭代器)是一个新的概念。迭代器都实现了一个 trait:std::iter::IntoIterator
,它负责遍历序列中的每一项和决定序列何时结束的逻辑。迭代器只有一个 next
方法,每次调用 next
方法,将返回迭代器序列中的一项,并将迭代器中用来记录序列位置的状态向后移动一次,即所谓消费了一个项。
for
表达式能够很好地和迭代器互动。创建迭代器的最简单的方法是使用区间标记 a..b
,这将会生成从 a
(包含此值) 到 b
(不含此值)的,步长为 1 的一系列值。例如:
let a = vec![0, 1, 2, 3, 4, 5];
for i in 0..a.len() {
println!("{}", a[i]);
}
对于集合类型数据,可以直接将他们当作迭代器来使用,for
循环会对给出的集合应用 into_iter
函数,把它转换成一个迭代器。例如:
let a = vec![0, 1, 2, 3, 4, 5];
for v in a {
println!("{}", v);
}
以上代码和下面的代码完全等价:
let a = vec![0, 1, 2, 3, 4, 5];
for v in a.into_iter() {
println!("{}", v);
}
into_iter
在每次迭代中,将提供集合中的数据本身。该函数会消耗集合,一旦集合被消耗了,之后就无法再使用了,因为它的所有权已经在循环中被移动了。例如:
let langs = vec!["Go", "Rust", "Java"];
for lang in langs.into_iter() {
match lang {
"Rust" => println!("{} 语言有点复杂", lang),
_ => println!("{} 语言比较简单", lang),
}
}
// println!("langs: {:?}", langs);
如果把上面后一个语句前面的注释取消,将无法通过编译,因为 langs
的所有权在上一个循环中已经被转移了。
还可以使用 iter
函数生成迭代器。iter
在每次迭代中借用集合中的一个元素。这样集合本身不会被改变,循环之后仍可以使用。下面的代码是可以编译和执行的:
let langs = vec!["Go", "Rust", "Java"];
for lang in langs.iter() {
match lang {
&"Rust" => println!("{} 语言有点复杂", lang),
_ => println!("{} 语言比较简单", lang),
}
}
println!("langs: {:?}", langs);
另外,还可以使用 iter_mut
函数生成迭代器。iter_mut
可变地借用集合中的每个元素,从而允许集合被就地修改。如下所示:
let mut langs = vec!["Go", "Rust", "Java"];
for lang in langs.iter_mut() {
match lang {
&mut "Rust" => {
println!("{} 语言有点复杂", lang);
*lang = "C++";
},
_ => println!("{} 语言比较简单", lang),
}
}
println!("langs: {:?}", langs);
如果还要同时获得索引,就要使用迭代器的 enumerate
方法,该方法产生迭代器中一项对应的索引和值构成的元组。如下所示:
let a = vec![0, 1, 2, 3, 4, 5];
for (i,v) in a.iter().enumerate() {
println!("{} {}", i, v);
}
其他结构
除了以上控制流结构外,两种语言都使用 return
、break
和 continue
关键词进行流控制,两种语言也都支持给代码加标签。其主要区别包括:
- 在 Go 中,这些关键词所表征的为语句,而 Rust 中,这些关键词所表征的为表达式。
- Go 中的
break
语句可以终止最内层的for
、switch
或select
语句的执行;而 Rust 中的break
则只终止各种循环体的执行。 - Go 中的
break
后面可以跟一个标签,该标签对应到具体的for
、switch
或select
语句;Rust 中的break
后面除了能跟循环标签外,还能跟表达式。 - Go 中的标签可以加在任何代码的前方,它可以是
goto
、break
或continue
的跳转目标;Rust 中的标签只能加在循环表达式前面,称为循环标签,是break
或continue
的跳转目标,标签名称前面还必须有一个额外的单引号'
。
另外,Go 中仍然保留了 goto
语句,这在特殊场合下仍然有用。
讨论
本来觉得用较小的篇幅就能讲清楚本文,然后结果这个篇幅却不算小,并且难度也不低。对于 Go,其各种控制流语句仍然保持简单,不需要涉及太多的知识。但 Rust 就复杂很多,从一开始就涉及到许多较为复杂的概念,如模式匹配、迭代器、所有权等,Rust 巧妙地将各种较为复杂的设计元素编织在一起形成一个语言系统,这固然很强大,但却导致我们难以循序渐进地学习该语言——难怪大家都说 Rust 的学习曲线比较陡峭!