通用编程概念

本文大部分内容翻译自:The Rust Programming Language

本文涵盖了几乎每种编程语言中都会出现的概念,以及它们在Rust中的工作原理。

许多编程语言的核心有很多共同点,本章中介绍的概念都不是Rust独有的,但我们将在Rust的背景下讨论它们,并解释使用这些概念的惯例。

1. 变量和可变性(Variables and Mutability)

默认情况下,变量是不可变的

这是Rust可以提供安全、简单的并发性来编写代码的原因之一,但我们仍然可以选择使变量可变。

下面会介绍Rust如何以及为什么鼓励你支持不变性(immutability),以及为什么有时你可能不去使用这个特性。

1.1. 变量的不变性

当变量不可变时,一旦它被绑定了一个值,该值就再也不能被更改。

创建variables工程:

1
cargo new variables

然后将src/main.rs改为如下内容:

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

然后执行:

1
cargo run

执行结果:
变量不可变

错误消息表明,导致错误的原因是“cannot assign twice to immutable variable”,即不能为不可变变量x赋值两次,这是因为程序中试图为不可变的x变量分配第二个值。

在我们试图更改被指定为不可变的值时,编译时报错(compile-time errors)是很重要的,因为这种情况可能会导致错误。

如果我们代码的一部分假设一个值永远不会改变,而另一部分却会改变该值,那么代码的第一部分可能不会得到它期望的结果。这种错误的原因在事后可能很难追踪,特别是当第二段代码只是偶尔更改这个值的时候。Rust编译器保证,当我们声明一个值不会改变,它就真的不会改变,我们不需要自己去跟踪它,代码也更容易推理。

1.2. 将变量变为可变

但可变性也非常有用。变量在默认情况下是不可变的,但可以通过在变量名前面添加mut来使其可变。添加mut向阅读代码的人传达了一个信息:代码的其他部分可能会修改这个变量的值。

将上面代码改为如下内容:

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

运行结果:
将变量变为可变

1.3. 常量(Constants)

与不可变变量一样,常量是绑定到名称且不允许更改的值,但常量和变量之间有一些差异:

  • 对常量不能使用mut。默认情况下,常量不仅不可变,而且总是不可变的。用const关键字而不是let关键字声明常量,并且必须标注类型
  • 常量可以在任何作用域中声明,包括全局作用域,这样程序的各个部分都可以使用它们
  • 常量只能被赋值为常量表达式,不能被赋值为那些只能在运行时计算出结果的值

常量声明举例:

1
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

常量的名称是THREE_HOURS_IN_SECONDS,它的值被设为60 * 60 * 3。Rust对常量的命名管理是全部使用大写字母,单词之间用下划线_连接。

编译器能够在编译时评估一组有限的操作,让我们能够选择以更容易理解和验证的方式写出此值,而不是将该常量设置为值10,800

常量在程序运行期间,在声明范围内有效。将程序中使用的硬编码值命名为常量,有利于将该值的含义传达给代码的未来维护者。并且,如果将来需要更新硬编码值,代码中只有一个位置需要更改。

1.4. Shadowing

Rust允许声明一个与上一个变量同名的新变量。第一个变量被第二个变量屏蔽(shadow),这意味着编译器在使用变量名称时,看到的是第二个变量。实际上,第二个变量覆盖了第一个变量,任何使用该变量名的操作都会作用于第二个变量,直到它自己也被屏蔽,或者离开作用域。我们可以通过使用同一变量的名称并重复使用let关键字来屏蔽变量,如下所示:

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

let x = x + 1;

{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}

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

这段程序首先将x绑定到5。然后通过重复let x =创建一个新的变量x,取原始值并加1,这样x的值就是6。然后,在用花括号创建的内部作用域中,第三个let语句也会屏蔽x并创建一个新变量,将上一个值乘以2,使x的值为12。当离开作用域时,内部屏蔽结束,x返回为6。

运行结果:
shadowing

Shadowing与mut的不同之处:

  • 屏蔽与将变量标记为mut不同,因为如果我们不小心在不使用let关键字的情况下重新给这个变量赋值,我们将收到编译错误(compile-time error)。通过使用let,我们可以变量做一些转换,但在这些转换完成后,变量依旧是不可变的(immutable)

  • mut不能更改变量类型。当我们再次使用let关键字时,我们正在创建一个新变量,可以更改变量的类型,但重用相同的名称。但mut不允许更改变量类型。例如:

    1
    2
    3
    4
    let spaces = "abc";
    println!("{}", spaces);
    let spaces = spaces.len(); // 允许
    println!("{}", spaces);

    运行结果:
    shadow可以改变变量类型

    1
    2
    let mut spaces = "  ";
    spaces = spaces.len(); // 不允许

    程序报错:
    mut不能改变变量类型

