0%

Rust 权威指南(02):通用编程概念

接下来会介绍一些编程领域中常见的概念,以及它们在 Rust 中实现的方式。

关键字

Rust 也拥有一系列只能被用于语言本身的保留关键字,不能使用这些关键字来命名自定义的变量或函数。有一些关键字目前还没有任何功能,但是它们被预留给了未来可能会添加的功能来使用。

变量与可变性

Rust 中的变量默认是不可变的。Rust 语言提供这一概念是为了让你安全且方便地写出复杂的、甚至是并行的代码。当一个变量是不可变时,一旦它被绑定到某个值上面,这个值就再也无法被改变。Rust 的编译器能够保证那些声明为不可变的值一定不会发生改变。这意味着你无需在阅读和编写代码时追踪一个变量如何变化,从而使代码逻辑更加易于理解和推导。

变量默认是可变的,但是可以通过在声明变量的名称前添加 mut 关键字来使其可变:

1
2
3
4
5
6
7
fn main() {
let mut x = 5;
println!("The value of x is {}", x);

x = 6;
println!("The value of x is {}", x);
}

设计一个变量的可变性时需要考虑许多因素:

  • 当你使用某些重型数据结构时,适当地使用可变性去修改一个实例,可能比赋值和重新返回一个新的实例更有效率
  • 而数据结构较为轻量时,采用更偏向函数式的风格,通过创建新变量来进行赋值,可能会使代码更加易于理解。此时为了可读性而损失少许性能也许是值得的

变量与常量之间的不同

绑定到常量上的值也无法被其他代码修改,但是常量与变量之间还是存在一些细微差别:

  • 不能使用 mut 关键字来修饰常量。常量不仅默认是不可变的,而且总是不可变的
  • 需要使用 const 关键字而不是 let 关键字来声明一个常量,在声明的同时,必须显式地标注值的类型
  • 常量可以被声明在任何作用域中,包括全局作用域。当一个值需要被不同部分代码共同引用时十分有用
  • 只能将常量绑定到一个常量表达式中,而无法将一个函数的返回值、或者其他需要在运行时计算的值绑定到常量上

在 Rust 程序中,约定以下划线分隔的全大写字母来命名一个常量,并在数值中插入下划线来提高可读性。将整个程序中硬编码的值声明为常量并使用有意义的名字,可以提高代码的可读性。而且使用同一个常量来索引相同的硬编码值也可以为将来的修改提供方便。

1
2
3
4
fn main() {
const MAX_POINTS: u32 = 100_1000;
println!("{}", MAX_POINTS);
}

隐藏

在 Rust 中,一个新声明的变量可以覆盖掉旧的同名变量。这种现象称为 第一个变量 被 第二个变量 隐藏了。这也意味着我们随后使用该名称时,它指向的将会是第二个变量。可以重复使用 let 关键字并配以相同的名称来不断隐藏变量。

1
2
3
4
5
6
7
8
9
fn main() {
let x = 5;

let x = x + 1;

let x = x * 2;

println!("The value of x is {}", x);
}

隐藏机制不同于将一个变量声明为 mut。因为如果不是在使用 let 关键字的情况下重新为这个变量赋值,会导致编译错误。通过使用 let,可以对这个值执行一系列变化操作,并允许这个变量在操作完成后保持自己的不可变性。而且由于重复使用 let 关键字会创建出新的变量,所以可以在复用变量名称的同时改变它的类型。

1
2
3
4
5
6
fn main() {
let spaces = " ";
let spaces = spaces.len();

println!("The value of x is {}", spaces);
}

如果想使用 mut 来模拟类似效果,则会出现编译错误:

1
2
3
4
5
6
fn main() {
let mut spaces = " ";
spaces = spaces.len();

println!("The value of x is {}", spaces);
}
1
2
3
4
5
6
7
8
9
10
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ------ expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `hide` due to previous error

数据类型

Rust 中每一个值都有特定的数据类型。Rust 会根据数据的类型来决定应该如何处理它们。Rust 中的数据类型可以分为两种:

  • 标量类型(scalar)
  • 复合类型(compound)

Rust 是一门静态类型语言,这意味着它在编译程序的过程中需要知道所有变量的具体类型。编译器可以根据我们如何绑定、使用变量的值来自动推导出变量的类型。有时候编译器无法自动推导出变量的类型,为了避免混淆,这时就需要我们手动地添加类型标注。

标量类型

标量类型是单个值类型的统称。Rust 中内建了 4 种基础的标量类型:

  • 整数
  • 浮点数
  • 布尔值
  • 字符

整数类型

整数类型是指那些没有小数部分的数字。Rust 内建的整数类型,每个长度不同的值都有有符号、无符号两种变体。有符号数通过二进制补码的形式存储。

