Q1ngying

今朝梦醒与君别,遥盼春风寄相思

0%

2.1Rust基本概念,变量绑定与解构

Rust 基本概念

在 Rust 中的一些概念:

  • 所有权、借用、生命周期
  • 宏编程
  • 模式匹配

下面,通过一点代码来简单浏览下Rust语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Rust 程序入口函数,main,该函数目前无返回值
fn main() {
// 使用 let 来声明变量,进行绑定 a 不可变
// 此处没有指定 a 的类型,编译器会默认根据 a 的值为 a 推测类型:i32 ,有符号 32 位整数
// 语句末尾必须以分号结尾
let a = 10;
// 主动指定 b 的类型为 i32
let b : i32 = 20;
// 这里有两点需要注意:
// 1. 可以在数值中带上类型:30i32 表示数组是 30,类型是 i32
// 2. c 是可变的,mut 是 mutable 的缩写。
let mut c = 30i32;
// 还能再数值和类型中间添加一个下划线,让可读性更好
let d = 30_i32;
// 和其他语言一样,可以使用一个函数的返回值来作为另一个函数的参数
let e = add(add(a, b), add(c, d));

// println! 是宏调用,看起来像是函数但是他返回的是宏定义的代码块
// 该函数将指定的格式化字符串输出到标准输出中(控制台)
// {} 是占位符,在具体执行过程中,会把 e 的值带入进来
println!{"(a + b) + (c + d) = {}", e};
}
// 定义一个函数,输入两个 i32 类型的 32 位有符号整数,返回它们的和
fn add(i : i32, j : i32) -> i32 {
// 返回相加值,这里可以省略 return
i + j
}

注意 在上面的 add 函数中,不要为 i+j 添加 ;,这会改变语法导致函数返回 () 而不是 i32,具体参见语句和表达式

有几点可以留意下:

  • 字符串使用双引号 "" 而不是单引号 '',Rust 中单引号是留给单个字符类型(char)使用的
  • Rust 使用 {} 来作为格式化输出占位符,其它语言可能使用的是 %s%d%p 等,由于 println! 会自动推导出具体的类型,因此无需手动指定

变量绑定与解构

为什么要手动设置变量的可变性?

声明可变的变量为编程提供了灵活性,声明不可变的变量为程序提供了安全性,而 Rust 中在底层二者同时存在。同时,还有一个优点,那就是运行性能上提升。将本身无需改变的变量声明为不可变在运行期会避免一些多余的 runtime 检查。

变量命名

命名方面,和其他语言没有区别,不过当给变量命名时,需要遵循 Rust 命名规范

Rust 语言有一些关键字keywords),和其他语言一样,这些关键字都是被保留给 Rust 语言使用的,因此,它们不能被用作变量或函数的名称。在 附录 A 中可找到关键字列表。

变量绑定

在其他语言中,我们使用 var a = "hello world"的方式为a赋值,也就是把等式右边的"hello world"字符串赋值给变量a,而在 Rust 中,我们这样写:let a = "hello world",同时给这个过程称作:变量绑定

使用绑定,是因为这涉及到了 Rust 最核心的原则——所有权,简单来说,任何内存对象都是有主人的,而且一般情况下完全属于它的主人,绑定就是把这个对象绑定给一个变量,让这个变量成为它的主人。在这种情况下,该对象之前的主人就会丧失对该对象的所有权

变量可变性

Rust 的变量在默认情况下是不可变的。这让我们的编写的代码更加安全,性能更好。我们也还可以使用mut关键字让变量变为可变的,让设计更灵活。

如果变量a不可变,那么一点为它绑定值,就不能再修改a

来自 Rust 圣经:

然后在新建的 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);
}

保存文件,再使用 cargo run 运行它,迎面而来的是一条错误提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: consider making this binding mutable: `mut x`
3 | println!("The value of x is: {}", x);
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable

error: aborting due to previous error

具体的错误原因是 cannot assign twice to immutable variable x(无法对不可变的变量进行重复赋值),因为我们想为不可变的 x 变量再次赋值。

这种错误是为了避免无法预期的错误发生在我们的变量上:一个变量往往被多处代码所使用,其中一部分代码假定该变量的值永远不会改变,而另外一部分代码却无情的改变了这个值,在实际开发过程中,这个错误是很难被发现的,特别是在多线程编程中。

这种规则让我们的代码变得非常清晰,只有你想让你的变量改变时,它才能改变,这样就不会造成心智上的负担,也给别人阅读代码带来便利。

但是可变性也非常重要,否则我们就要像 ClojureScript 那样,每次要改变,就要重新生成一个对象,在拥有大量对象的场景,性能会变得非常低下,内存拷贝的成本异常的高。

在 Rust 中,可变性很简单,只要在变量名前加一个 mut 即可, 而且这种显式的声明方式还会给后来人传达这样的信息:嗯,这个变量在后面代码部分会发生改变。

为了让变量声明为可变,将 src/main.rs 改为以下内容:

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
2
3
4
5
6
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6

选择可变还是不可变,更多的还是取决于你的使用场景,例如不可变可以带来安全性,但是丧失了灵活性和性能(如果你要改变,就要重新创建一个新的变量,这里涉及到内存对象的再分配)。而可变变量最大的好处就是使用上的灵活性和性能上的提升。

例如,在使用大型数据结构或者热点代码路径(被大量频繁调用)的情形下,在同一内存位置更新实例可能比复制并返回新分配的实例要更快。使用较小的数据结构时,通常创建新的实例并以更具函数式的风格来编写程序,可能会更容易理解,所以值得以较低的性能开销来确保代码清晰。