2. 数据类型(Data Types)

Rust中的每个值都有其所属类型,这样Rust才知道如何处理这些数据。

Rust是一种静态类型语言,这意味着它必须在编译时知道所有变量的类型。编译器通常可以根据变量被赋予的值和变量被使用的方式来推断数据的类型。在类型有多种可能的情况下,必须加上类型注释(type annotation),如:

1
let guess: u32 = "42".parse().expect("Not a number!");

如果不使用: u32类型注释,Rust就会显示下面的错误,意思是编译器需要更多的信息才能知道我们想要使用哪种类型:
类型注释(type annotation)

2.1. 标量类型(Scalar Types)

标量类型表示单个值。Rust有四种主要标量类型:整数、浮点数、布尔值和字符。

2.1.1. 整数类型(Integer Types)

整数是没有小数部分的数字。例如u32类型,表示与之关联的值应该是占用32位空间的无符号整数(有符号整数类型以i开头,而不是u)。下表显示了Rust中的内置整数类型,我们可以使用其中任何一个来声明整数值的类型。

Length Signed Decimal Range Unsigned Decimal Range
8-bit i8 $[-2^{7},2^{7}-1]$=[-128,127] u8 [0,255]
16-bit i16 $[-2^{15},2^{15}-1]$=[-65536,65535] u16 [0,$2^{16}-1$]
32-bit i32 $[-2^{31},2^{31}-1]$ u32 [0,$2^{32}-1$]
64-bit i64 [$-2^{63}, 2^{63} - 1$] u64 [0,$2^{64}-1$]
128-bit i128 [$-2^{127},2^{127} - 1$] u128 [0,$2^{128}-1$]
arch isize usize

每个成员都可以有符号或无符号,并具有显式大小。有符号和无符号是指数字是否可能是负数。有符号的数字使用二进制补码(two’s complement)来存储。


二进制补码(two’s complement):
正数使用原码,负数用其相反数的二进制取反加1表示。

例如,若要用二进制补码表示-3

  • 求其相反数 => 3
  • 求上面结果的二进制表示 => 0011
  • 将上面结果取反 => 1100
  • 将上面结果加1 => 1101

那么1101就是-3的二进制补码(two’s complement)


每个有符号变量可以存储从$[-2^{n-1},2^{n-1}-1]$的数字,其中$n$是变量的位数;无符号成员可以存储$[0,2^{n}-1]$的数字。

此外,isizeusize类型取决于正在运行本程序的计算机的架构,该架构在表中表示为“arch”:如果是64位架构,则为64位;如果使用的是32位架构,则为32位。

可以使用下表的任何形式表示整数。当数字有可能是多种数字类型时,允许用类型后缀来指定类型,如57u8。还可以使用_作为可视化分隔符,使数字更易于阅读,例如1_000,其值与1000相同。

Number literals Example
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte (u8 only) b'A'

Rust默认使用的整数类型是i32。使用isizeusize的主要用于对集合进行索引。

2.1.2. 浮点类型(Floating-Point Types)

Rust还有两种浮点数的原始类型,即具有小数点的数字。

Rust的浮点类型是f32(单精度)和f64(双精度),大小分别为32位和64位。默认类型是f64,因为在现代CPU上,它的速度与f32大致相同,但能够提高精度。所有浮点类型都是有符号的。

举例:

1
2
3
4
5
fn main() {
let x = 2.0; // f64

let y: f32 = 3.0; // f32
}

Rust支持所有数字类型的基本数学运算:加法、减法、乘法、除法和余数。

整数除法四舍五入到最近的整数。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// addition
let sum = 5 + 10;

// subtraction
let difference = 95.5 - 4.3;

// multiplication
let product = 4 * 30;

// division
let quotient = 56.7 / 32.2;
let floored = 2 / 3; // Results in 0

// remainder
let remainder = 43 % 5;
}

2.1.3. 布尔类型(Boolean Type)

与大多数其他编程语言一样,Rust中的布尔类型有两个可能的值:truefalse

