变量声明
基础知识
Rust中的变量分局部变量和全局变量两种,且必须先声明后使用,常见的声明语法为:
// 声明局部变量,使用let关键字
let var: i32 = 100;
// 声明全局变量,使用static关键字
static GLOBAL: i32 = 0;
由于Rust非常注重内存安全,因此全局变量的使用有许多限制,我们日常使用最多的还是局部变量。与传统的C/C++语言相比,Rust的变量声明语法不同,这样设计主要有下列三种优点:
- 语法分析更容易:Rust的局部变量声明一定是以关键字
let
开头,类型一定跟在:
后面,语法歧义更少,语法分析器更容易编写。 - 类型推导更方便:Rust的变量声明的一个重要特点是,要声明的变量前置,对它的类型描述后置。这是吸取了其他语言的教训后的结果,因为在变量声明语句中,最重要的是变量本身,类型只是附属的额外描述,并非必不可少的部分,类型可以由编译器自动推导获得,因此类型后置的语法更合适。
- 支持模式解构:
let
不仅能声明局部变量,还具有模式结构(pattern destructure)的功能,这里暂且不表。
Rust中变量声明默认是“只读”的,如果需要让变量可写,则需要使用mut
关键字,mut
是mutable
的简写:
let x = 5;
x = 10; // x为只读变量,因此会报编译错误
let mut y = 5; // 使用mut关键字声明一个可写变量
y = 10;
Rust中变量必须被初始化后才可以使用,否则会报编译错误。因此,不被初始化的变量是没有默认值的。变量既可以在声明时初始化,也可以在使用前初始化:
let x: i32; // 声明变量x
x = 1; // 初始化变量x,不需要x是mut,因为这是初始化不是修改
println!("{}", x);
Rust中的合法标识符(包括变量名、函数名等)必须由数字、字母、下划线组成,且不能以数字开头。注意,单独的下划线是一个特殊的标识符,在编译器内部是被特殊处理的,不能作为普通变量使用,其具体用法这里暂且不表。Rust中关于变量的命名规范是蛇形命名法,使用下划线,一般用小写,即file_name
。
变量遮蔽
Rust中允许在同一个代码块中声明同样名字的变量,如果这样做,后面声明的变量会将前面声明的变量遮蔽(Shadowing)起来,从而前面的变量将无法访问。例如:
let x = "hello";
println!("x is {}", x);
let x = 5;
println!("x is {}", x);
上面这种形式的代码在很多编程语言中是无法编译通过的,因为变量x被重复声明,但在Rust中是可以编译通过的:前后两个x
是完全不同的两个变量,内存空间不同,类型也不同,只是恰巧名字相同。
Rust这种设计有时非常实用,例如,我们需要在同一个函数内部把一个变量转换为另一个类型的变量,但又不想给它们起不同的名字。但是,我个人认为变量遮蔽可能会带来阅读代码时的歧义,但以我现在的水平尚不能仔细分析变量遮蔽这一特性的利弊权衡,或许未来能够在RFC中找到答案。
类型推导
Rust编译器的类型推导功能很强大,不仅可以从变量声明的当前语句中获取信息进行推导,而且还能通过上下文信息进行推导。类型推导和“动态类型”是两码事,Rust仍然是静态类型的,所有变量的类型都必须在编译阶段确定,类型推导只是辅助我们不需要显示写出类型而已。
类型别名
可以使用type
关键字给同一个类型起个别名,例如:
type Age = u32; // u32代表无符号的32位整数类型
let x: Age = 20;
println!("x is {}", x);
静态变量
可以用static
关键字声明静态变量,这也是Rust中唯一的声明全局变量的方法:
static GLOBAL: i32 = 0;
为了保证内存安全,全局变量的使用有很多限制:
- 必须在声明时立即初始化
- 初始化必须是编译期可确定的常量
- 带有
mut
修饰的全局变量,在使用的时候必须使用unsafe
关键字。
unsafe
关键字用于逃过Rust编译器的检查以写一些可能存在危险的代码,当下无须深入研究。从上面第三条限制可以看出,Rust并不鼓励我们使用可变的全局变量,更希望我们把全局变量用成全局常量。注意,全局变量的声明周期是整个程序,从启动到退出。
常量
可以使用const
关键字声明一个常量:
const GLOBAL: i32 = 0;
常量和静态变量最大的区别在于:编译器不一定会给常量分配内存空间,可能会在编译过程中将常量內联优化。
变量绑定
通过let
关键字来创建变量,这是Rust语言从函数式语言中借鉴的语法形式。let
创建的变量一般称为绑定(binding),而不是我们通常说的赋值,因为它表明了位置表达式和值表达式之间建立的一种关联关系。
Rust中的表达式可以分为位置表达式和值表达式,在其他语言中,一般称为左值和右值。
- 位置表达式:表示内存位置的表达式,有本地变量、静态变量、解引用、数组索引和字段引用五种。通过位置表达式可以对某个数据单元的内存进行读写。
- 值表达式:只引用了某个存储单元地址中的数据,相当于数据值,只能进行读操作。值表达式要么是字面量,要么是表达式求值过程中创建的临时值。
表达式的求值过程在不同的上下文中会有不同的结果,求值上下文也分位置上下文和值上下文。下面几种表达式属于位置上下文(不必看懂,知道就好):
- 赋值或者复合赋值语句左侧的操作数
- 一元引用表达式的独立操作数
- 包含隐式借用的操作数
- match判别式或let绑定右侧在使用ref模式匹配的时候也是位置上下文
除了上述几种情况,其余表达式都属于值上下文。一般情况下,值表达式出现在值上下文中,位置表达式出现在位置上下文中,但也存在特殊情况:
值表达式不能出现在位置上下文中,否则会报错:
// error[E0070]: invalid left-hand side of assignment "hello" = 1;
当位置表达式出现在值上下文中时,该位置表达式会把所有权转移给另外一个位置表达式。
变量引用
我们刚刚看到了一个新名词,“所有权(ownership)”,所有权代表着以下意义:
- 每个值在Rust中都有一个变量来管理它,这个变量就是这个值、这块内存的所有者。
- 每个值在一个时间点上只有一个管理者。
- 当变量所在的作用域结束的时候,变量以及它代表的值将会被销毁。
当位置表达式出现在值上下文中时,这种所有权转移在Rust中称为移动语义。但在日常开发中,有时候并不需要转移所有权,Rust提供了引用操作符&
,可以直接获得位置表达式的内存地址,并通过该地址进行读写操作,解引用使用*
操作符。下面看一个引用和解引用的示例:
let x = 32;
let y = &x;
assert_eq!(32, *y);
在上面的代码中,32这个值的所有权归变量x所有,变量y中存储的是其地址,最后通过解引用操作符*
将引用y中的值取出来,以供assert_eq!
宏使用,因为变量x仍旧保留它们的所有权,所以引用也被称为借用。
生命周期是Rust的核心概念,而所有权、借用等概念又和生命周期息息相关,但目前先不去深入了解它,待把基础语法掌握后,再去学习Rust的精髓,方能事半功倍。
参考文献
- 《Rust编程之道》张汉东
- 《深入浅出Rust》范长春