长度 有符号 无符号
8bit i8 u8
16bit i16 u16
32bit i32 u32
64it i64 u64
arch isize usize

isize 和 usize 取决于程序运行的目标平台,在 64 位架构上,它们就是 64 位的,而在 32 位架构上,它们就是 32 位的。

在书写整数字面量时,可以使用类型后缀,同时可以使用 _ 作为分隔符方便读数。同时可以使用 0x 前缀表示十六进制数、0o 前缀表示八进制数、0b 前缀表示二进制二进制数、b 前缀表示一个字节(此时不能使用类型后缀,永远都是 u8)。

Rust 对于整数字面量默认推导类型为 i32,当我们决定不了使用哪种整数类型时,也可以直接使用 i32,它在大部分情形下都是运算速度最快的那个。

浮点数类型

浮点数即为带小数的数字,两种浮点数类型为 f32 和 f64,分别占用 32 位和 64 位空间,对应着 IEEE-754 标准中的单精度浮点数、双精度浮点数。在 Rust 中,默认会将浮点数字面量推导为 f64。

对于所有的数值类型,Rust 支持常见的数学运算,包括:+、-、*、/、%。

布尔类型

布尔类型 bool 只有两个可能的值:true 和 false,它会占据单个字节空间大小。

字符类型

在 Rust 中,char 类型被用于描述语言中最基础的单个字符。char 类型字面量使用单引号指定,不同于字符串使用双引号指定。Rust 中的 char 类型占据 4 字节,是一个 Unicode 标量值,它可以表示比 ASCII 多得多的字符内容,包括中文等都是有效的 char 类型值。

复合类型

Rust 提供两种内置的基础符合类型:

  • 元组(tuple)
  • 数组(array)

元组

元组可以将不同类型的多个值组合进一个复合类型中。元组还拥有一个固定长度:你无法在声明结束后增加或减少其中的元素数量。为了创建元组,需要把一系列值用逗号分割后放置到一对圆括号中。元组每个位置的值都有一个类型,这些类型不需要是相同的。

1
2
3
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}

上面代码中的类型注解不是必须得。一个元组被视为一个单独的复合元素,为了从元组中获得单个值,可以使用模式匹配来解构元组:

1
2
3
4
5
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;
println!("x: {} y: {} z: {}", x, y, z);
}

除了解构,还可以通过索引并使用点号来访问元组中的值。元组的索引从 0 开始:

1
2
3
4
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
println!("x: {} y: {} z: {}", tup.0, tup.1, tup.2);
}

数组类型

我们同样可以在数组中存储多个值的集合。与元组不同,数组中的每一个元素都必须是相同类型。Rust 中的数组拥有固定的长度,一旦声明就再也不能随意更改大小。可以将以逗号分割的值放置在一对方括号中来创建一个数组。

通常而言,如果你想在栈上而不是堆上分配空间时,或者想要确保总有固定数量的元素时,数组时一个非常有用的工具。Rust 标准库也提供了一个更加灵活的动态数组(vector)类型,它允许用户自有地调整数组的长度。

如果想要写出数组的类型,需要使用一对方括号,并在方括号中填写数组内所有元素的类型、一个分号以及数组内元素的数量。

另外一种初始化数组的语法是,假设你想要创建一个包含相同元素值的数组,可以在方括号指定元素的值,并填入一个分号以及数组的长度。

1
2
3
4
5
6
7
fn main() {
let a = [1, 2, 3, 4, 5];
let b = [10; 5];
let c:[i32; 2] = [0, 1];

println!("{} {} {}", a[0], b[4], c[1]);
}

数组由一整块分配在栈上的内存组成,可以通过索引来访问一个数组中的所有元素。当访问非法数组索引时,不会产生编译错误,而是会产生运行时错误。每次通过索引访问数组元素时,Rust 都会检查该索引是否小于当前数组长度。如果索引超出了当前数组的长度,则会引发 panic。通过立即中断程序,而不是自作主张地运行下去,Rust 帮助我们尽早发现错误。

函数

Rust 代码使用蛇形命名法来作为规范函数和变量名称的风格:即只使用小写字母命名,并以下划线分隔单词。在 Rust 中,函数定义以 fn 关键字开始并紧随函数名称与一对圆括号,另外还有一对花括号用于标识函数体开始和结尾的地方。使用函数名加圆括号的方式来调用函数。Rust 并不关心你在何处定义函数,只要这些定义对于使用区域是可见的即可。

1
2
3
4
5
6
7
8
fn main() {
println!("Hello, world!");
another_function();
}

fn another_function() {
println!("Another function");
}

函数参数

