ex12: 变量绑定

欢迎来到 Rust 语言的更深层次内容的学习。在前面的章节中我们主要针对 Rust 语言的项目构建基础、基本的变量类型和基本的输出调试方法进行了介绍,接下来我们会看到更多的 Rust 语言有趣的特性。有了这些进一步的知识我们就可以准备开始自主实现一个可以运行的程序了。

变量绑定

变量绑定我们在 Rust Jumpstart 中很早就已经接触到了,我们使用let关键字就可以将一个值绑定到一个变量上。在程序设计中这基本上是不可或缺的一步,因为我们需要在程序中处理一些有变化的量,或是保存某些值。

let hello = "Hello";

使用以上的语句就可以把一个值绑定到一个变量上。这个过程可以认为分为两个步骤:首先你需要得到等号右侧的值,它可能是一个固定的文本量(Literal),也可能是一个需要经过若干步计算才能得到的值;之后编译器会把等号左边书写的变量名记录下来,把等号右边的值“绑定”到这个变量名上。这个过程有点像你创建了一个文件,你首先需要先编辑那个文件的内容,然后为这个文件设计一个名字。

如果你创建的变量没有被使用,编译器会提醒你在前面添加一个下划线进行提示:

let _unused_var = "unused";

可变性 (Mutability)

Rust 语言给人的第一个神奇的特点就在变量的可变性上。至少从中文的理解上,变量之所以叫做“变量”,那么它至少应该是“可变的”对吧?但是在 Rust 中,这个很明显的事情变得不是那么显然(在绝大多数程序设计语言中变量确实一上来就是可以变的)。Rust 中的变量 (variable) 具有一个名为“可变性” (Mutability) 的标记,一个变量默认是不可变的(immutable),它绑定的值不能被修改,除非你特别声明。如果你希望像其他的语言一样让变量的值可以改变,那么你需要在使用let绑定的时候添加mut关键字。

let mut mutable = 10;
mutable = 11;

不过 Rust 是一个强类型语言,也就是说虽然变量是“可变”的,但是它只能在自己所处的类型中进行改变。

let mut integer = 10;
// integer = 12.5; // 这是不允许的

作用域 与 shadowing

在变量绑定中有一个很重要但是理解起来可能有一点困难的概念:shadowing。这个概念一般和“作用域”的概念结合着使用,在很多的类 C 语言中都存在,不过在 Rust 当中,这个概念的理解可能还会更加困难一点。

我们首先提一下:“作用域”的概念,一般来讲,每一个大括号是一个作用域,一个文件是一个作用域。作用域之间可以嵌套,内部的作用域可以访问外部作用域的内容。在大多数语言中,如果一个作用域结束了,除了外层作用域的变量,内侧作用域的所有数据都会消失。Rust 也根据这个机制实现了资源的管理系统,不过这个我们之后再说。

不妨先看一下下面的代码:

{
    let mut a = 'a';
    {
        let a = 'b';
    }
    print!("{} ", a);
    {
        a = 'b';
    }
    print!("{}", a);
}

以上这段代码运行的结果应该是什么呢?最后的输出应该是a b。这个过程我们可以慢一点理解。

我们在外侧的作用域里面定义了一个可变变量a,这是所有在同一个作用域下(一般是同一个大括号下)的所有代码都可以直接访问到a,因为它是可变的,所以它的值可能会改变,这就是为什么第二个大括号中我们写的代码改变了定义在外侧的变量a的值。

但是如果你在一个更小的作用域中使用let绑定了一个同名的变量,则在该作用域下,所有对于变量a的访问会改为这个新绑定的变量a。请注意,外侧的变量a并没有消失,你可以认为不同作用域的变量的名称不同(unsafe{实际上不是这样,变量的名字是相同的,只是作用域在检查的时候采用的作用域不同})。在这个时候:当我们在不同的作用域中使用同样的变量名时,我们进行的操作就是 shadowing 。

shadowing 最特别的特点是它并不是重新对变量进行赋值,而是对一个同名的变量进行了绑定。如果在同一个作用域中,那么你之前绑定的数据可能就消失了。

let variable = "Hello World";
let variable = 27; // shadowing 是允许改变变量类型的,但是之前的同名变量就会被抛弃

