开始

先用一段代码来了解下 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
28
// 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
}

变量绑定

既然是前端学 rust ,咱们就不讨论什么是变量了,直接进入正题吧。rust 中使用 let 关键字来定义变量,let x = "hello world" 这个过程,在 JS 中我们我们称之为赋值,而在 rust 中,我们可以更准确地称之为: 变量绑定

简而言之,在 rust 中,所有内存对象都是有主人的,一个对象同时只能有一个主人,例如 let x = 5 表示变量 x 成为了 5 这块内存对象的主人,这块内存对象绑定给了 xx 对他存在 所有权。也就是说,rust 更加强调将 变量值(对象) 关联起来,而不像 JS 那样,一个对象可能会有多个变量对应它。

关于所有权概念,我们之后再详细讨论,现在只需要知道什么是 变量绑定 即可。

可变变量与不可变变量

在我们的 JavaScript 中,使用 varlet 声明变量

1
2
let x = 5;
let y = 8

xy 都是可变的,在之后可以随意赋值甚至更改类型,这也为 JS 开发提供了灵活性,当然也会埋下隐患或者是导致一些难以发现的 bug,所以实际开发中我们会使用很多的类型检查和数据校验来确保程序的健壮性,或者是使用 ts

然而在 rust 中,使用 let 定义的变量默认是不可变的:

1
2
3
4
let x = 5;
println!("x={}", x);
x = 6;
println!("x={}", x);

这段代码会抛出一个 error[E0384]: cannot assign twice to immutable variable x ,无法对不可变的变量再次赋值。
如果需要变量可变,需要在定义时显式指定 mut 关键字。

1
2
3
4
5
6
let mut x = 5;
println!("x={}", x);
x = 6;
println!("x={}", x);
// x=5
// x=6

为什么要这样设计呢?

试想在我们的开发中,一个变量被多处代码引用,一部分代码认为变量是不变的,然而另外一部分代码却修改了这个变量,这样的场景在大型项目中是很常见的。 而在 rust 中,我们开发时确定一个变量需要改变,我们才让它可变,不仅能降低心智负担,也能让代码变得清晰。

不可变变量如果需要改变,需要重新定义变量,绑定新的值,也就是会造成新的内存分配,这会带来一定的性能损失:

1
2
3
4
5
6
let x = 5;
println!("x={}", x);
let x = 6;
println!("x={}", x);
// x=5
// x=6

如果大量的不可变变量频繁改变,性能就会异常的低下了。

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

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

另外在前端开发中,很少遇到多线程常见的 数据竞争 问题,因为事件循环机制, JS 的代码执行是单线程的。当然在 JSWeb Workersnode.JSWorker threads 出现之后,咱们以后可能也要考虑多线程问题了😭。Rust 使用可变与不可变变量、所有权系统、借用规则等来强制规避 数据竞争 问题,我们之后将会详细讨论。

变量解构与解构式赋值

与 JS 类似,Rust 中也可以使用 let 关键字解构对象:

1
2
3
4
5
let (a ,mut b) :(bool,bool) = (true, false);
println!("a={},b={}",a,b);
b = true;
assert_eq!(a, b);
//a=true,b=false

assert_eq! 也是 rust 中的一个宏函数,主要用来对比两个值是否相等,如果不相等会抛出错误,相等则继续执行。

解构式赋值

在Rust 1.57之后,我们也可以对元组、数组、结构体进行解构赋值了。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Struct {
e: i32,
_f: i32
}
let (a, b, c, d, e);

(a, b) = (1, 2);
// _ 代表匹配一个值,但是我们不关心具体的值是什么,因此没有使用一个变量名而是使用了 _
[c, .., d, _] = [1, 2, 3, 4, 5];
Struct { e, .. } = Struct { e: 5 , _f: 6};
// ..表示忽略一部分值
assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
// 正常运行

常量

不可变变量可能跟常量很像,但是也是有很大区别的:

  1. 常量值被绑定到一个常量名,并且不允许更改;
  2. 常量不允许使用 mut 。也就是说,它被创建起,就永久不可变
  3. 常量使用 const 定义,并且必须指定类型。
  4. 常量名约定使用全大写英文字母,单词之间使用 _ 分割,如果是数字,可以使用 _ 分割提高可读性
    1
    2
    const MAX_NUM:i32 = 100_000;
    println!("最大值为{}",MAX_NUM);

变量遮蔽

对不可变变量重新定义,声明相同的变量名,后一个相同的变量名会遮蔽前一个,称之为 变量遮蔽

1
2
3
4
5
6
7
8
let x = 5;
let x = x + 5;
{
let x = x + 5;
println!("x={}", x)
}

// x=15

这里先将 5 绑定到 x,之后再重新定义新的 x 遮蔽了之前的 x 。这样其实是重新生成了新的变量,重新分配了内存对象,也就是上面所说的对不可变变量的修改。
rust 中也存在 作用域 概念,所以对同一作用域中,对于一些只出现一次或者之后不需要再使用的变量名场景,变量遮蔽其实是很优雅的。一个很简单的例子:

1
2
3
4
let spaces = "      ";
let spaces = spaces.len();
println!("空格长度{}",spaces);
//空格长度6

这样我们就不用再使用什么 space_len, space_num 这样的变量名了。

未使用变量时不被提醒

我们实际开发过程中,有时候确实会出现定义变量暂时没有使用的情况,这时候编译 rust 会给出提示,变量未被使用,可以使用在变量名前添加 _ 的方式忽略提醒。

1
2
3
4
let x = 9;
let _y = 15;
//x会被提醒未被使用,y不会被提醒

1
2
3
4
5
6
7
warning: unused variable: `x`
--> src\main.rs:10:9
|
10 | let x = 9;
| ^ help: if this is intentional, prefix it with an underscore: `_x`
|
= note: `#[warn(unused_variables)]` on by default

总结

这篇文章我们学习了 rust 中 变量绑定, 可变变量与不可变变量, 常量 等概念及 解构, 变量遮蔽 等用法,下篇文章我们学习rust里的 所有权系统.