布尔值占一个字节,使用bool来指定。

举例:

1
2
3
4
5
fn main() {
let t = true;

let f: bool = false; // with explicit type annotation
}

主要在条件语句中使用布尔值,例如if表达式。

2.1.4. 字符类型(Character Type)

Rust的字符类型是最原始的字母类型。

举例:

1
2
3
4
5
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}

用单引号'指定字符类型(char)。Rust的字符类型大小为4个字节,表示Unicode标量值,这意味着它可以表示的不仅仅是ASCII。重音字母中文日语韩语字符表情符号零宽度空格都是Rust中有效的字符值。Unicode标量值从U+0000U+D7FFU+E000U+10FFFF

2.2. 复合类型(Compound Types)

复合类型可以将多个值组合成一种类型。Rust有两种原始复合类型:元组(Tuple)和数组(Array)。

2.2.1. 元组类型(Tuple Type)

元组是一种将具有各种类型的多个值组合成一种复合类型的通用方法。元组具有固定的长度,一旦声明,它们的大小就无法改变。

创建元组的方法:在括号()内写一系列以逗号分隔的值。

元组中的每个元素都有一个类型,不同元素的类型不一定相同。

举例:

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

tup变量与整个元组绑定,想要得到元组中元素的值,可以用模式匹配的方法。例如:

1
2
3
4
5
6
7
fn main() {
let tup = (500, 6.4, 1);

let (x, y, z) = tup;

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

这段程序首先创建一个元组,并将其绑定到变量tup。然后使用一个let模式获取tup,将其转换为三个独立的变量,xyz。这种方法被称为解构(destructing),因为它将单个元组分为三部分。最后,程序打印出y的值,即6.4

我们也可以使用句点.加我们要访问的值的索引来访问元组元素。例如:

1
2
3
4
5
6
7
8
9
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);

let five_hundred = x.0;

let six_point_four = x.1;

let one = x.2;
}

不含任何值的元组有一个特殊名称,unit。此值及其相应类型都写作(),表示空值或空返回类型。如果表达式不返回任何其他值,则隐式返回unit值。

2.2.2. 数组类型(Array Type)

获得一个拥有多个值的集合的另一个方法是使用数组(Array)。与元组不同,数组的每个元素都必须具有相同的类型。与其他语言中的数组不同,Rust中的数组具有固定的长度

2.2.2.1. 声明数组

我们将数组中的值写成方括号[]内以逗号分隔的列表:

1
2
3
fn main() {
let a = [1, 2, 3, 4, 5];
}

当希望将数据分配到栈(stack)而不是堆(heap)上时,或者想确保我们始终拥有固定数量的元素时,数组非常有用。然而,数组不像向量类型(vector)那么灵活。向量是标准库提供的类似集合类型,它的大小可以增长或缩小。如果不确定是使用数组还是向量,很可能应该使用向量。

然而,当元素数量不需要更改时,数组会更有用。例如,如果想要在程序中使用月份的名称,可能会使用数组而不是向量,因为我们知道它总是包含12个元素:

1
2
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];

数组的类型表示:
用方括号来声明数组的类型,包括每个元素的类型,分号;,数组中的元素数量:

1
let a: [i32; 5] = [1, 2, 3, 4, 5];

这里,i32是每个元素的类型。在分号之后,数字5表示数组包含5个元素。

还可以通过初始值; 数组长度来初始化数组,这样数组中的每个元素都有相同的值:

1
let a = [3; 5];

名为a的数组包含5个元素,这些元素初始值被设为3。这与let a = [3, 3, 3, 3, 3]一样,但更简洁。

2.2.2.2. 访问数组中的元素

数组是已知的、大小固定的、可在栈上分配的单个内存块。可以使用索引来访问数组的元素:

1
2
3
4
5
6
fn main() {
let a = [1, 2, 3, 4, 5];

let first = a[0];
let second = a[1];
}

变量first将被赋值为1,因为这是数组中索引[0]的值。变量second将被数组的索引[1]赋值为2

2.2.2.3. 越界访问

越界访问(访问的索引值大于等于数组长度)会造成panic:

1
2
3
4
5
6
fn main() {
let a = [1, 2, 3, 4, 5];

println!("The value of the element at index {} is: {}", 1, a[1]);
println!("The value of the element at index {} is: {}", 5, a[5]);
}

cargo build结果:
越界访问

3. 函数(Functions)

