Appearance
2.1 变量声明
Rust 的变量必须先声明后使用。对于局部变量,最常见的声明语法为:
rust
let variable : i32 = 100;
与传统的 C/C++ 语言相比,Rust 的变量声明语法不同。这样设计主要有以下几个方面的考虑。
- 语法分析更容易
从语法分析的角度来说,Rust 的变量声明语法比 C/C++ 语言的简单,局部变量声明一定是以关键字let
开头,类型一定是跟在冒号:
的后面。语法歧义更少,语法分析器更容易编写。
- 方便引入类型推导功能
Rust 的变量声明的一个重要特点是:要声明的变量前置,对它的类型描述后置。这也是吸取了其他语言的教训后的结果。 因为在变量声明语句中,最重要的是变量本身,而类型其实是个附属的额外描述,并非必不可少的部分。 如果我们可以通过上下文环境由编译器自动分析出这个变量的类型,那么这个类型描述完全可以省略不写。 Rust 一开始的设计就考虑了类型自动推导功能,因此类型后置的语法更合适。
- 模式解构
let 语句不光是局部变量声明语句,而且具有 pattern destructure(模式解构)的功能。关于“模式解构”的内容在后面的章节会详细描述。
实际上,包括 C++/C#/Java 等传统编程语言都开始逐步引入这种声明语法,目的是相似的。
Rust 中声明变量缺省是“只读”的,比如如下程序:
rust
fn main() {
let x = 5;
x = 10;
}
会得到“re-assignment of immutable variable'x'
”这样的编译错误。
如果我们需要让变量是可写的,那么需要使用 mut 关键字:
rust
let mut x = 5; // mut x: i32
x = 10;
此时,变量x
才是可读写的。Rust 要求你尽可能明确你的意图。
实际上,let
语句在此处引入了一个模式解构,我们不能把let mut
视为一个组合,而应该将mut x
视为一个组合。
mut x
是一个“模式”,我们还可以用这种方式同时声明多个变量:
rust
let (mut a, mut b) = (1, 2);
let Point { x: ref a, y: ref b} = p;
其中,赋值号左边的部分是一个“模式”,第一行代码是对 tuple 的模式解构,第二行代码是对结构体的模式解构。 所以,在 Rust 中,一般把声明的局部变量并初始化的语句称为“变量绑定”,强调的是“绑定”的含义,与 C/C++中的“赋值初始化”语句有所区别。
Rust 中,每个变量必须被合理初始化之后才能被使用。使用未初始化变量这样的错误,在 Rust 中是不可能出现的(利用 unsafe 做 hack 除外)。 如下这个简单的程序,也不能编译通过:
rust
fn main() {
let x: i32;
println!("{}", x);
}
错误信息为:
txt
error[E0381]: used binding `x` isn't initialized
编译器会帮我们做一个执行路径的静态分析,确保变量在使用前一定被初始化:
rust
fn test(condition: bool) {
let x: i32; // 声明 x,不必使用 mut 修饰
if condition {
x = 1; // 初始化 x,不需要 x 是 mut 的,因为这是初始化,不是修改
println!("{}", x);
}
// 如果条件不满足,x 没有被初始化
// 但是没关系,只要这里不使用 x 就没事
}
类型没有“默认构造函数”,变量没有“默认值”。对于let x: i32;
如果没有显式赋值,它就没有被初始化,不要想当然地以为它的值是0
。
Rust 里的合法标识符(包括变量名、函数名、trait 名等)必须由数字、字母、下划线组成,且不能以数字开头。这个规定和许多现有的编程语言是一样的。Rust 将来会允许其他 Unicode 字符做标识符,只是目前这个功能的优先级不高,还没有最终定下来。另外还有一个 raw identifier 功能,可以提供一个特殊语法,如 r#self,让用户可以以关键字作为普通标识符。这只是为了应付某些特殊情况时迫不得已的做法。
Rust 里面的下划线是一个特殊的标识符,在编译器内部它是被特殊处理的。它跟其他标识符有许多重要区别。比如,以下代码就编译不过:
rust
fn main() {
let _ = "hello";
println!("{}", _);
}
我们不能在表达式中使用下划线来作为普通变量使用。下划线表达的含义是“忽略这个变量绑定,后面不会再用到了”。在后面讲析构的时候,还会提到这一点。
2.1.1 变量遮蔽
Rust 允许在同一个代码块中声明同样名字的变量。如果这样做,后面声明的变量会将前面声明的变量“遮蔽”(Shadowing)起来。
rust
fn main() {
let x = "hello";
println!("x is {}", x);
let x = 5;
println!("x is {}", x);
}
上面这个程序是可以编译通过的。请注意第5
行的代码,它不是x = 5;
,它前面有一个let
关键字。 如果没有这个let
关键字,这条语句就是对x
的重新绑定(重新赋值)。 而有了这个let
关键字,就是又声明了一个新的变量,只是它的名字恰巧与前面一个变量相同而已。
但是这两个x
代表的内存空间完全不同,类型也完全不同,它们实际上是两个不同的变量。 从第5
行开始,一直到这个代码块结束,我们没有任何办法再去访问前一个x
变量,因为它的名字已经被遮蔽了。
变量遮蔽在某些情况下非常有用,比如,我们需要在同一个函数内部把一个变量转换为另一个类型的变量,但又不想给它们起不同的名字。 再比如,在同一个函数内部,需要修改一个变量绑定的可变性。例如,我们对一个可变数组执行初始化,希望此时它是可读写的,但是初始化完成后,我们希望它是只读的。
可以这样做:
rust
// 注意:这段代码只是演示变量遮蔽功能,并不是 Vec 类型的最佳初始化方法
fn main() {
let mut v = Vec::new(); // v 必须是 mut 修饰,因为我们需要对它写入数据
v.push(1);
v.push(2);
v.push(3);
let v = v; // 从这里往下,v 成了只读变量,可读写变量 v 已经被遮蔽,无法再访问
for i in &v {
println!("{}", i);
}
}
反过来,如果一个变量是不可变的,我们也可以通过变量遮蔽创建一个新的、可变的同名变量。
rust
fn main() {
let v = Vec::new();
let mut v = v;
v.push(1);
println!("{:?}", v);
}
请注意,这个过程是符合“内存安全”的。“内存安全”的概念一直是 Rust 关注的重点,我们将在第二部分详细讲述。 在上面这个示例中,我们需要理解的是,一个“不可变绑定”依然是一个“变量”。 虽然我们没办法通过这个“变量绑定”修改变量的值,但是我们重新使用“可变绑定”之后,还是有机会修改的。 这样做并不会产生内存安全问题,因为我们对这块内存拥有完整的所有权,且此时没有任何其他引用指向这个变量,对这个变量的修改是完全合法的。 Rust 的可变性控制规则与其他语言不一样。更多内容请参阅本书第二部分内存安全。
实际上,传统编程语言 C/C++中也存在类似的功能,只不过它们只允许嵌套的区域内部的变量出现遮蔽。 而 Rust 在这方面放得稍微宽一点,同一个语句块内部声明的变量也可以发生遮蔽。
2.1.2 类型推导
Rust 的类型推导功能是比较强大的。它不仅可以从变量声明的当前语句中获取信息进行推导,而且还能通过上下文信息进行推导。
rust
fn main() {
// 没有明确标出变量的类型,但是通过字面量的后缀,
// 编译器知道 elem 的类型为 u8
let elem = 5u8;
// 创建一个动态数组,数组内包含的是什么元素类型可以不写
let mut vec = Vec::new();
vec.push(elem);
// 到后面调用了 push 函数,通过 elem 变量的类型,
// 编译器可以推导出 vec 的实际类型是 Vec<u8>
println!("{:?}", vec);
}
我们甚至还可以只写一部分类型,剩下的部分让编译器去推导,比如下面的这个程序,我们只知道 players 变量是 Vec 动态数组类型,但是里面包含什么元素类型并不清楚,可以在尖括号中用下划线来代替:
rust
fn main() {
let player_scores = [
("Jack", 20), ("Jane", 23), ("Jill", 18), ("John", 19),
];
// players 是动态数组,内部成员的类型没有指定,交给编译器自动推导
let players : Vec<_> = player_scores
.iter()
.map(|&(player, _score)| {
player
})
.collect();
println!("{:?}", players);
}
自动类型推导和“动态类型系统”是两码事。 Rust 依然是静态类型的。一个变量的类型必须在编译阶段确定,且无法更改,只是某些时候不需要在源码中显式写出来而已。这只是编译器给我们提供的一个辅助工具。
Rust 只允许“局部变量/全局变量”实现类型推导,而函数签名等场景下是不允许的,这是故意这样设计的。 这是因为局部变量只有局部的影响,全局变量必须当场初始化而函数签名具有全局性影响。 函数签名如果使用自动类型推导,可能导致某个调用的地方使用方式发生变化,它的参数、返回值类型就发生了变化,进而导致远处另一个地方的编译错误,这是设计者不希望看到的情况。
2.1.3 类型别名
我们可以用 type 关键字给同一个类型起个别名(type alias)。示例如下:
rust
type Age = u32;
fn grow(age: Age, year: u32) -> Age {
age + year
}
fn main() {
let x : Age = 20;
println!("20 years later: {}", grow(x, 20));
}
类型别名还可以用在泛型场景,比如:
rust
type Double<T> = (T, Vec<T>); // 小括号包围的是一个 tuple,请参见后文中的复合数据类型
那么以后使用Double<i32>
的时候,就等同于(i32,Vec<i32>)
,可以简化代码。
2.1.4 静态变量
Rust 中可以用static
关键字声明静态变量。如下所示:
rust
static GLOBAL: i32 = 0;
与let
语句一样,static 语句同样也是一个模式匹配。与let
语句不同的是,用static
声明的变量的生命周期是整个程序,从启动到退出。 static
变量的生命周期永远是'static
,它占用的内存空间也不会在执行过程中回收。这也是 Rust 中唯一的声明全局变量的方法。
由于 Rust 非常注重内存安全,因此全局变量的使用有许多限制。这些限制都是为了防止程序员写出不安全的代码:
全局变量必须在声明的时候马上初始化;
全局变量的初始化必须是编译期可确定的常量,不能包括执行期才能确定的表达式、语句和函数调用;
Rust 要求必须使用 unsafe 语句块才能访问和修改
static
变量,因为这种使用方式往往并不安全,其实编译器是对的,当在多线程中同时去修改时,会不可避免的遇到脏数据。
只有在同一线程内或者不在乎数据的准确性时,才应该使用全局静态变量。
示例如下:
rust
fn main() {
//局部变量声明,可以留待后面初始化,只要保证使用前已经初始化即可
let x;
let y = 1_i32;
x = 2_i32;
println!("{} {}", x, y);
//全局变量必须声明的时候初始化,因为全局变量可以写到函数外面,被任意一个函数使用
static G1 : i32 = 3;
println!("{}", G1);
//可变全局变量无论读写都必须用 unsafe 修饰
static mut G2 : i32 = 4;
unsafe {
G2 = 5;
println!("{}", G2);
}
//全局变量的内存不是分配在当前函数栈上,函数退出的时候,并不会销毁全局变量占用的内存空间,程序退出才会回收
}
Rust 禁止在声明 static 变量的时候调用普通函数,或者利用语句块调用其他非 const 代码:
rust
// 这样是允许的
static array : [i32; 3] = [1,2,3];
// 这样是不允许的
static vec : Vec<i32> = { let mut v = Vec::new(); v.push(1); v };
调用const fn
是允许的,因为const fn
是编译期执行的:
rust
fn main() {
use std::sync::atomic::AtomicBool;
static FLAG: AtomicBool = AtomicBool::new(true);
}
Rust 不允许用户在main
函数之前或者之后执行自己的代码。所以,比较复杂的 static 变量的初始化一般需要使用 lazy 方式,在第一次使用的时候初始化。
在 Rust 中,如果用户需要使用其他情况的全局变量初始化,推荐使用 lazy_static 库。
通过lazy_static
消除static
静态变量本身的一些限制:
rust
#[macro_use]
extern crate lazy_static;
use std::collections::HashMap;
lazy_static!{
// 初始化动态数组
static ref VEC:Vec<u8> = vec![0x18u8, 0x11u8];
static ref MAP: HashMap<u32, String> = {
let mut map = HashMap::new();
map.insert(18, "hury".to_owned());
map
};
// 使用函数初始化静态变量
static ref PAGE:u32 = mulit(18);
}
fn mulit(i: u32) -> u32 {
i * 2
}
fn main() {
println!("{:?}", *PAGE);
println!("{:?}", *VEC);
println!("{:?}", *MAP);
}
2.1.5 常量
在 Rust 中还可以用 const 关键字做声明。如下所示:
rust
const PI: f32 = 3.14159265359;
fn main() {
let radius = 5.0;
let area = PI * radius * radius;
println!("The area of a circle with radius {} is {}.", radius, area);
}
使用const
声明的是常量,常量是一种不可变的值,在程序运行期间不能被修改。因此一定不允许使用mut
关键字修饰这个变量绑定,这是语法错误。
常量的初始化表达式也一定要是一个编译期常量,不能是运行期的值。 它与 static 变量的最大区别在于:编译器并不一定会给 const 常量分配内存空间,在编译过程中,它很可能会被内联优化。 因此,用户千万不要用 hack 的方式,通过 unsafe 代码去修改常量的值,这么做是没有意义的。 以 const 声明一个常量,也不具备类似 let 语句的模式匹配功能。