可以在函数声明中定义参数(parameter),它们是一种特殊的变量,并且视为函数签名的一部分。当函数存在参数时,需要在调用函数时为这些变量提供具体的值(argument)。

1
2
3
4
5
6
7
fn main() {
another_function(5);
}

fn another_function(x: i32) {
println!("The value of x is {}", x);
}
  • 在函数签名中,必须显式地声明每个参数的类型。由于类型被显式注明,编译器不需要通过其他代码进行推导就能明确地知道你的意图
  • 使用逗号分隔符来为函数声明多个参数

函数体中的语句和表达式

函数体由若干条语句组成,并且可以以一个表达式作为结尾。Rust 是一门基于表达式的语言,所以它将语句与表达式区别为两个概念:

  • 语句指那些执行操作但不返回值的指令
  • 而表达式则是指会进行计算并产生一个值作为结果的指令

语句不会返回值,而表达式会计算出某个值作为结果:

  • 使用 let 关键字创建变量并绑定值时使用的指令是一条语句,因此不能将一条 let 语句赋值给另一个变量
  • Rust 中编写的大部分代码都是表达式:字面量是表达式、调用函数是表达式、调用宏是表达式、用来创建新作用域的花括号同样是表达式
  • 如果在表达式的结尾加上分号,代码就会变成语句而不会返回任何值

函数的返回值

函数可以向调用它的代码返回值,虽然不用为这个返回值命名,但需要在 -> 后面声明它的类型。在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。可以使用 return 关键字并指定一个值来提前从函数中返回,但大多数函数都隐式地返回了最后的表达式。

1
2
3
4
5
6
7
8
fn main() {
let x = five();
println!("The value of x is: {}", x);
}

fn five() -> i32 {
5
}

如果上面代码的 5 后面添加分号,则会产生编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# cargo build
Compiling returns v0.1.0 (/root/code/private/rust/chapter_03/returns)
error[E0308]: mismatched types
--> src/main.rs:6:14
|
6 | fn five() -> i32 {
| ---- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
7 | 5;
| - help: remove this semicolon to return this value

For more information about this error, try `rustc --explain E0308`.
error: could not compile `returns` due to previous error

由于语句不会产生值,此时 Rust 默认会返回一个空元组,和函数声明的返回类型不一致,因此产生编译错误。

注释

在 Rust 中,注释必须以 // 开始,并持续到本行结尾。对于超过一行的注释,需要每一行前面都使用 //。注释也可以放置在代码行的结尾处。Rust 编译器会忽略掉注释。

Rust 还有一种被称为文档注释的注释格式,将在后续文章介绍。

控制流

通过条件来执行或者重复执行某些代码是大部分编程语言的基础组成部分。在 Rust 中用来控制程序执行流的结构就是 if 表达式和循环表达式。

if 表达式

所有 if 表达式以 if 关键字来开头,并紧随一个判断条件。其后的花括号放置了条件为真时需要执行的代码片段(也称为分支)。if 表达式可以有可选的 else 表达式、else if 表达式。

1
2
3
4
5
6
7
8
9
fn main() {
let number = 3;

if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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");
}
}

条件表达式必须产生一个 bool 类型的值,否则会触发编译错误。Rust 不会自动尝试将非布尔类型的值转换为布尔类型。

由于 if 是一个表达式,所以可以在 let 语句的右侧使用它来生成一个值。这也意味着分支可能返回的值都必须是同一种类型。代码块输出的值就是最后一个表达式的值,另外数字本身也可以作为一个表达式使用。

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let condition = true;

let number = if condition {
5
} else {
6
};

println!("The value of number is: {}", number);
}

循环

Rust 提供了多种循环工具:

  • loop:使用 loop 关键字来指示 Rust 反复执行一段代码,直至显式地声明退出为止。在循环中可以使用 break 关键字来通知程序退出循环,需要从循环中返回的值可以添加到 break 表达式后面
  • while:while 通常用于实现条件循环,即每次执行循环之前都判断一次条件,假如条件为真则执行代码片段,如果条件为假或者在执行过程中碰到 break 就退出当前循环
  • for:使用 for 循环来遍历集合。for 循环的安全性和简捷性使它成为了 Rust 中最为常用的循环结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let mut counter = 0;

let result = loop {
counter += 1;

if counter == 10 {
break counter * 2;
}
};

println!("The result is {}", result)
}
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut number = 3;

while number != 0 {
println!("{}", number);

number -= 1;
}

println!("LIFTOFF!!")
}
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let a = [10, 20, 30, 40, 50];

for element in a.iter() {
println!("The value is: {}", element);
}

for number in (1..4).rev() {
println!("{}!", number);
}
}