Rust 入门实战系列(3)- 变量和常量

1,356 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第27天,点击查看活动详情

从这一篇开始,我们会结构化地学习 Rust 相关的语法知识。今天我们的主题是变量,常量以及 可变性(mutability)。

上一节我们提到过,Rust 的变量默认都是不可变的,即 immutable,这样在编译期进行检查能够很大程度上减轻开发者的心智负担,毕竟一个 immutable 的变量一定是并发安全的(类似 Golang 中的 string)。当然,我们也可以通过加上 mut 标识来让一个变量变成 mutable,这样就可以更改了。

今天我们一起来看看这样设计的原因以及是如何助力我们开发。

变量不可变

对于一个 immutable 的变量,一旦发生赋值,你就无法再改变这个值。我们来实验一下:

  1. 首先在我们的 rust-learn 目录下,用 cargo new variable 来创建一个新工程。

image.png

  1. 更新 main.rs 为以下内容:
fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

我们先声明了一个变量 5,这里没加 mut 所以它是默认 immutable 的。随后尝试赋值 6,看看能不能正常运行。

  1. 进入 variable 目录,触发 cargo build 编译。
$ cargo build

=======================================================
   Compiling variable v0.1.0 (/Users/ag9920/go/src/github.com/ag9920/rust-learn/variable)
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

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variable` due to previous error

果然,这里报错信息也很明确 cannot assign twice to immutable variable 不能两次赋值。如果确实需要,consider making this binding mutable 考虑让它变成 mutable 的。

Rust 编译器能够保证,当你声明一个值是 immutable 时,它真的就会是 immutable,不会改变,你不用自己去分析各种链路代码,这样心智负担会小很多。

这里还是要说 Rust 这样的强编译器检查还是很有帮助的。要求严格,把问题扼杀在编译期,这样你的运行时环境会稳很多。虽然写代码时可能会痛苦一些,因为你要遵循规范,要多思考一些,但是这样也能保证 least surprise。而且,Rust 编译器不仅仅告诉你为什么报错,还会给你具体的位置,错误详细信息,甚至怎么改都给出了提示。还是要适应好这种规范。

mut

在变量名称前加上 mut 就能让它变成 mutable 的,用起来很简单,而且关键在于读者在看代码的时候就能识别这种意图,这个变量是会被改的,用的时候要小心。

我们把 main.rs 做一下修改,加上 mut 再试试:

fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

我们执行下编译,再运行下看看

$ cargo build
========================
   Compiling variable v0.1.0 (/Users/ag9920/go/src/github.com/ag9920/rust-learn/variable)
    Finished dev [unoptimized + debuginfo] target(s) in 2.84s
    
    
$ cargo run
============================
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/variable`
The value of x is: 5
The value of x is: 6

这次就符合预期了,两个print 都输出,mut 生效。

所以,语法上 Rust 其实都支持,具体要不要加 mut 需要开发者自行来评估,选择最适用业务场景的用法。

常量

其实默认不可变的【变量】从直觉上看对我们来说,就是一个常量。为什么还有一个【常量】?

我们来看一下区别:

  1. 我们使用 const 而不是 let 关键字来声明一个【常量】,并且必须显式地声明类型(不像变量可以自己推断);
  2. 你不能针对【常量】加 mut 前缀,【常量】是不可能修改的;
  3. 常量可以在任意 scope 中定义,包括全局定义;
  4. 只能用【常量表达式】对【常量】赋值,不能出现任何需要在运行时计算才能得出的值给【常量】赋值。
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

这里定义了一个名为 THREE_HOURS_IN_SECONDS 的常量,它的类型是 u32,值可以在编译器就计算出,这样也省得我们直接写 10800(很不好理解为什么冒出来这个数字)

Rust 对【常量】的命名规范是:大写 + 下划线。

【常量】的生命周期取决于所属的 scope,只要项目在运行,且在 scope 内,你就能够访问到这个【常量】。所以我们可以把一些 magic number 收敛起来变成【常量】,加上合理的注释,方便以后维护。

变量隐藏

这里是指 variable shadowing,各个语言中都有,Rust 也不例外。我们在上一篇文章其实已经见过了。一句话:使用一个此前的变量名称来定义一个新的变量。

此时,我们说原先的变量被新的变量给 shadow(隐藏)了。从此以后,编译器看到这个名称,只会关联到新变量,直到这个 scope 完结,或者新变量又被未来更新的变量 shadow。

看个示例,更新我们的 main.rs:

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

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

运行后输出:

$ cargo run
=============================================

   Compiling variable v0.1.0 (/Users/ag9920/go/src/github.com/ag9920/rust-learn/variable)
    Finished dev [unoptimized + debuginfo] target(s) in 1.03s
     Running `target/debug/variable`
The value of x in the inner scope is: 12
The value of x is: 6

x 就这样从 5 变成 6,在 {} 这个 scope 内变成 12。理解 scope 范围很重要,你可以把它理解成【局部变量】的作用域。

注意区分不同 scope 下被 shadow 的变量 和 被 mut 修饰的变量。

一旦被 mut 修饰,代表这个变量本身是可以被修改的。

而被 shadow 的变量,代表着在这个 scope 内,有了完全不一样的含义。

事实上如果我们把 {} 内的 let x = x * 2 改成 x = x * 2 这里会直接报错,因为你没有 shadow,那么用的还是原来的 x 变量,而默认变量是 immutable 的,所以不能修改。

这里之所以修改,是因为虽然在这个 scope 里它也叫 x,但跟外面的 x 不是一码事。改了这里的 x,一旦回到上个 scope,你会发现原来的 x 还是以前的值,它是 immutable 的。

另一个需要注意的知识点在于,虽然我们上面举例的 shadow 发生在同类型之间,事实上换 type 也是没问题的。

例如我们可以这样,先声明一个字符串类型的变量,随后保持同名,改成一个数字类型变量。

let spaces = "   ";
let spaces = spaces.len();

这样的好处在于我们不用再定义一个 spaces_num, 或者 spaces_str,这一点还是很有用处的,省很多事。否则同一个语义的东西我们要折腾出来好几个名字。

但是注意,还是要区分 shadow 和 mut,上面说的规则是 shadow,不要以为标识了 mut 之后也能串着类型来修改,下面这样是 不 ok 的:

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

运行之后会报出这样的错误:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
2 |     let mut spaces = "   ";
  |                      ----- expected due to this value
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` due to previous error