The Rust programming language 读书笔记——通用编程概念

一、变量

Rust 中的变量默认是不可变的

可以通过如下代码测试变量的不可变性:

  • 使用 cargo new variables 命令创建新的 Rust 项目
  • 进入新创建的 variables 目录,编辑 src/main.rs 源代码文件
1
2
3
4
5
6
fn main() {
let x = 10;
println!("The value is {}", x);
x = 20;
println!("The value is {}", x);
}

运行 cargo run 命令编译并执行 Rust 程序,出现如下报错:

1
error[E0384]: cannot assign twice to immutable variable `x`

即 Rust 编译器不允许程序代码对不可变变量进行二次赋值。

可以通过 let mut 关键字声明可变变量

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

1
2
3
4
5
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target/debug/variables`
The value is 10
The value is 20

PS:在使用重型数据结构时,适当地使用可变性去修改一个实例,可能比重新返回一个新分配的实例更有效率;而数据结构更为轻量时,采用偏函数式的风格创建新变量来进行赋值,可能会使代码更易于理解。

常量
  • 使用 const 而不是 let 关键字声明常量
  • 声明常量时必须显式地标注值的类型
  • 常量可以被声明在任何作用域中。在一个值需要被不同部分的代码共同引用时很有用处
  • 无法将一个函数的返回值或其他需要在运行时计算的值绑定到常量上

const PI: f32 = 3.1415;

Shadow

Shadow 的意思是,一个新声明的变量可以覆盖掉旧的同名变量

1
2
3
4
5
6
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("The value is {}", x);
}
1
2
3
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/variables`
The value is 12

Shadow 机制允许在复用变量名称的同时改变变量的类型。比如下面的代码就是合法的:

1
2
let spaces = "     ";
let sapces = spaces.len();

通过复用 spaces 这个名字,就不需要再声明诸如 spaces_strspaces_num 之类的变量。

但如果使用 mut 关键字来模拟上述效果就会报错。

1
2
let mut spaces = "     ";
spaces = spaces.len();

因为编译器拒绝修改变量的类型,即便该变量是可变的。
Shadow 的机制在于使用 let 关键字重新声明了变量。

二、数据类型

Rust 是一门静态类型语言,在编译过程中需要知道所有变量的具体类型。

大部分情况下,编译器可以自动推导出变量类型。但比如需要将 String 类型转换为数值类型时,就必须显式地添加类型标注:
let guess: u32 = "42".parse().expect("Not a number");

标量(Scalar)类型

标量类型是单个值类型的统称。Rust 内置 4 种基础的标量类型:整数、浮点数、布尔值和字符。

整数类型

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

有无符号代表了一个整数类型是否包含负数。即有符号的整数总是需要一个 bit 表示当前数值是否为正。
对于一个 n bit 的有符号整数,其取值范围是 - 2 ^ (n - 1)2 ^ (n - 1) - 1;长度为 n bit 的无符号整数,其取值范围则是 02 ^ n - 1

浮点数类型
包含 f32f64 两种类型。Rust 默认会将未标注类型的浮点数推导为 f64。整数会推导为 i32

字符类型
Rust 中,char 类型使用单引号指定,字符串类型使用双引号指定。

char 类型占用 4 个字节,是一个 Unicode 标量值。
let smile = '😀';

复合类型

复合类型可以将多个不同类型的值组合成一个类型。Rust 内置两种复合类型,元组和数组。

元组
  • 元组每个位置上的值都有一个类型
  • 元组拥有固定的长度。无法在声明结束后增加或减少其中的元素。
1
2
3
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}

为了从元组中获取单个值,可以使用模式匹配解构(拆包)元组。

1
2
3
4
5
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
print!("The value of y is: {}", y);
}

还可以通过索引使用点号访问元组中的元素。

1
2
3
4
5
6
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;
}

数组
  • 数组中的所有元素都必须是相同的类型
  • 数组拥有固定的长度,一旦声明就无法更改大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
}

数组的类型标注:let a: [i32; 5] = [1, 2, 3, 4, 5];

初始化含有相同元素的数组:let a = [3; 5];
等价于 let a = [3, 3, 3, 3, 3];

数组由一整块分配在栈上的内存组成,可以通过索引访问数组中的元素。

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

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

三、函数

必须显式地声明每个参数(如果有)的数据类型

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

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

Rust 是一门基于表达式的语言。它将语句(statement)表达式(expression)区分为两个不同的概念。

如使用 let 关键字创建变量并绑定值时的指令就是一条语句:
let y = 6;
语句不会返回值

在 C 或 Ruby 语言里,赋值语句会返回所赋的值。因此可以使用 x = y =6 这样的语句,但 Rust 不支持这样的语法。

表达式会计算出某个值作为结果返回
5 + 6 就是表达式(返回 11),let y = 6 中的数字 6 也是表达式(返回 6 本身)。
表达式本身可以作为语句的一部分。
用来创建新作用域的大括号也是表达式

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

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

在上述代码中,let y = 后面大括号部分的内容就是一个表达式,它会将计算出的结果 4 作为返回值。该返回值接着通过赋值语句绑定给变量 y
结尾处的 x + 1 并没有添加分号。若加上分号,则这段代码就变成了语句而不会返回任何值。

函数可以向调用它的代码返回值。但需要在箭头后面声明值的类型
可以使用 return 关键字指定一个值提前从函数中返回,但大多数函数都隐式地返回了最后的表达式

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

fn plus_one(x: i32) -> i32 {
x + 1
}

上述代码会输出 6。但如果在 plus_one 函数末尾的 x + 1 处加上分号,该表达式就会变成语句(不返回任何值),最终在编译时报出 mismatched types 错误。
原因是 plus_one 的声明中指定返回值类型为 i32,但由于语句不返回任何值,Rust 默认返回了一个空元组(()),导致实际的返回值类型与函数定义产生了冲突。

四、控制流

if 表达式

if 表达式必须产生一个 bool 类型的值,否则会触发编译错误

1
2
3
4
5
6
fn main() {
let number = 3;
if number {
print!("number was three");
}
}

在上面的代码中,if 表达式的计算结果为 3,而 Rust 期望获得一个 bool 值,因此编译时会爆出 mismatched types 错误。
Rust 不会自动尝试将非布尔值转换为布尔类型

if 是一个表达式。可以在 let 语句右侧使用 if 表达式来完成赋值。

1
2
3
4
5
6
7
8
9
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
print!("The value of number is {}", number);
}

整个 if 表达式的值取决于具体哪一个代码块得到了执行。因此,所有 if 分支可能返回的值都必须是同一种类型的。否则会触发编译错误。

1
2
3
4
5
6
7
8
9
fn main() {
let condition = true;
let number = if condition {
5
} else {
"six" // 错误,类型不匹配
};
print!("The value of number is {}", number);
}

循环

Rust 提供了 3 种循环:loopwhilefor

从 loop 循环中返回值

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

let result = loop {
counter += 1;

if counter == 10 {
break counter * 2;
}
};
print!("The result is {}", result);
}

break 关键字中断循环并返回 counter * 2

while 循环

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

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

number = number - 1;
}

println!("LIFTOFF!!!");
}

for 循环遍历集合中的元素

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

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

for 循环重构前面使用 while 循环的代码

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

参考资料

The Rust Programming Language