使用下划线开头忽略未使用的变量

如果创建了一个变量,却不在任何地方使用它,Rust 会给出一个警告,因为这可能是一个 BUG。但是有时创建一个不会被使用的变量是有用的,比如正在设计原型或刚刚开始一个项目。如果希望 Rust 不要警告未使用的变量,可以使用 下划线_ 作为变量名的开头

1
2
3
4
fn main() {
let _x = 5;
let y = 10;
}

使用 cargo run 运行下试试:

1
2
3
4
5
6
7
warning: unused variable: `y`
--> src/main.rs:3:9
|
3 | let y = 10;
| ^ help: 如果 y 故意不被使用,请添加一个下划线前缀: `_y`
|
= note: `#[warn(unused_variables)]` on by default

可以看到,两个变量都是只有声明,没有使用,但是编译器却独独给出了 y 未被使用的警告,充分说明了 _ 变量名前缀在这里发挥的作用。

值得注意的是,这里编译器还很善意的给出了提示( Rust 的编译器非常强大,这里的提示只是小意思 ): 将 y 修改 _y 即可。这里就不再给出代码,留给大家手动尝试并观察下运行结果。

更多关于 _x 的使用信息,请阅读后面的模式匹配章节

变量解构

let表达式不仅仅用于变量的绑定,还可以进行复杂变量的解构:从一个相对复杂的变量中,匹配出该变量的一部分内容:

1
2
3
4
5
6
7
8
fn main() {
let (a, mut b): (bool, bool) = (true, false);
// a = true, 不可变; b = false,可变
println!("a = {:?}, b = {:?}", a, b);

b = true;
assert_eq!(a, b);
}

解构式赋值

Rust 1.59 版本后,我们可以在赋值语句的左式中使用元组、切片和结构体模式了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Struct {
e : i32
}

fn main() {
let (a, b, c ,d, e);

(a, b) = (1, 2);
// _ 代表匹配一个值,但是我们不关心具体是什么,因此没有使用一个变量名而是使用了 _
[c, .., d, _] = [1, 2, 3, 4, 5];
Struct {e, ..} = Struct {e : 5};

assert_eq!([1], 2, 3, 4, 5], [a, b, c, d, e]);
}

这种使用方式跟之前的 let 保持了一致性,但是 let 会重新绑定,而这里仅仅是对之前绑定的变量进行再赋值。

需要注意的是,使用 += 的赋值语句还不支持解构式赋值。

变量和常量之间的差异

常量(constant)。与不可变变量一样,常量也是绑定到一个常量名且不允许更改的值,但是常量和变量之间存在一些差异:

  • 常量不允许使用mut常量不仅仅默认不可变,而且自始至终不可变关键字。因为常量在编译完成后,已经确定它的值
  • 常量使用const关键字而不是let关键字来声明,并且值的类型必须标注。

下面是一个常量声明的例子,其常量名为MAX_POINTS,值设置为100,000。(Rust 常量的命名约定是全部字母都是用大写,并使用下划线分割单词,另外对数字字面量可插入下划线以提高可读性):

1
const MAX_POINTS: u32 = 100_000;

常量可以在任意作用域内声明,包括全局作用域,在声明的作用域内,常量在程序运行的整个过程中都有效。对于需要在多处代码共享一个不可变的值时非常有用,例如游戏中允许玩家赚取的最大点数或光速。

在实际使用中,最好将程序中用到的硬编码值都声明为常量,对于代码后续的维护有莫大的帮助。如果将来需要更改硬编码的值,你也只需要在代码中更改一处即可。

变量遮蔽(shadowing)

Rust 允许声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let x = 5;
// 在 main 函数的作用域内对之前的 x 进行遮蔽
let x = x + 1;

{
// 在当前的花括号作用域内,对之前的 x 进行遮蔽
let x = x + 2;
println!("The value of x in the inner scope is : {}", x);
}

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

这个程序首先将数值 5 绑定到 x,然后通过重复使用 let x = 来遮蔽之前的 x,并取原来的值加上 1,所以 x 的值变成了 6。第三个 let 语句同样遮蔽前面的 x,取之前的值并乘上 2,得到的 x 最终值为 12。当运行此程序,将输出以下内容:

1
2
3
4
5
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
...
The value of x in the inner scope is: 12
The value of x is: 6

这和mut变量的使用不同,第二个let生成了完全不同的新变量,两个变量只是恰好拥有同样的名称,涉及一次内存对象的再分配,而**mut声明的变量,可以修改同一个内存地址上的值,并不会发生内存对象的再分配,性能更好。**

变量遮蔽的用处在于,如果你在某个作用域内无需再使用之前的变量(在被遮蔽后,无法在访问到之前的同名变量),就可以重复的使用变量名字,而不用绞尽脑汁去想名字。

例如,假如有一个程序要统计一个空格字符串的空格数量:

1
2
3
4
// 字符串类型
let spaces = " ";
// usize数值类型
let spaces = spaces.len();

这种结构是允许的,因为第一个spaces变量是一个字符串类型,第二个spaces变量是一个全新的变量且和第一个具有相同的变量名,且是一个数值类型。

所以变量遮蔽可以帮我们节省些脑细胞,不用去想如 spaces_strspaces_num 此类的变量名;相反我们可以重复使用更简单的 spaces 变量名。如果你不用 let :

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

运行一下,你就会发现编译器报错:

1
2
3
4
5
6
7
8
9
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`

error: aborting due to previous error

显然,Rust 对类型的要求很严格,不允许将整数类型 usize 赋值给字符串类型。usize 是一种 CPU 相关的整数类型,在数值类型中有详细介绍。