Skip to content

2.1 变量声明

Rust 的变量必须先声明后使用。对于局部变量,最常见的声明语法为:

rust
let variable : i32 = 100;

与传统的 C/C++ 语言相比,Rust 的变量声明语法不同。这样设计主要有以下几个方面的考虑。

  1. 语法分析更容易

从语法分析的角度来说,Rust 的变量声明语法比 C/C++ 语言的简单,局部变量声明一定是以关键字let开头,类型一定是跟在冒号:的后面。语法歧义更少,语法分析器更容易编写。

  1. 方便引入类型推导功能

Rust 的变量声明的一个重要特点是:要声明的变量前置,对它的类型描述后置。这也是吸取了其他语言的教训后的结果。 因为在变量声明语句中,最重要的是变量本身,而类型其实是个附属的额外描述,并非必不可少的部分。 如果我们可以通过上下文环境由编译器自动分析出这个变量的类型,那么这个类型描述完全可以省略不写。 Rust 一开始的设计就考虑了类型自动推导功能,因此类型后置的语法更合适。

  1. 模式解构

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 语句的模式匹配功能。

Released under the MIT License