函数(Functions)在Rust代码中很普遍。Rust中最重要的函数之一:主函数(main function)。这是许多程序的切入点。

fn关键字来声明新函数,后面跟着函数名和一组花括号{}。花括号是用来告诉编译器,函数的主体开始或结束了。

Rust使用Snake case作为函数和变量名的惯例样式,所有字母都是小写字母,用下划线_将单词分开:

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

another_function();
}

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

another_function()可以定义在main()之后,也可以定义在maint()之前,都可以被main()调用。

3.1. Parameters

带参数的函数示例:

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

fn another_function(x: i32) {
println!("The value of x is: {x}");
}

another_function有一个参数x,类型为i32

在函数签名中,必须声明每个参数的类型

多个参数示例:

1
2
3
4
5
6
7
fn main() {
print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}

3.2. Statements和Expressions

3.2.1. Statements

Statements没有返回值。例如:

1
2
3
fn main() {
let y = 6; // this is a statement
}

创建变量并给它赋值是statement,函数定义是statement,上面的整段代码都是statements。

由于statement不返回任何值,所以不能将一个statement赋值给一个变量,如:

1
2
3
fn main() {
let x = (let y = 6);
}

这段代码会有编译错误,因为它将一个let statement赋值给了变量x

3.2.2. Expressions

表达式(Expressions)可以计算出一个值,并且末尾没有分号;。例如5 + 6,这是一个计算结果为值11的表达式。

expression可以是statement的一部分,比如let y = 6;中,6是expression。函数调用是expression,调用宏(macro)是expression,使用花括号创建的新的代码块是expression。

举例:

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

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

这个expression:

1
2
3
4
{
let x = 3;
x + 1
}

是一个计算结果为4的代码块,这个结果会被绑定到变量y,成为let statement的一部分。

expressions的末尾不能加分号;,否则就会被转换为statement,就不能返回值了。

3.3. 有返回值的函数

返回值的类型必须声明在->之后。可以使用return关键字并指定返回值来从函数返回,但大多数函数都隐式返回最后一个表达式。

例1:

1
2
3
4
5
6
7
8
9
fn five() -> i32 {
5
}

fn main() {
let x = five();

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

例2:

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

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

fn plus_one(x: i32) -> i32 {
x + 1 // 注意后面没有分号,否则就变成statement了,会造成编译错误
}

如果x + 1后面加了分号,变成x + 1;,会出现“mismatched types”错误。因为函数plus_one的定义说,它将返回一个类型为i32的值,但statement并不会计算出一个值,而是由单位类型(unit type)()表示。因此,plus_one没有返回任何东西,这与它的定义相矛盾,因此导致错误。

4. 注释(Comments)

举例:

1
2
3
// 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.

5. 控制流(Control Flow)

5.1. if表达式

5.1.1. 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");
}
}

if后跟的条件必须是bool类型,否则程序会报错。

5.1.2. else if

举例:

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");
}
}

5.1.3. 在let语句中使用if

举例:

1
2
3
4
5
6
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };

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

注意,这里ifelse后面的值必须是同一种类型,否则会报错。如:

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

let number = if condition { 5 } else { "six" };

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

ifelse后面的值不兼容,会出现编译错误。

5.2. loop

5.2.1. 无限循环

举例:

1
2
3
4
5
fn main() {
loop {
println!("again!");
}
}

跳出循环的方法:break关键词。

5.2.2. 返回结果

还可以使用loop来返回结果:

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}");
}

result变量用来保存loop返回的结果,上面例子中,返回的是counter * 2,即20。

5.2.3. loop labels:跳出label指定的loop

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;

loop {
println!("remaining = {remaining}");
if remaining == 9 {
break; // exit inner loop only
}
if count == 2 {
break 'counting_up; // exit the outer loop
}
remaining -= 1;
}

count += 1;
}
println!("End count = {count}");
}

默认情况下,breakcontinue作用于他们所在的loop。loop label以单引号'开始,使用loop label后,可用于跳出label指定的loop。

5.3. while

举例:

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!!!");
}

5.4. for

举例:

1
2
3
4
5
6
7
fn main() {
let a = [10, 20, 30, 40, 50];

for element in a {
println!("the value is: {element}");
}
}

使用Range

1
2
3
4
5
6
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}

rev()用于reverse the range。

参考资料

[1] Common Programming Concepts:https://doc.rust-lang.org/stable/book/ch03-00-common-programming-concepts.html