如果你在一个更小的作用域中定义了某个变量,如果你不对它进行一些操作,那么在这个作用域结束后这个变量以及其所绑定的值会被抛弃

{
    let x = 10;
    {
        let y = 20;
        x = y;
    }
    print!("x: {} ", x); // x == 10
    // print!("y: {}", y); // 你无法做到这件事,因为 y 在离开大括号的时候就已经消失了
}

先声明后绑定

你可以像其他语言一样先声明后绑定数值,但是这种方法非常不推荐,因为可能导致变量忘记初始化,这个时候变量是没有默认的值的。在所有要定义变量的时候最好直接提供初始化的值。

let num;
{
    num = 5;
}
println!("{}", num);

代码

fn main() {
    // 为一个“变量”绑定对应的值
    let variable = "Hello World";
    println!("I said: \"{}\"", variable);
    // “变量”默认是“不可变”(immutabale)
    // !error! 下面这一行是错误的
    variable = "Nya nya nya";
    // 但是你可以靠着 shadowing 来进行重新绑定
    let variable = "Nya nya nya"; // 你需要使用 let 关键字进行 shadowing
    println!("I said: \"{}\"", variable);

    // 定义一个“可变”(mutable)的“变量”
    let mut mutable_variable = 0;
    println!("before mutation: {}", mutable_variable);
    mutable_variable = 1;
    println!("after mutation: {}", mutable_variable);

    // 没有被使用的变量应该使用 _ 作为前缀
    let _unused_variable = 2333;

    // shadowing 和作用域
    let variable = 1; // 请注意:我们改变了 variable 绑定的数据类型
    println!("outer variable: {}", variable);
    let mut variable = 2; // 将声明过的 variable 进行 shadowing
    println!("outer variable shadowing: {}", variable);
    {
        // 此时我们依然可以访问外层定义的可变 variable 变量
        println!("inner access outer variable: {}", variable);
        variable = 12; // 此时修改的是外层定义的 variable 变量
        println!("inner change outer variable: {}", variable);
        // 在内侧定义一个名称全新的变量
        let another_variable = 'a';
        println!("inner another_variable: {}", another_variable);
        // 在内侧重新绑定一个*名称相同*的变量 variable,但是它和外层的 variable 已经不是同一个变量
        // 这并不会导致外层的变量消失
        let mut variable = 20;
        println!("inner variable: {}", variable);
        variable = 21; // 对重新绑定的变量进行更改
        println!("inner changed variable: {}", variable);
        let variable = variable; // freeze,使用一个定义过的可变变量赋值给同名的不可变变量 #1
        println!("inner freezed variable: {}", variable);
        // !error! 下面这句是错误的。variable 最后一次绑定时是不可变的。
        variable = 16;
    }
    // !error! 下面这句是错误的。inner_variable 已经离开作用域并释放
    println!("out access inner variable: {}", inner_variable);
    println!("outer variable is changed inside: {}", variable); // 可以发现外层的变量已经被修改

    // 可以先声明变量之后赋值,不过这并不推荐
    let binding;
    {
        let value = "Hello World";

        binding = value;
    }
    println!("binding is: {}", binding);

    let forget_to_bind;
    // !error! 下面这一行是错误的
    println!("{}", forget_to_bind);
    forget_to_bind = "Oh, I forgot to bind.";
    println!("{}", forget_to_bind);

}

代码说明

#1

freeze 代表的是在同一个或者不同的作用域中利用一个可变的变量的值初始化一个不可变的变量,这会使得这个值在当前作用域中不可修改。这可以防止一个变量在作用域中被修改。

let mut x = 10;
{
    let x = x; // freezing
    let y = x;
    // x = 20 // 不可以这么做
}
x = 30;

本节总结

在这一节中我们主要介绍了在 Rust 中进行变量绑定的若干操作,重点是要弄清变量的可变性以及 shadowing 。

你应该对以下的内容有所掌握:

  1. 能够自由地使用letmut定义可变以及不可变的变量

  2. 能够在不同的作用域之间正确地使用同名的变量

你应该对以下的内容有所了解:

  1. 作用域的概念

  2. shadowing 与 freezing 的概念

参考资料

最后更新于