变量绑定(Variable Bindings)
几乎每个程序的基本能力是存储和修改数据。 Rust也不例外。 让我们以一个简单示例开始。
绑定(binding)的基础知识
首先,我们将使用Cargo生成一个新的项目。 打开终端,并定位到你想保存项目的目录。在那里,让我们生成一个新的项目:
$ cargo new --bin bindings
$ cd bindings
这会创建一个新的项目,'bindings',并且会建立Cargo.toml
和src/main.rs
文件。
我们在"Hello, World!"章节中见过,Cargo会生成这些文件,并且创建一个‘hello world’程序:
fn main() {
println!("Hello, world!");
}
让我们用这个程序覆盖上面那个:
fn main() {
let x = 5;
println!("The value of x is: {}", x);
}
并运行它:
$ cargo run
Compiling bindings v0.1.0 (file:///home/steve/tmp/bindings)
Running `target/debug/bindings`
The value of x is: 5
如果你看到的是错误信息,请仔细检查你有没有复制正确上面的代码。 让我们一行一行来解读。
fn main() {
main()
函数是每个Rust程序的入口。
我们在下节会讲更多关于函数(function)的内容,但是现在,只需要知道程序从哪里开始就够了。
开头的大括号{
,表示函数体的开始。
let x = 5;
这是我们的第一个‘变量绑定’,它是用‘let
语句(statement)’创建的。
这个let
语句格式是这样的:
let NAME = EXPRESSION;
一个let
语句首先会对EXPRESSION
进行求值,然后把结果值绑定给NAME
,以便它能在后面的程序中被引用。
在这个简单示例中,表达式已经是一个值了,是5,但是我们能实现相同的效果:
let x = 2 + 3;
一般情况下,let
语句跟模式(pattern)一起工作,变量名只是模式的一种简单形式。
模式,是Rust的重要组成部分,我们将在后面看到更复杂更强大的模式。
在这样做之前,我们先来把这个例子的剩余部分弄清楚。 来看下一行:
println!("The value of x is: {}", x);
println!
宏在屏幕上打印文本。
我们可以告诉大家,这是一个宏,因为那个!
。
现在还不会学习如何编写宏,你将在本书的后面学到,但是我们将用到由整个标准库提供的宏。
每次你看到!
,请记住,这意味着一个宏。
宏可以为语言增加新的语法,并且!
意味着提醒某件事非比寻常。
println!
,具体来说,有一个必须的参数,‘格式字符串(format string)’,以及任意个数的可选参数。
格式字符串可以包含特殊的文本{}
。
每个{}
都对应一个额外的参数。这里有一个例子:
let x = 2 + 3;
let y = x + 5;
println!("The value of x is {}, and the value of y is {}", x, y);
你可以想象{}
是一个固定好值的小蟹钳。
这样的占位符有很多高级的格式选项,我们将在后面讨论。
}
最后,一个闭合大括号匹配了该main()
函数声明的起始大括号,函数声明结束。
对于输出:
The value of x is: 5
将5
绑定给x
,然后用println!
将其打印到屏幕上。
多个绑定
让我们尝试一个更复杂的模式。 对之前的示例做如下改变:
fn main() {
let (x, y) = (5, 6);
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}
使用cargo run
运行它:
$ cargo run
Compiling bindings v0.1.0 (file:///home/steve/tmp/bindings)
Running `target/debug/bindings`
The value of x is: 5
The value of y is: 6
我们用一个let
创建了两个绑定!
这是模式:
(x, y)
这是值:
(5, 6)
如你所见,上面的两行是让x
绑定5
,y
绑定6
。
我们也能用两个let
语句:
fn main() {
let x = 5;
let y = 6;
}
对于像这样简单的情况来说,两个let
也许更清晰,但是其他情况,一次创建多个绑定也许更好。
当我们变得越来越精通Rust,就会找出哪种风格更好,但最多也只是个主观判断。
类型注解(type annotation)
你可能已经注意到了,在前一个例子中,我们并没有声明x
或y
的类型。
Rust是一个 *静态类型(statically typed)*语言,也就是说,在编译时,我们必须知道所有绑定的类型。
但是注解每一个绑定的类型是个繁杂的工作,并且会让代码变的嘈杂。
为了解决这个问题,Rust使用了‘类型推导(type inference)’,即,它会尝试推断绑定的类型。
类型推导的主要方式是通过查看它是如何被用的。 让我们再次查看示例:
fn main() {
let x = 5;
}
当我们把5
绑定给x
的时候,编译器知道x
应该是一个数值类型。
在没有其他任何信息的情况下,它默认是i32
类型,32位整数类型。
我们将在3.3节讨论更多关于Rust的基本类型。
下面是使用了类型注解
的let
语句示例:
fn main() {
let x: i32 = 5;
}
我们可以添加一个冒号,然后是类型名称。
这里是使用了类型注解的let
语句结构:
let PATTERN: TYPE = VALUE;
注意,冒号和TYPE
在PATTERN
后面。
这里有使用了两个绑定的复杂模式:
fn main() {
let (x, y): (i32, i32) = (5, 6);
}
就像用PATTERN
匹配VALUE
那样,我们也用PATTERN
匹配TYPE
。
延迟初始化(Delayed Initialization)
绑定的初始值不是必须提供的,可以在之后再把值指派给它。试试这段代码:
fn main() {
let x;
x = 5;
println!("The value of x is: {}", x);
}
使用cargo run
执行:
$ cargo run
Compiling bindings v0.1.0 (file:///home/steve/tmp/bindings)
Running `target/debug/bindings`
The value of x is: 5
执行的很顺利。 这就是引出一个问题: 如果在声明一个值之前尝试打印绑定会怎么样? 下面是演示此问题的代码:
fn main() {
let x;
println!("The value of x is: {}", x);
x = 5;
}
可以通过执行cargo run
找出答案:
Compiling bindings v0.1.0 (file:///home/steve/tmp/bindings)
src/main.rs:4:39: 4:40 error: use of possibly uninitialized variable: `x` [E0381]
src/main.rs:4 println!(“The value of x is: {}”, x);
^
<std macros>:2:25: 2:56 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
src/main.rs:4:5: 4:42 note: in this expansion of println! (defined in <std macros>)
src/main.rs:4:39: 4:40 help: run `rustc --explain E0381` to see a detailed explanation
error: aborting due to previous error
Could not compile `bindings`.
To learn more, run the command again with --verbose.
一个错误!
编译器不让我们这样去写程序。
这是编译器帮助我们在程序中发现错误的第一个示例。
不同的程序语言对这个问题的处理方法不同。
有些语言总是会初始化某种默认值。
另外一些语言会留着未初始化的值,而不保证你在初始化它之前用到它的时候会出现什么么蛾子。
Rust选择了另外一种: 抛出错误,并且强制要求程序员去说清楚他们到底想做什么。
在用x
之前,必须初始化某种值。
扩展错误解释
这有关于错误消息更有趣的部分:
src/main.rs:4:39: 4:40 help: run `rustc --explain E0381` to see a detailed explanation
通过给rustc
传入--explain
就能看到扩展的错误解释。
并不是每个错误都有更详细的解释,但是大部分是如此。
这些扩展的解释试图展示错误发生的常见原因,以及问题的常见解决方案。
下面是E0381
:
$ rustc --explain E0381
It is not allowed to use or capture an uninitialized variable. For example:
fn main() {
let x: i32;
let y = x; // error, use of possibly uninitialized variable
To fix this, ensure that any declared variables are initialized before being
used.
如果你对某个错误束手无策的话,这些解释可以帮到你。 编译器是你的朋友,并为你提供帮助。
可变绑定
改变绑定的值会怎么样? 这里有另外一个示例展示了这个问题:
fn main() {
let x = 5;
x = 6;
println!("The value of x is: {}", x);
}
cargo run
给出了答案:
$ cargo run
Compiling bindings v0.1.0 (file:///home/steve/tmp/bindings)
src/main.rs:4:5: 4:10 error: re-assignment of immutable variable `x` [E0384]
src/main.rs:4 x = 6;
^~~~~
src/main.rs:4:5: 4:10 help: run `rustc --explain E0384` to see a detailed explanation
src/main.rs:2:9: 2:10 note: prior assignment occurs here
src/main.rs:2 let x = 5;
^
错误里提到re-assigment of immutable variable
(重新指派不可变变量)
这是对的: 绑定是不可变的.
但是仅仅是指它们默认不可变。
当我们创建新绑定的时候,我们可以在模式前面增加mut
来把绑定变成可变的。
这有个例子:
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
运行它:
$ cargo run
Compiling bindings v0.1.0 (file:///home/steve/tmp/bindings)
Running `target/debug/bindings`
The value of x is: 5
The value of x is: 6
我们现在可以改变x
绑定的值了。
注意该语法let mut
不是一体的,而是在模式前使用了mut
对模式使用()
更明显:
fn main() {
let (mut x, y) = (5, 6);
x = 7;
y = 8;
}
编译器会对这段程序发点牢骚:
$ cargo build
Compiling bindings v0.1.0 (file:///home/steve/tmp/bindings)
src/main.rs:5:5: 5:10 error: re-assignment of immutable variable `y` [E0384]
src/main.rs:5 y = 8;
^~~~~
src/main.rs:5:5: 5:10 help: run `rustc --explain E0384` to see a detailed explanation
src/main.rs:2:17: 2:18 note: prior assignment occurs here
src/main.rs:2 let (mut x, y) = (5, 6);
^
重新指派x
是可以的,但是y
却不行。
mut
仅对就近的绑定起作用,而不是整个模式。
重新指派,而不是变化
还有个细微之处,我们还没有讨论过: mut
允许你去改变_绑定_,而不是改变_绑定的值_。
换句话说:
fn main() {
let mut x = 5;
x = 6;
}
这不会改变x
绑定的值,而是创建了一个新值,6
,然后让x
绑定了新值。
这是一个微妙但很重要的区别。
现在并没有太大差别,但是当我们的程序变得更复杂的时候,差异就会明显。
特别是,当给函数传递参数的时候,差异将显现。
我们将在下一节讨论函数的时候来探讨这种差别。
作用域(scope)
变量绑定有其生效的‘作用域’。 作用域起始于绑定的声明,结束于该声明所在代码块的结尾大括号。 我们仅能在‘作用域中’访问到绑定。 在“进入作用域之前”和“离开作用域之后”都不能访问绑定。 这里是一个示例:
fn main() {
println!("x is not yet in scope");
let x = 5;
println!("x is now in scope");
println!("In real code, we’d now do a bunch of work.");
println!("x will go out of scope now! The next curly brace is ending the main function.");
}
我们可以通过使用{
和}
创建任意作用域:
fn main() {
println!("x is not yet in scope");
let x = 5;
println!("x is now in scope");
println!("Let’s start a new scope!");
{
let y = 5;
println!("y is now in scope");
println!("x is also still in scope");
println!("y will go out of scope now!");
println!("The next curly brace is ending the scope we started.");
}
println!("x is still in scope, but y is now out of scope and is not usable");
println!("x will go out of scope now! The next curly brace is ending the main function.");
}
一旦学习了关于引用(reference)
和特质(trait)
之后,绑定在作用域中的进进出出将会变的更加重要。
绑定屏蔽(Shadowing)
关于绑定的最后一件事是: 它们可以用相同的名称‘屏蔽(shadow)’前一个绑定。 这里是一个简单示例:
fn main() {
let x = 5;
let x = 6;
println!("The value of x is: {}", x);
}
运行它,我们能看到屏蔽起作用了:
src/main.rs:2:9: 2:10 warning: unused variable: `x`, #[warn(unused_variables)] on by default
src/main.rs:2 let x = 5;
^
Running `target/debug/bindings`
The value of x is: 6
在此输出中有两件有意思的事。
首先,Rust能编译并运行这段程序,没有问题。
其次,如我们所见,x
的值是6
。
但是,我们并没有把x
声明为可变绑定。
而是声明了一个_新_的绑定,也把它命名为x
,然后指派给它一个新值。
只要新的x
被声明,旧的x
绑定的值就无法被访问了。
如果你想改变一个值,但是又想让它不可变,那可以用绑定屏蔽。
比如:
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("The value of x is: {}", x);
}
将会输出:
Compiling bindings v0.1.0 (file:///home/steve/tmp/bindings)
Running `target/debug/bindings`
The value of x is: 12
这样可以修改x
,但没有把绑定改成可变的。
这样做是有好处的,因为后面如果修改它的话,编译器会提示我们的。
假设计算出12
以后,还想修改x
。
如果没把程序改成“可变绑定”的风格,大概是这样:
fn main() {
let mut x = 5;
x = x + 1;
x = x * 2;
println!("The value of x is: {}", x);
x = 15;
println!("The value of x is: {}", x);
}
Rust很乐意让我们再次修改它为15
。
把上面程序改成“不可变风格”以后,再去修改x
:
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("The value of x is: {}", x);
x = 15;
println!("The value of x is: {}", x);
}
如果我们尝试编译,会得到下面错误:
$ cargo build
Compiling bindings v0.1.0 (file:///home/steve/tmp/bindings)
src/main.rs:8:5: 8:11 error: re-assignment of immutable variable `x` [E0384]
src/main.rs:8 x = 15;
^~~~~~
src/main.rs:8:5: 8:11 help: run `rustc --explain E0384` to see a detailed explanation
src/main.rs:4:9: 4:10 note: prior assignment occurs here
src/main.rs:4 let x = x * 2;
^
error: aborting due to previous error
Could not compile `bindings`.
正是我们想要的。
绑定屏蔽可能需要一些时间来适应,但是它非常强大,并且这和“不可变风格”的代码很配。
我们讨论一下关于在本节最初那个程序编译输出的问题。 这是其中一部分:
src/main.rs:2:9: 2:10 warning: unused variable: `x`, #[warn(unused_variables)] on by default
下面是两行相关的代码:
let x = 5;
let x = 6;
Rust知道屏蔽了绑定x
,但是我们从始至终都没有用到过初始值。
这不是个错误,确切来说,它可能不是我们想要的结果。
在这种情况下,编译器发出了一个‘警告’,但是仍然会编译我们的程序。
#[warn(unused_variables)]
语法被叫做‘属性(attribute)’,我们将在后面的章节讨论。
更具体来讲,这样的警告被称为'lint',它是一个古老术语,是对那些你不想织入布里的羊毛的称呼。
代码里这行lint很相似,它告诉我们,这里可能有一些并不需要的多余代码。
没有它,代码也会工作的很好。
这些警告值得注意,并要根据它们的提示修复这些问题。
因为它们可能是某种严重问题的信号。
拿本例来说,我们有可能并没有意识到已经屏蔽了绑定x
(也许会造成严重问题)。
绑定屏蔽和作用域
跟其他绑定一样,一个绑定屏蔽另外一个绑定也是只在作用域内有效。 这里是示例代码:
fn main() {
let x = 5;
println!("Before shadowing, x is: {}", x);
{
let x = 6;
println!("Now that x is shadowed, x is: {}", x);
}
println!("After shadowing, x is: {}", x);
}
如果我们运行这个程序,我们能看到“屏蔽绑定”的变化:
$ cargo run
Compiling bindings v0.1.0 (file:///home/steve/tmp/bindings)
Running `target/debug/bindings`
Before shadowing, x is: 5
Now that x is shadowed, x is: 6
After shadowing, x is: 5