第二章中提到过,变量默认是不可改变的(immutable)。
变量和可变性
当变量不可变时,一旦值被绑定一个名称上,你就不能改变这个值。
在尝试改变预设为不可变的值时,产生编译时错误是很重要的,因为这种情况可能导致 bug。如果一部分代码假设一个值永远也不会改变,而另一部分代码改变了这个值,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 的起因难以跟踪,尤其是第二部分代码只是 有时 会改变值。
Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要追踪一个值如何和在哪可能会被改变,从而使得代码易于推导。
不过可变性也是非常有用的。变量只是默认不可变;正如在第二章所做的那样,你可以在变量名之前加 mut 来使其可变。除了允许改变值之外,mut 向读者表明了其他代码将会改变这个变量值的意图。
除了防止出现 bug 外,还有很多地方需要权衡取舍。例如,使用大型数据结构时,适当地使用可变变量,可能比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用更偏向函数式的编程风格,可能会使代码更易理解,为可读性而牺牲性能或许是值得的。
变量和常量的区别
不允许改变值的变量,可能会使你想起另一个大部分编程语言都有的概念:常量(constants)。类似于不可变变量,常量是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。
- 不允许对常量使用 mut,常量不光默认不能变,它总是不能变。
- 声明常量使用 const 关键字而不是使用 let,并且 必须 注明值的类型。
- 常量可以在任何作用域中声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。
- 最后一个区别是,常量只能被设置为常量表达式,而不能是函数调用的结果,或任何其他只能在运行时计算出的值。
这是一个声明常量的例子,它的名称是 MAX_POINTS,值是 100,000。(Rust 常量的命名规范是使用下划线分隔的大写字母单词,并且可以在数字字面值中插入下划线来提升可读性):
const MAX_POINTS: u32 = 100_000;
在声明它的作用域之中,常量在整个程序生命周期中都有效,这使得常量可以作为多处代码使用的全局范围的值,例如一个游戏中所有玩家可以获取的最高分或者光速。
将遍布于应用程序中的硬编码值声明为常量,能帮助后来的代码维护人员了解值的意图。如果将来需要修改硬编码值,也只需修改汇聚于一处的硬编码值。
隐藏(Shadowing)
隐藏与将变量标记为 mut 是有区别的。当不小心尝试对变量重新赋值时,如果没有使用 let 关键字,就会导致编译时错误。通过使用 let,我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。
mut 与隐藏的另一个区别是,当再次使用 let 时,实际上创建了一个新变量,我们可以改变值的类型,但复用这个名字。
数据类型
在 Rust 中,每一个值都属于某一个 数据类型(data type),这告诉 Rust 它被指定为何种数据,以便明确数据处理方式。我们将看到两类数据类型子集:标量(scalar)和复合(compound)。
Rust 是 静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。
标量类型
标量(scalar)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。
整型
浮点型
布尔型
字符类型
复合类型
复合类型(Compound types)可以将多个值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。
元组类型
元组是一个将多个其他类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,其长度不会增大或缩小。
我们使用包含在圆括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。这个例子中使用了可选的类型注解:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
数组类型
另一个包含多个值的方式是 数组(array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。
Rust 中,数组中的值位于中括号内的逗号分隔的列表中:
fn main() {
let a = [1, 2, 3, 4, 5];
}
当你想要在栈(stack)而不是在堆(heap)上为数据分配空间(第四章将讨论栈与堆的更多内容),或者是想要确保总是有固定数量的元素时,数组非常有用。但是数组并不如 vector 类型灵活。vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector。第八章会详细讨论 vector。
函数如何工作
Rust使用 fn
关键字来声明新函数。
Rust 代码中的函数和变量名使用 snake case 规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。这是一个包含函数定义示例的程序:
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
Rust 中的函数定义以 fn 开始并在函数名后跟一对圆括号。大括号告诉编译器哪里是函数体的开始和结尾。
源码中 another_function 定义在 main 函数 之后;也可以定义在之前。
Rust 不关心函数定义于何处,只要定义了就行。
函数参数
函数也可以被定义为拥有 参数(parameters),参数是特殊变量,是函数签名的一部分。
当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。
parameter:形参,函数的参数
argument:实参,调用函数时传入的具体值
下面被重写的 another_function 版本展示了 Rust 中参数是什么样的:
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {}", x);
}
在函数签名中,必须 声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解,意味着编译器不需要你在代码的其他地方注明类型来指出你的意图。
包含语句和表达式的函数体
函数体由一系列的语句和一个可选的结尾表达式构成。目前为止,我们只介绍了没有结尾表达式的函数,不过你已经见过作为语句一部分的表达式。因为 Rust 是一门基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及这些区别是如何影响函数体的。
语句(Statements)是执行一些操作但不返回值的指令。
表达式(Expressions)计算并产生一个值。
语句
语句不返回值。
因此,不能把 let 语句赋值给另一个变量,比如下面的例子尝试做的,会产生一个错误:
fn main() {
let x = (let y = 6);
}
let y = 6 语句并不返回值,所以没有可以绑定到 x 上的值。这与其他语言不同,例如 C 和 Ruby,它们的赋值语句会返回所赋的值。在这些语言中,可以这么写 x = y = 6,这样 x 和 y 的值都是 6;Rust 中不能这样写。
表达式
表达式会计算出一些值,并且你将编写的大部分 Rust 代码是由表达式组成的。
-
考虑一个简单的数学运算,比如 5 + 6,这是一个表达式并计算出值 11。
-
表达式可以是语句的一部分:在示例 3-1 中,语句 let y = 6; 中的 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,与你见过的大部分代码行不同。
表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。
具有返回值的函数
函数可以向调用它的代码返回值。
我们并不对返回值命名,但要在箭头(->)后声明它的类型。
在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。
使用 return 关键字和指定值,可从函数中提前返回;但大部分函数隐式的返回最后的表达式。
这是一个有返回值的函数的例子:
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {}", x);
}
在 five 函数中没有函数调用、宏、甚至没有 let 语句——只有数字 5。
这在 Rust 中是一个完全有效的函数。注意,也指定了函数返回值的类型,就是 -> i32。
让我们看看另一个例子:
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 代码中最常见的用来控制执行流的结构是 if 表达式和循环。
if 表达式
if 表达式允许根据条件执行不同的代码分支。你提供一个条件并表示 “如果条件满足,运行这段代码;如果条件不满足,不运行这段代码。”
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
所有的 if 表达式都以 if 关键字开头,其后跟一个条件。在这个例子中,条件检查变量 number 的值是否小于 5。
条件 必须 是 bool 值。如果条件不是 bool 值,我们将得到一个错误。
在 let 语句中使用 if
因为 if 是一个表达式,我们可以在 let 语句的右侧使用它,例如在示例 3-2 中:
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
println!("The value of number is: {}", number);
}
示例 3-2:将 if 表达式的返回值赋给一个变量
number 变量将会绑定到表示 if 表达式结果的值上。
记住,代码块的值是其最后一个表达式的值,而数字本身就是一个表达式。在这个例子中,整个 if 表达式的值取决于哪个代码块被执行。这意味着 if 的每个分支的可能的返回值都必须是相同类型;
使用循环重复执行
Rust 有三种循环:loop、while 和 for。我们每一个都试试。
使用 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 的变量并初始化为 0。
接着声明了一个名为 result 来存放循环的返回值。
在循环的每一次迭代中,我们将 counter 变量加 1,接着检查计数是否等于 10。
当相等时,使用 break 关键字返回值 counter * 2。
循环之后,我们通过分号结束赋值给 result 的语句。
最后打印出 result 的值,也就是 20。
while 条件循环
在程序中计算循环的条件也很常见。当条件为真,执行循环。当条件不再为真,调用 break 停止循环。
Rust 为此内置了一个语言结构,它被称为 while 循环。
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number = number - 1;
}
println!("LIFTOFF!!!");
}
这种结构消除了很多使用 loop、if、else 和 break 时所必须的嵌套,这样更加清晰。当条件为真就执行,否则退出循环。
使用 for 遍历集合
作为更简洁的替代方案,可以使用 for 循环来对一个集合的每个元素执行一些代码。
更为重要的是,我们增强了代码安全性,并消除了可能由于超出数组的结尾或遍历长度不够而缺少一些元素而导致的 bug。
for 循环的安全性和简洁性使得它成为 Rust 中使用最多的循环结构。
即使是在想要循环执行代码特定次数时,例如示例 3-3 中使用 while 循环的倒计时例子,大部分 Rustacean 也会使用 for 循环。这么做的方式是使用 Range,它是标准库提供的类型,用来生成从一个数字开始到另一个数字之前结束的所有数字的序列。
下面是一个使用 for 循环来倒计时的例子,它还使用了一个我们还未讲到的方法,rev,用来反转 range:
fn main() {
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!!!");
}