Common Programing Concepts
Functions 函数
函数在Rust中被普遍使用,你已经见过了在这门语言中最重要的函数:main
函数,它是很多程序的入口点。你也看到过fn
这个关键字,它允许你定义一个新的函数。
Rust代码使用 snake case 蛇形命名法作为函数和变量命名的传统规则。在蛇形命名法中,所有的字母都得是小写且通过下划线来分隔单词。如下就是一个定义函数的例子:
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
Rust函数定义以fn
关键字作为开头,并且在函数名后紧跟一对括号,而后面的尖括号部分则是告诉编译器函数体的起止位置。
我们可以调用任何我们定义的函数,通过输入对应的函数名与一对括号。譬如在上面的例子中,我们在程序里定义了another_function
这个函数,所以它可以在main
函数中被调用。这里我们在main
函数之后才定义了another_function
,但即使你将它放到main
函数之前,也是一样的结果。Rust不会去关心你在哪里定义了函数,只会检查这个函数是不是有定义。
让我们新建一个叫做functions
的项目,将another_function
这个例子写进src/main.rs
并运行,你应该会看到下面的输出结果:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.28 secs
Running `target/debug/functions`
Hello, world!
Another function.
可以看到,文本按照它们在main
函数中出现的顺序依次显示了出来。首先是"Hello,world!"这个消息,接着调用another_function
并将它的消息打印了出来。
Function Parameters 函数形参
我们当然可以定义带形参的函数,形参是一类特殊的变量,它们是函数签名的一部分。当一个函数包含形参的时候,你可以在调用函数时将具体的值传给这些形参。在一些专业术语中,会把这些具体的值叫做实参。现实中,通过形参来表示函数定义中的变量,而用实参来表示调用函数时的具体值。
下面是一个重写的another_function
函数,用来展示如何在Rust中使用形参和实参:
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {}", x);
}
试着运行程序,你会看到像下面的结果:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 1.21 secs
Running `target/debug/functions`
The value of x is: 5
在another_function
的定义中,它有一个形参叫做x
,且x
的类型被指定为i32
。当5
这个实参传递给another_function
,宏println!
会将5
放入字符串中的尖括号对中。
在函数签名中,你必须定义每个参数的类型,这是设计Rust这门语言时深思熟虑的结果:在函数的定义阶段就指明类型,编译器就几乎不需要再在你代码的其它地方来解析它究竟是什么。
如果你想要一个函数有多个形参,可以通过一个逗号将它们分隔,就像下面这样:
fn main() {
another_function(5, 6);
}
fn another_function(x: i32, y: i32) {
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}
这个例子创建了一个函数,它有两个形参,且都为i32
类型。这个函数会将它们的值都打印出来。函数形参并不需要都是同样的类型,这仅仅是一个示例。
现在尝试下运行这段代码,把它覆盖你项目中的src/main.rs,并通过cargo run
执行:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
Running `target/debug/functions`
The value of x is: 5
The value of y is: 6
因为我们调用这个函数时,将5
传给了x
,将6
传给了y
,所以两行字符串分别打印了这两值。
Function Bodies Contain Statements and Expressions 包含语句和表达式的函数体
Rust中的函数体是由一系列语句组成,也可以选择以表达式结尾。目前为止,我们只介绍了没有表达式结尾的函数,但是你已经看到在一些语句中的部分,我们用到了表达式。因为Rust是基于表达式的编程语言,这是它与其它编程语言相比非常重要的一个区别,其它语言间并没有这种区别。所以让我们来看下什么是语句,什么是表达式,它们又对函数体有什么不同的影响。
我们实际上已经使用过语句和表达式了。语句泛指那些执行某些操作但是不会返回任何值得指令;表达式则代表一个结果值。我们来看几个具体的例子。
创建一个变量并通过let
关键字给它指定一个值,这就是典型的语句:
fn main() {
let y = 6;
}
函数定义中也包含有语句,我们上面用到过的例子本身就包含一个语句。
语句不会返回值,因此你不能将let
语句指定给另一个变量,就像下面的例子做的那样:
fn main() {
let x = (let y = 6);
}
当你运行程序,你会得到一个报错消息:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found statement (`let`)
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: variable declaration using `let` is a statement
let y = 6
这个语句不会返回任何值,所以没有任何东西能给x
去绑定。这与其它编程语言有很大的不同,像是C或是Ruby,在这些编程语言里,指定操作会返回指定的值本身。所以你可以在这些语句中使用 x = y = 6
来将x
和y
同时赋值6
,但在Rust中这并不可行。
表达式是一些指令的计算结果,你未来的Rust生涯中将大量的使用到表达式。让我们来构思一个简单的数学操作,譬如5+6
,这就是一个表达式,它的计算结果是11
。表达式可以是语句的一部分,就像上面例子let y = 6;
这个语句,里面的6
就是一个表达式。调用函数可以是一个表达式,调用一个宏命令也可以是一个表达式。甚至我们通过尖括号{}
包裹的句块也是一个表达式:
fn main() {
let x = 5;
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {}", y);
}
这个片段就是表达式:
{
let x = 3;
x + 1
}
上面的句块是个表达式,并且计算结果为4
,这个值可以在let
语句中使用并被绑定到变量y
上。留心,x + 1
这行代码后,我们并没有添加分号,这点与你之前看过的很多代码都不同。表达式是没有终止符分号的,如果你在表达式的末尾添加了分号,它就变成了语句且不会返回任何值。请牢牢记住这点,我们接下来将介绍函数返回值。
Functions with Return Values 带有返回值的函数
函数在被调用时可以返回一些值,我们不需要给这些值命名,但是我们需要使用一个箭头符号->
来定义它们的类型。在Rust中,函数的返回值即是函数体语句块最后一个表达式。你可以使用return
关键字,指定一个值并提前在函数体中返回它,但一般来说,大多数函数都是含蓄的通过最后一个表达式去返回值。下面是一个简单的带返回值函数:
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {}", x);
}
five
中没有函数调用,没有宏,甚至连let
语句也没有,只有一个5
。这在Rust中是一个有效的函数,请注意,我们已在函数签名里通过-> i32
指明了返回值类型,运行这段代码,会输出如下结果:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.30 secs
Running `target/debug/functions`
The value of x is: 5
5
是five
函数的返回值,并且类型是i32
。我们再来细细分析这段代码,这里有两个非常重要的地方:第一,是语句let x = five();
,它展示了我们如何通过函数返回值来初始化一个变量。因为five
函数返回值5
,所以它其实等效于下面的语句:
let x = 5;
第二,five
函数没有任何形参,定义了返回值的类型。函数体中只有一个孤零零的5
,且没有任何分号——它是一个表达式,返回了我们想要返回的值。
让我们再看看另一个例子:
fn main() {
let x = plus_one(5);
println!("The value of x is: {}", x);
}
fn plus_one(x: i32) -> i32 {
x + 1
}
运行这段代码,会打印出The value of x is: 6
,但是如果我们在x+1
后面添加一个分号,会将它从表达式变为语句,我们的程序就会报错。
fn main() {
let x = plus_one(5);
println!("The value of x is: {}", x);
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
编译这段代码,会有如下报错信息:
error[E0308]: mismatched types
--> src/main.rs:7:28
|
7 | fn plus_one(x: i32) -> i32 {
| ____________________________^
8 | | x + 1;
| | - help: consider removing this semicolon
9 | | }
| |_^ expected i32, found ()
|
= note: expected type `i32`
found type `()`
上面的报错消息 “mismatched types” 直接暴露了问题的核心,函数plus_one
说它会返回一个i32
值,但语句并没有计算出结果,只有一个()
空元组。因此 ,没有任何值返回,这违背了函数的定义,所以导致报错。在这个输出结果中,Rust提供了一些信息来帮助解决这个错误:它建议移除第八行末尾的分号,这样就能修复这个错误。
Comments 注释
程序员们都想方设法要使自己的代码易于理解,所以有时候一些额外的注释是很有必要的。在一些案例中,程序员会在他们的代码中留下笔记或注释,编译器会忽略这部分但其它人却可以通过这些注释来帮助他们阅读源码。
这是一个简单的注释:
// hello, world
在Rust中,注释以两个斜线开始,并直到这行语句末尾结束。如果你的注释不止一行,纳尼须要在每一行前添加`//``,就像下面这样:
// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.
你也可以在代码后面使用注释:
fn main() {
let lucky_number = 7; // I’m feeling lucky today
}
但更多时候,你会看到注释会独占一行并被放在它须要注明的代码前:
fn main() {
// I’m feeling lucky today
let lucky_number = 7;
}
Rust还有另一种注释,叫做文档注释,我们会在第十四章中再做讨论。
Control Flow 控制流
通过判断某些条件是否符合来决定是否运行某些代码,或是在某些情况下重复执行某段代码,这些是大部分编程语言的基础功能。常见的用来控制Rust程序执行流的结构是if
表达式和loop
。
if
Expressions if
表达式
if
表达式允许你依据不同条件为你的代码创建分支。你提供一个条件,并说明如果给出的条件符合,执行下面的代码块;如果条件不符合,下面的代码块就不执行。
在你的projects目录下创建一个新的项目branches来研究if
表达式。将下面的代码复制到src/main.rs
文件中:
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
所有的if
表达式都是以关键字if
开始,后面再跟上一个判断条件。在上面的例子中,这个条件用于检查number
变量的值是否小于5。当这个表达式为真时,你想要执行的代码块被放在了后面的尖括号中。代码块和与它相关的条件,在if
表达式中有时也被称为arms臂,就像我们第二章时讨论的match
表达式里的臂一样。
可选的,我们还包含了一个else
表达式,当之前的判断条件是错误时,程序将执行else
后面的代码块。如果你没有为条件失败的情况编写else
表达式,程序就会跳过if
代码块,继续执行下面的代码。
试着运行这个程序,你会看到下面的结果:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
Running `target/debug/branches`
condition was true
再试试修改number
的值,使得我们的判断条件返回false
:
let number = 7;
再次运行程序,就会看到下面的结果:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
Running `target/debug/branches`
condition was false
须要注意一点,代码中条件的值必须是一个bool
值,如果判断条件并不是一个bool
值,就会发生错误,试试看运行下面的代码:
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
这次if
语句的条件计算结果是一个3
,Rust抛出了一个错误:
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected bool, found integer
|
= note: expected type `bool`
found type `{integer}`
这个错误提示到,Rust希望获得一个bool
值,但是实际上收到的却是一个整数。不同于Ruby和Javascript,Rust不会自动将非波尔类型转化为一个波尔值,你必须明确的给if
语句提供一个波尔值。如果我们希望if
语句块只在一个数字不等于0
时才执行,我们可以像下面的方法修改if
表达式:
fn main() {
let number = 3;
if number != 0 {
println!("number was something other than zero");
}
}
运行上面的代码,将显示number was something other than zero
。
Handling Multiple Conditions with else if
通过else if
语句处理多个条件
通过将if
和else
组合成else if
表达式,你就能同时处理多个条件:
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
这个程序有四个可能执行的路径,执行完毕后,你将会看到如下输出:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
Running `target/debug/branches`
number is divisible by 3
当程序执行时,它会依次检查每一个if
表达式并只执行第一个符合条件的代码块,所以你可以看到,尽管6可以被2整除,但我们既不会看到number is divisible by 2
,也不会看到 number is not divisible by 4, 3, or 2
,这是因为Rust只会执行第一个条件为真的代码块,之后就不会再去检查剩下的条件是否符合。
使用太多else if
表达式会使你的代码变得混乱,当你有多个条件时,你可能希望尝试优化你的代码。第六章中,我们将介绍Rust中一个强力的代码分支结构match
来应对这种情况。
Using if
in a let
Statement 在let
语句中使用if
因为if
是一个表达式,所以我们可以在let
语句中使用它。
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
println!("The value of number is: {}", number);
}
这个number
变量会绑定一个if
表达式的返回值,运行这段代码,我们会得到如下的显示:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.30 secs
Running `target/debug/branches`
The value of number is: 5
请记住几点,代码块返回的值取决于它的最后一个表达式;数字本身也是表达式。在上面的例子中,整个if
表达式的值依赖于它到底执行哪个代码块,这隐喻着一个要求,你必须确保if
的每一臂的返回值都是相同的类型,就像我们做的那样,if
和else
臂返回的都是i32
有符号整数。我们尝试修改代码,使得两臂的返回值类型不匹配:
fn main() {
let condition = true;
let number = if condition {
5
} else {
"six"
};
println!("The value of number is: {}", number);
}
当我们尝试再次编译上面的代码,我们就会得到一个报错消息,if
和else
的返回值导致无法编译,并且Rust精确的给我们注释了在程序的哪里可以找到这个问题:
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
|
= note: expected type `{integer}`
found type `&str`
if
表达式的返回值是一个整数类型,而else
表达式的返回值却是一个字符串类型。程序无法执行,因为它要求变量的类型必须一致。Rust在编译时就须要知道number
变量的类型,这样它就能在编译时检查是否number
在用到它的地方总是有效的。Rust无法做到这种检查,当number
的类型只在程序运行时被定义。如果要实现这种功能,就会让编译逻辑变得非常复杂,这就无法保证代码的安全性,因为编译器不得不须要跟踪记录每个变量可能会有到的多个类型。
Repetition with Loops 通过循环语句重复操作
反复执行一段代码块是非常常见的情形。为了这个功能,Rust提供了几种循环语句。循环会执行循环体内的代码,并在到达循环体尾部后重新回到循环体开头。为了研究循环,让我们来新建一个项目loops。
Rust总共有三种循环:loop
,while
和for
,让我们来挨个尝试。
Repeating Code with loop
通过loop
语句循环执行代码
loop
关键字告诉Rust去反复执行一个代码块,直到你明确告诉它停止。
下面是一个loop的例子,请修改你loops项目文件夹下的src/main.rs文件:
fn main() {
loop {
println!("again!");
}
}
当我们执行这个程序,我们就会看到again!
反复持续的显示,直到我们手动终止了程序。大部门终端工具提供快捷键 ctrl-c 来中断一个陷入死循环的程序:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
这里的^C
符号表示你按了ctrl-c。你可能会或者也可能不会看到again!
出现在^C
后面,这个主要取决于当收到中断信号时,代码执行到了哪里。
幸运的是,Rust提供了另一个更加合理的方式用来中断循环。你可以将break
关键字添加到你的循环中来告诉程序何时终止循环。还记得我们第二章在猜数字游戏中做的么?当用户猜对正确的数字后,就会退出这个程序。
Returning Values from Loops 通过循环返回值
loop
的一个使用场景是反复尝试一个你知道可能会失败的操作,譬如检查是否一个线程已经完成了它的任务。但有时候,你可能需要将操作的结果传递给剩下的代码,为了这个目的,你可以将你想返回的值放到break
表达式后,当退出循环时,你就能获得到它:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {}", result);
}
在loop
前,我们定义了一个叫做counter
的变量并初始化它的值为0
。接着我们须要定义一个变量result
来兜住循环的返回值。每次循环时,我们都会为counter
加1
,并检查是否counter
的值等于10
。当counter
等于10
时,我们使用break
关键字并紧跟值counter * 2
。循环外,我们使用一个分号来结束这个语句,并将返回值指派给result
,最终我们打印出result
的值,在本例中即是20。
Conditional Loops with while
通过while
执行有条件的循环
程序在循环中使用判定条件常常是非常有用的。当条件符合,继续循环;当条件不符合时,程序使用break
终止循环。这种循环程序会使用到loop
,if
,else
,break
的组合。如果你乐意,你可以现在就在一个程序中尝试。
这种模式是如此普遍,所以Rust为它提供了一个语言内建的结构体,叫做while
循环。下面的例子中就用到了while
,这是个倒计时程序,它将执行三次,数字每次递减,循环结束将打印另一条消息并退出程序。
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("LIFTOFF!!!");
}
这种结构剔除了很多当你使用loop
,if
,else
,break
时须要用到的标记,代码看起来更加简洁。当条件符合时,代码会一直循环跑,否则它就直接退出循环。
Looping Through a Collection with for
通过for
来遍历集合
你能够使用while
来循环遍历一个集合,譬如一个数组。
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index += 1;
}
}
在上面的例子中,代码列举了数组中所有的元素,它从索引0
开始,然后循环执行直到数组末尾(即 index < 5
条件不再满足)。运行这段代码,会打印出数组中的所有元素:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50
数组的所有五个值,都和预期的一样在终端中显示了出来。尽管在某一阶段,index
会变成5
,但循环会在尝试读取第六个数组值前停止。
但这种写法会有一个漏洞,我们会导致程序恐慌,如果给的索引长度不对。同时程序的运行速度也会很慢,因为编译器在运行时代码中须要添加额外的代码来执行条件检查,并且每次循环每个元素都要检查一遍。
这种情况,我们有一个更加简洁的可选方法,我们可以使用for
循环来遍历集合中的每个元素,执行相同的代码。我们可以用下面的代码替换之前例子中的代码:
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
}
当我们执行这段代码,我们会看到同之前例子一样的结果。但不同的是,这次我们程序的安全性得到了提高,我们避免了一些潜在的bug,像是数组访问越界或是跳过个别数组元素。
举个例子,如果我们删除了a
数组中的一个元素而忘了修改判断条件while index < 4
,程序可能会惊慌。通过使用for
循环,当你修改数组元素时,你不需要修改任何代码。
for
循环安全和简洁的特征使得它成为了Rust中最为普遍使用的循环结构。在某些场景下,你需要某段代码执行固定的次数,大多数Rustaceans 会选择使用for
循环。这里我们还会用到Range
,它是Rust标准库提供的一个类型,能够产生有序的数字。
下面是我们通过for
循环实现倒计时的方式,这里有一个方法我们还没讲过:rev
,它会反转range中的元素:
fn main() {
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!!!");
}
这段代码是不是看上去比之前的倒计时程序还要好?
Summary 总结
搞定!这是一个内容较多的章节:你知道了什么是变量,什么是标量;你学会了如何使用一些复杂的数据类型、函数、注释、if
表达式和循环。如果你想练习下本章中讨论的概念,不妨尝试编写下下面的程序:
- 转化华氏温度和摄氏温度
- 产生一个费波纳茨数列
- 打印圣诞歌曲"The Twelve Days of Christmas,"的歌词,并尝试优化歌曲中循环的部分
如果你想继续深入学习,我们将在下一章中讨论一个在众多编程语言中Rust独有的概念:ownership(所有权)。