01-变量绑定与解构
变量命名
在命名方面,和其他语言没有区别,不过当给变量命名时,需遵循 Rust 命名规范1。
Rust 语言有一些关键字(keywords),和其他语言一样,这些关键字都是被保留给 Rust 语言使用的,因此,它们不能被用作变量或函数的名称。在 Rust 关键字2 中可找到关键字列表。
变量绑定
在其它语言中,我们用 var a = "hello world"
的方式给 a
赋值,也就是把等式右边的 "hello world"
字符串赋值给变量 a
,而在 rust 中,我们这样写:let a = "hello world"
,同时给这个过程起了另一个名字:变量绑定。
为何不用赋值而用绑定呢(其实你也可以称之为赋值,但是绑定的含义更清晰准确)?这里就涉及 Rust 最核心的原则——所有权,简单来讲,任何内存对象都是有主人的,而且一般情况下完全属于它的主人,绑定就是把这个对象绑定给一个变量,让这个变量成为它的主人(聪明的读者应该能猜到,在这种情况下,该对象之前的主人就会丧失对该对象的所有权),像极了我们的现实世界,不是吗?
那为什么要引进“所有权”这个新的概念呢?请稍安勿躁,时机一旦成熟,我们就回来继续讨论这个话题。
变量可变性
Rust 的变量在默认情况下是不可变的。当然你可以通过 mut
关键字让变量变为可变的,让设计更灵活。
如果变量 a
不可变,那么一旦为它绑定值,就不能再修改 a
。举个例子,在我们的工程目录下使用 cargo new variables
新建一个项目,叫做 variables。
然后在新建的 variables 目录下,编辑 src/main.rs,改为下面代码:
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
保存文件,再使用 cargo run 运行它,迎面而来的是一条错误提示:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
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
error: aborting due to previous error
具体的错误原因为 cannot assign twice to immutable variable x
(无法对不可变的变量重复赋值),因为我们想为不可变的 x
变量再次赋值。
在 Rust 中,可变性很简单,只要在变量名前加一个 mut
即可,而且这种显示声明方式还会给后来人传达这样的信息:嗯,这个变量在后面代码部分会发生改变。
为了让变量为可变,将 src/main.rs 改为以下内容:
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 variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
选择可变还是不可变,更多的还是取决于你的使用场景,例如不可变可以带来安全性,但是丧失了灵活性和性能(如果你要改变,就要重新创建一个新的变量,这里涉及到内存对象的再分配)。而可变变量最大的好处就是使用上的灵活性和性能上的提升。
例如,在使用大型数据结构或者热点代码路径(被大量频繁调用)的情形下,在同一内存位置更新实例可能比复制并返回新分配的实例要更快。使用较小的数据结构时,通常创建新的实例并以更具函数式的风格来编写程序,可能会更容易理解,所以值得以较低的性能开销来确保代码清晰。
使用下划线开头忽略未使用的变量
如果你创建了一个变量却不在任何地方中使用它,Rust 通常会给你一个警告,因为这可能会是个 Bug。但是有时创建一个不会被使用的变量是有用的,比如正在设计原型或刚刚开始一个项目。这时你希望告诉 Rust 不要警告未使用的变量,为此可以用下划线作为变量名的开头:
fn main() {
let _x = 5;
let y = 10;
}
使用 cargo run
运行下试试:
warning: unused variable: `y`
--> src/main.rs:3:9
|
3 | let y = 10;
| ^ help: 如果 y 故意不被使用,请添加一个下划线前缀: `_y`
|
= note: `#[warn(unused_variables)]` on by default
可以看到,两个变量都只是声明,没有使用,但编译器却只给出了 y
未被使用的警告,充分说明了 _
变量名前缀在这里发挥的作用。
变量解构
let
表达式不仅仅用于变量的绑定,还能进行复杂变量的解构:从一个相对复杂的变量中,匹配出该变量的一部分内容:
fn main() {
let (a, mut b) = (true, false);
// a = true,不可变; b = false,可变
println!("a = {:?}, b = {:?}", a, b);
b = true;
assert_eq!(a, b);
}
解构式赋值
在 Rust 1.59 版本后,可以在赋值语句中的左式中使用元祖、切片和结构体模式了。
#[allow(dead_code)]
struct Struct {
e: i32,
f: i32,
}
fn main() {
let (a, b, c, d, e): (i32, i32, i32, i32, i32);
(a, b) = (1, 2);
// _ 代表匹配一个值,但是我们不关心具体的值是什么,因为没有使用一个变量名而是 _
[c, .., d, _] = [1, 2, 3, 4, 5];
Struct { e, .. } = Struct { e: 5, f: 6 };
assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e])
}
这种使用方式跟之前的 let 保持了一致性,但是 let 会重新绑定,而这里仅仅对之前绑定的变量进行再赋值。
需要注意的是,使用 += 的赋值语句还不支持解构式赋值。
变量和常量之间的差异
常量与不可变变量一样,常量也是绑定到一个常量名且不允许更改的值,但是常量和变量之间存在一些差异:
- 常量不允许使用
mut
。常量不仅默认不可变,而且自始至终不可变。因为常量在编译完成后,就已经确定了它的值。 - 常量使用
const
关键字而不是let
关键字来声明,并且值的类型必须标注。
下面是一个常量声明的例子,其常量名为 MAX_POINTS,值为 100,000。(Rust 常量的命名约定是全部字母都大写,并使用下划线分隔单词,另外对数字字面量可插入下划线以提高可读性)
const MAX_POINTS: u32 = 100_000;
常量可以在任意作用域内声明,包括全局作用域,在声明的作用域内,常量在程序运行的整个过程中都有效。对于需要在多处代码共享一个不可变的值时非常有用,例如游戏中允许玩家赚取的最大点数或光速。
变量遮蔽
Rust 允许声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的,如下所示:
fn main() {
let x = 5;
// 在main函数的作用域内对之前的x进行遮蔽
let x = x + 1;
{
// 在当前的花括号作用域内,对之前的x进行遮蔽
let x = x * 2;
println!("The value of x in the inner scope is: {}", x);
}
println!("The value of x is: {}", x);
}
这个程序首先将数值 5
绑定到 x
,然后通过重复使用 let x =
来遮蔽之前的 x
,并取原来的值加上 1
,所以 x
的值变成了 6
。第三个 let
语句同样遮蔽前面的 x
,取之前的值并乘上 2
,得到的 x 最终值为 12
。当运行此程序,将输出以下内容:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
...
The value of x in the inner scope is: 12
The value of x is: 6
这和 mut
变量的使用是不同的,第二个 let
生成了完全不同的新变量,两个变量只是恰好拥有同样的名称,涉及一次内存对象的再分配 ,而 mut
声明的变量,可以修改同一个内存地址上的值,并不会发生内存对象的再分配,性能要更好。
变量遮蔽的用处在于,如果你在某个作用域内无需再使用之前的变量(在被遮蔽后,无法再访问到之前的同名变量),就可以重复的使用变量名字,而不用绞尽脑汁去想更多的名字。
例如,假设有一个程序要统计一个空格字符串的空格数量:
// 字符串类型
let spaces = " ";
// usize 数值类型
let spaces = spaces.len();
这种结构是允许的,因为第一个 spaces
变量是一个字符串类型,第二个 spaces
变量是一个全新的变量且和第一个具有相同的变量名,且是一个数值类型。所以变量遮蔽可以帮我们节省些脑细胞,不用去想如 spaces_str
和 spaces_num
此类的变量名;相反我们可以重复使用更简单的 spaces
变量名。如果你不用 let
:
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
|
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
error: aborting due to previous error
显然,Rust 对类型的要求很严格,不允许将整数类型 usize
赋值给 字符串类型
。usize
是一种 CPU 相关的整数类型。
命名规范
基本的 Rust 命名规范在 RFC 430 中有描述。
通常,对于 type-level 的构造 Rust 倾向于使用驼峰命名法,而对于 value-level 的构造使用蛇形命名法。详情如下:
条目 | 惯例 |
---|---|
包 Crates | unclear |
模块 Modules | snake_case |
类型 Types | UpperCamelCase |
特征 Traits | UpperCamelCase |
枚举 Enumerations | UpperCamelCase |
结构体 Structs | UpperCamelCase |
函数 Functions | snake_case |
方法 Methods | snake_case |
通用构造器 General constructors | new or with_more_details |
转换构造器 Conversion constructors | from_some_other_type |
宏 Macros | snake_case! |
局部变量 Local variables | snake_case |
静态类型 Statics | SCREAMING_SNAKE_CASE |
常量 Constants | SCREAMING_SNAKE_CASE |
类型参数 Type parameters | UpperCamelCase ,通常使用一个大写字母: T |
生命周期 Lifetimes | 通常使用小写字母:'a ,'de ,'src |
Features | unclear but see C-FEATURE |
对于驼峰命名法,复合词的缩略形式我们认为是一个单独的词语,所以只对首字母进行大写:使用 Uuid
而不是 ,UUID
Usize
而不是 ,USize
Stdin
而不是 。StdIn
对于蛇形命名法,缩略词用全小写:is_xid_start
。
对于蛇形命名法(包括全大写的 SCREAMING_SNAKE_CASE
),除了最后一部分,其它部分的词语都不能由单个字母组成: btree_map
而不是 ,b_tree_map
PI_2
而不是 .PI2
包名不应该使用 -rs
或者 -rust
作为后缀,因为每一个包都是 Rust 写的,因此这种多余的注释其实没有任何意义。
特征命名
特征的名称应该使用动词,而不是形容词或者名词,例如 Print
和 Draw
明显好于 Printable
和 Drawable
。
类型转换要遵守 as\_
,to\_
,into\_
命名惯例(C-CONV)
类型转换应该通过方法调用的方式实现,其中的前缀规则如下:
方法前缀 | 性能开销 | 所有权改变 |
---|---|---|
as_ |
Free | borrowed -> borrowed |
to_ |
Expensive | borrowed -> borrowedborrowed -> owned (non-Copy types)owned -> owned (Copy types) |
into_ |
Variable | owned -> owned (non-Copy types) |
例如:
str::as\_bytes()
把str
变成 UTF-8 字节数组,性能开销是 0。输入是一个借用的&str
,输出也是一个借用的&str
Path::to\_str
会执行一次昂贵的 UTF-8 字节数组检查,输入和输出都是借用的。对于这种情况,如果把方法命名为as_str
是不正确的,因为这个方法的开销还挺大str::to\_lowercase()
在调用过程中会遍历字符串的字符,且可能会分配新的内存对象。输入是一个借用的str
,输出是一个有独立所有权的String
String::into\_bytes()
返回String
底层的Vec<u8>
数组,转换本身是零消耗的。该方法获取String
的所有权,然后返回一个新的有独立所有权的Vec<u8>
当一个单独的值被某个类型所包装时,访问该类型的内部值应通过 into_inner()
方法来访问。例如将一个缓冲区值包装为 BufReader
类型,还有 GzDecoder
、AtomicBool
等,都是这种类型。
如果 mut
限定符在返回类型中出现,那么在命名上也应该体现出来。例如,Vec::as\_mut\_slice
就说明它返回了一个 mut
切片,在这种情况下 as_mut_slice
比 as_slice_mut
更适合。
// 返回类型是一个 `mut` 切片
fn as_mut_slice(&mut self) -> &mut [T];
标准库中的一些例子
读访问器(Getter)的名称遵循 Rust 的命名规范(C-GETTER)
除了少数例外,在 Rust代码中 get
前缀不用于 Getter。
pub struct S {
first: First,
second: Second,
}
impl S {
// 而不是 get_first
pub fn first(&self) -> &First {
&self.first
}
// 而不是 get_first_mut,get_mut_first,or mut_first
pub fn first_mut(&mut self) -> &mut First {
&mut self.first
}
}
至于上文提到的少数例外,如下:当有且仅有一个值能被 Getter 所获取时,才使用 get
前缀。例如,Cell::get
能直接访问到 Cell
中的内容。
有些 Getter 会在过程中执行运行时检查,那么我们就可以考虑添加 _unchecked
Getter 函数,这个函数虽然不安全,但是往往具有更高的性能。 典型的例子如下:
fn get(&self, index: K) -> Option<&V>;
fn get_mut(&mut self, index: K) -> Option<&mut V>;
unsafe fn get_unchecked(&self, index: K) -> &V;
unsafe fn get_unchecked_mut(&mut self, index: K) -> &mut V;
标准库示例
std::io::Cursor::get\_mut
std::ptr::Unique::get\_mut
std::sync::PoisonError::get\_mut
std::sync::atomic::AtomicBool::get\_mut
std::collections::hash\_map::OccupiedEntry::get\_mut
<[T]>::get\_unchecked
一个集合上的方法,如果返回迭代器,需遵循命名规则:iter
,iter\_mut
,into\_iter
(C-ITER)
fn iter(&self) -> Iter // Iter implements Iterator<Item = &U>
fn iter_mut(&mut self) -> IterMut // IterMut implements Iterator<Item = &mut U>
fn into_iter(self) -> IntoIter // IntoIter implements Iterator<Item = U>
上面的规则适用于同构性的数据集合。与之相反,str
类型是一个 UTF-8 字节数组切片,与同构性集合有一点微妙的差别,它可以认为是字节集合,也可以认为是字符集合,因此它提供了 str::bytes
去遍历字节,还有 str::chars
去遍历字符,而并没有直接定义 iter
等方法。
上述规则只适用于方法,并不适用于函数。例如 url
包的 percent\_encode
函数返回一个迭代器用于遍历百分比编码(Percent encoding)的字符串片段. 在这种情况下,使用 iter
/iter_mut
/into_iter
诸如此类的函数命名无法表达任何具体的含义。
标准库示例
迭代器的类型应该与产生它的方法名相匹配(C-ITER-TY)
例如形如 into_iter()
的方法应该返回一个 IntoIter
类型,与之相似,其它任何返回迭代器的方法也应该遵循这种命名惯例。
上述规则主要应用于方法,但是经常对于函数也适用。例如上文提到的 url
包中的 percent\_encode
函数,返回了一个 PercentEncode
类型。
特别是,当这些类型跟包名前缀一起使用时,将具备非常清晰的含义,例如 vec::IntoIter
。
标准库示例
Vec::iter
returnsIter
Vec::iter\_mut
returnsIterMut
Vec::into\_iter
returnsIntoIter
BTreeMap::keys
returnsKeys
BTreeMap::values
returnsValues
Cargo Feature 的名称不应该包含占位词(C-FEATURE)
不要在 Cargo feature 中包含无法传达任何意义的词,例如 use-abc
或 with-abc
,直接命名为 abc
即可。
一个典型的例子就是:一个包对标准库有可选性的依赖。标准的写法如下:
# 在 Cargo.toml 中
[features]
default = ["std"]
std = []
// 在我们自定义的 lib.rs 中
#![cfg_attr(not(feature = "std"), no_std)]
除了 std
之外,不要使用任何 ust-std
或者 with-std
等自以为很有创造性的名称。
命名要使用一致性的词序(C-WORD-ORDER)
这是一些标准库中的错误类型:
JoinPathsError
ParseBoolError
ParseCharError
ParseFloatError
ParseIntError
RecvTimeoutError
StripPrefixError
它们都使用了 谓语-宾语-错误
的词序,如果我们想要表达一个网络地址无法分析的错误,由于词序一致性的原则,命名应该如下 ParseAddrError
,而不是 AddrParseError
。
词序和个人习惯有很大关系,想要注意的是,你可以选择合适的词序,但是要在包的范畴内保持一致性,就如标准库中的包一样。
附录 A:关键字
下面的列表包含 Rust 中正在使用或者以后会用到的关键字。因此,这些关键字不能被用作标识符(除了原生标识符),包括函数、变量、参数、结构体字段、模块、包、常量、宏、静态值、属性、类型、特征或生命周期。
目前正在使用的关键字
如下关键字目前有对应其描述的功能。
as
- 强制类型转换,或use
和extern crate
包和模块引入语句中的重命名break
- 立刻退出循环const
- 定义常量或原生常量指针(constant raw pointer)continue
- 继续进入下一次循环迭代crate
- 链接外部包dyn
- 动态分发特征对象else
- 作为if
和if let
控制流结构的 fallbackenum
- 定义一个枚举类型extern
- 链接一个外部包,或者一个宏变量(该变量定义在另外一个包中)false
- 布尔值false
fn
- 定义一个函数或 函数指针类型 (function pointer type)for
- 遍历一个迭代器或实现一个 trait 或者指定一个更高级的生命周期if
- 基于条件表达式的结果来执行相应的分支impl
- 为结构体或者特征实现具体功能in
-for
循环语法的一部分let
- 绑定一个变量loop
- 无条件循环match
- 模式匹配mod
- 定义一个模块move
- 使闭包获取其所捕获项的所有权mut
- 在引用、裸指针或模式绑定中使用,表明变量是可变的pub
- 表示结构体字段、impl
块或模块的公共可见性ref
- 通过引用绑定return
- 从函数中返回Self
- 实现特征类型的类型别名self
- 表示方法本身或当前模块static
- 表示全局变量或在整个程序执行期间保持其生命周期struct
- 定义一个结构体super
- 表示当前模块的父模块trait
- 定义一个特征true
- 布尔值true
type
- 定义一个类型别名或关联类型unsafe
- 表示不安全的代码、函数、特征或实现use
- 在当前代码范围内(模块或者花括号对)引入外部的包、模块等where
- 表示一个约束类型的从句while
- 基于一个表达式的结果判断是否继续循环
保留做将来使用的关键字
如下关键字没有任何功能,不过由 Rust 保留以备将来的应用。
abstract
async
await
become
box
do
final
macro
override
priv
try
typeof
unsized
virtual
yield
原生标识符
原生标识符(Raw identifiers)允许你使用通常不能使用的关键字,其带有 r#
前缀。
例如,match
是关键字。如果尝试编译如下使用 match
作为名字的函数:
fn match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
会得到这个错误:
error: expected identifier, found keyword `match`
--> src/main.rs:4:4
|
4 | fn match(needle: &str, haystack: &str) -> bool {
| ^^^^^ expected identifier, found keyword
该错误表示你不能将关键字 match
用作函数标识符。你可以使用原生标识符将 match
作为函数名称使用:
文件名: src/main.rs
fn r#match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
fn main() {
assert!(r#match("foo", "foobar"));
}
此代码编译没有任何错误。注意 r#
前缀需同时用于函数名定义和 main
函数中的调用。
原生标识符允许使用你选择的任何单词作为标识符,即使该单词恰好是保留关键字。 此外,原生标识符允许你使用其它 Rust 版本编写的库。比如,try
在 Rust 2015 edition 中不是关键字,却在 Rust 2018 edition 是关键字。所以如果用 2015 edition 编写的库中带有 try
函数,在 2018 edition 中调用时就需要使用原始标识符语法,在这里是 r#try
。