适合我。
如果你想看,最好能够:
序号为奇数的点更重要。
想学了。
参见 https://www.zhihu.com/question/490394258/answer/2153862539,是一个融合了面向过程和函数式的混合范式语言。
始终根据(编写时的)最新情况,边学编写的笔记。以语言本身为核心,但也重视开发环境中的实践。
根据官网教程操作即可。假设安装的过程可以正常使用代理、速度正常。
无论是 Windows 还是类 UNIX 操作系统,都选择安装到默认位置(%userprofile%/.cargo
或 ~/.cargo
),符合系统基础软件安装的习惯。
官网教程说道,Cargo 是 Rust 语言的构建系统和包管理器,一切命令都是 cargo
而非 rust
。使用以下命令检查安装是否正常。
cargo --version
见官网教程。直接按其操作即可。
此时先不要理会代码。该章的目的是搭建开发环境。
项目自动创建完成后,可以看到源文件是 src/main.rs
。教程提供的创建项目指令可能会创建 git 仓库,学习时,将源代码目录下的 .git/
目录删掉即可。
在 VS Code 扩展商店中安装 rust-analyzer
扩展即可获得 Rust 语言的语法支持。在 main
函数上点击 Run 或 Debug 按钮即可进行运行和调试。
此外,该扩展支持调用 Cargo 提供的代码格式化等功能。在之后的实践中具体感受。
考虑到网络环境不佳,请最好先进行下面的操作。
参考 https://blog.csdn.net/tanshiqian/article/details/121963284,在 Cargo 安装目录(.cargo/
)下新建名为 config.toml
的文件,文件内容如下。
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = "tuna"
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
更改完成后,删除 .cargo/.package-cache
文件,之后会根据该配置文件设定的源重新配置缓存。
Cargo 作为包管理器(package manager),可以方便地使用和管理第三方包(Rust 中的包称为 crate)。
根据官网教程操作即可。要点是修改清单文件(the manifest)Cargo.toml
中的 [dependencies]
栏目。
学会了 Rust 的数据和控制,就能够使用 Rust 编写任意程序了。下面我们首先学习数据部分,这也是 Rust 的精髓所在。
Rust 使用 let
关键字定义不可变变量。
fn main() {
let a_number = 114514;
println!("{}", a_number);
}
import std.core; // 按 C++23 标准,应该使用 `import std;`。但考虑到实际编译器实现,暂时全部使用 `std.core`。
int main()
{
const auto a_number = 114514;
std::println("{}", a_number);
}
很显然,指明一个变量不可变有利于编译器给出相关警告或进行优化。
Rust 中,使用 println!
输出一行文字,与 C++ 中的 std::println
一样。如果你不知道怎么使用,可以参见 C++ 的 std::format
,或者 Python 的 str.format
,或者 C# 的 String.Format
,等等,反正现在大家觉得这种用法是最好的。
println
后的感叹号(!
)表示 println!
是一个宏。Rust 中,函数的签名是确定的,要使得函数支持任意数量的参数,只能利用宏。另外,由于 println
是一个宏,所以它甚至支持字符串插值,请自行查阅相关资料。
要定义可变变量,需要结合使用 mut
关键字。
fn main() {
let mut dual_number = 114;
print!("{}", dual_number);
dual_number = 514;
println!("{}", dual_number);
}
import std.core;
int main()
{
auto dual_number = 114;
std::print("{}", dual_number);
dual_number = 514;
std::println("{}", dual_number);
}
同 C++ 类似,Rust 中使用 print!
宏进行输出时不会额外输出换行符。之后的 C++ 程序中,将使用编译器已经支持的特性进行输出(使用 cout
和 format
代替 print
和 println
)。
如果删去 mut
关键字,Rust 编译器会报错:
error[E0384]: cannot assign twice to immutable variable `dual_number`
--> src\main.rs:4:5
|
2 | let dual_number = 114;
| -----------
| |
| first assignment to `dual_number`
| help: consider making this binding mutable: `mut dual_number`
3 | print!("{}", dual_number);
4 | dual_number = 514;
| ^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable
error: aborting due to previous error
For more information about this error, try `rustc --explain E0384`.
这就如同给 C++ 程序 2 加上 const
关键字后,编译器(MSVC)报错:
error C3892: “dual_number”: 不能给常量赋值
相比之下,Rust 编译器的前端往往比 C++ 的编译器更强,能给出的修正方法更多。
要显式指定变量的类型,使用的语法为:
"let" ("mut") ":" ("=" ) ";"
fn main() {
let x: i32 = 11;
print!("{}", x);
let x: u64 = 45;
print!("{}", x);
let x: i128 = 14;
println!("{}", x);
}
import std.core;
int main()
{
const int32_t x_1 = 11;
std::cout << std::format("{}", x_1);
const uint64_t x_2 = 45; // C++ 不能定义同名变量。
std::cout << std::format("{}", x_2);
const __int128_t x_3 = 14; // C++ 标准不支持 128 位整数。
std::cout << std::format("{}", x_3) << std::endl;
// 之后只能使用 x_3,但编译器不知道。
}
从程序 3 可以看到 Rust 基本整数类型的命名逻辑。
程序 3 说明了 Rust 的变量**遮蔽(shadowing)**特性。显然,这样的特性有利于编译器对代码进行优化,因为可以肯定之前的 x
不会再被使用了。要注意,Rust 是强类型的语言。
使用整数字面量时,如果不显式指明类型,IDE 将提示我们变量的类型是 i32
。但事实上,使用无后缀整数字面量初始化的变量应该被视为“整数”类型(记作 {integer}
),而不能进一步推导出具体类型。下面很快就会见到一个例子。
要定义编译时确定的常量,使用 const
关键字代替 let
关键字,同时必须显式指定类型。
fn main() {
const X : i32 = 114;
let y = 514;
println!("{}{}", X, y);
}
import std.core;
int main()
{
constexpr int32_t X = 114;
const auto y = 514;
std::cout << std::format("{}{}", X, y) << std::endl;
}
Rust 编译器规定,常量最好用大写字母加下划线,变量最好用小写字母加下划线,否则编译器会警告。
是否有必要必须为常量指明类型还在讨论中。
此处首先介绍部分概念。
Rust 不是面向对象的语言,但是借用了部分面向对象的概念,我们可以在 Rust 中使用方法(method)。
fn main() {
let x: i32 = -114514;
let x = x.abs();
let x = i32::abs(x);
println!("{}", x);
}
其中,x.abs()
等价于 i32::abs()
,因为此处已经明确 x
的类型为 i32
。
程序 5 中,第一次定义 x
时必须指明类型为 i32
,否则第二次定义 x
时只能知道 x.abs()
的类型为一个整数,无法知道具体的类型,会出现以下错误。
error[E0689]: can't call method `abs` on ambiguous numeric type `{integer}`
--> src\main.rs:3:15
|
3 | let x = x.abs();
| ^^^
|
help: you must specify a type for this binding, like `i32`
|
2 | let x: i32 = -114514;
| +++++
For more information about this error, try `rustc --explain E0689`.
可以看出,Rust 具有很强的类型系统。
特征(trait)其实就是 C++ 中的概念(concept)。大家也都知道,没有概念时,C++ 中也用特征(type_trait
)解决问题。
特征可以说明一个类型的能力,比如这个类型是否具有 abs
方法、是否“平凡”。对于泛型(generics)函数,即 C++ 中的模板函数,我们可以限制泛型类型必须具有某些特征,从而让编译器帮我们检查,在正确的地方给出错误提示。
import std.core;
template <typename T>
concept quackable = requires(T t) // 名为 quackable 的特征。
{
{t.quack()} -> std::same_as<void>;
}; // 对于类型为 T 的值,必须具有 quack() 方法,且返回值类型为 void。
template <quackable T> // 要求类型 T 具有该特征。
void f(T t)
{
t.quack();
}
int main()
{
// f(114514); // 不满足特征,编译错误。
}
取消注释 f(114514)
,编译器将给出以下错误信息:
源.cpp(17,2): error C2672: “f”: 未找到匹配的重载函数
源.cpp(10,6): message : 可能是“void f(T)”
源.cpp(17,2): message : 未满足关联约束
源.cpp(9,11): message : 计算结果为 false 的概念“quackable”
源.cpp(6,2): message : 表达式无效
据此可以知道,错误应该发生在第 17 行。如果在第 9 行使用 typename
,不约束类型 T
,则错误信息变成:
源.cpp(12,4): error C2228: “.quack”的左边必须有类/结构/联合
源.cpp(12,4): message : 类型是“T”
with
[
T=int
]
源.cpp(17,10): message : 查看对正在编译的函数 模板 实例化“void f(T)”的引用
with
[
T=int
]
虽然正确给出了需要检查的地方,但出现 error 的代码与错误无关,更容易造成误解。
目前,C++ 编译器还在努力实现中。而对于 Rust 而言,相关功能的错误提示会更强大。
除了帮我们检查类型错误,如果我们知道了一个类型具有某个特征,我们就知道了这个类型应该有的行为,这是我们接下来要关注的。
传统的内存回收机制包括:
复习编译原理,引用计数相比垃圾回收的优点包括不会出现性能抖动。Rust 在引用计数的基础上限制了计数数量,一个值最多只能被一个变量持有,称该机制为所有权,这使得编译时就知道何时回收内存成为可能。
fn main() {
let x = String::from("114514");
let y = x; // 编译时在逻辑上转移所有权。
println!("{}", y);
// println!("{}", x); // 编译错误:x 失去值的所有权。
}
import std.core;
int main()
{
auto x = std::make_unique<const std::string>("114514");
auto y = std::move(x); // 运行时转移“所有权”。
std::cout << std::format("{}", *y) << std::endl;
std::cout << std::format("{}", *x) << std::endl; // 运行时错误:x 失去值的“所有权”。
}
可以看到,Rust 语言在编译时能做的事比 C++ 还要多很多。
Rust 很严格。C++ 编译器虽然可能知道程序 7 会出现运行时错误,给出警告:
使用已移动的 from 对象: x (lifetime.1)。
但 Rust 编译器根本不允许这样的程序通过编译。
注:事实上,应该把所有权机制看作一个新的机制,它不完全等同于最大计数为 1 的引用计数。所有权与变量名绑定,如果不存在变量名(例如数组中的元素),就没有所有权一说。所有权存在的目的是用于计算变量被回收的时机,如果一个变量回收的时间本身就能确定(例如已初始化的数组中的元素,一定正好在数组销毁前回收),也就无需引入所有权。
由此看来,let
语句称为变量绑定更为合适。那要复制数据该怎么办?分为两种情况。
Copy
特征的类型。clone
方法复制。适用于具有 Clone
特征的类型。fn main() {
let x1: i32 = 114514;
let x2 = x1; // i32 具有 Copy 特征,直接复制,x1, x2 分别具有一个整数的所有权。
let x3 = x2.clone(); // 亦可调用 clone() 方法。
let s1 = String::from("114514");
let s2 = s1; // String 不具有 Copy 特征,不复制,s1 失去所有权。
let s3 = s2.clone(); // s2, s3 分别具有一个 String 的所有权。
println!("{}", x1);
println!("{}", x2);
println!("{}", x3);
// println!("{}", s1); // 编译错误:s1 失去值的所有权。
println!("{}", s2);
println!("{}", s3);
}
import std.core;
int main()
{
const int32_t x1 = 114514;
const auto x2 = x1;
const auto x3 = x2; // 对应于 Rust,clone 不一定会分配堆内存。
auto s1 = std::make_unique<const std::string>("114514");
auto s2 = std::move(s1);
auto s3 = std::make_unique<const std::string>(*s2);
std::cout << std::format("{}", x1) << std::endl;
std::cout << std::format("{}", x2) << std::endl;
std::cout << std::format("{}", x3) << std::endl;
std::cout << std::format("{}", *s1) << std::endl; // 运行时错误。
std::cout << std::format("{}", *s2) << std::endl;
std::cout << std::format("{}", *s3) << std::endl;
}
要注意,clone
方法不代表会在堆上分配内存。可以认为 Rust 会在编译时期计算出哪些值需要在堆上分配内存,且尽可能会在栈上分配内存。
目前需要知道,i32
这样的类型具有 Copy
特征,String
不具有 Copy
特征。如何知道一个类型是否具有 Copy
特征呢?
fn is_copy<T>()
where
T: Copy,
{
}
fn main() {
is_copy::<i32>();
// is_copy::(); // 编译错误:不满足约束。
}
import std.core;
template <typename T>
void is_copy()
requires std::is_trivially_copyable_v<T>
{
}
int main()
{
is_copy<int32_t>();
// is_copy(); // 编译错误:不满足约束。
}
只讨论变量的所有权是简单的,但功能也弱。要有力地使用变量,需要**借用(borrow)**变量,也即引用变量。这也使得下面的内容非常复杂,是 Rust 的精髓所在。
此处,应该将取“地址”理解为:
Rust 编译器保证,你敲门时房子一定还在。敲门的方法是使用 &
运算符,敲门得到的类型也是在原类型前加 &
。
fn main() {
let x = String::from("114514");
let x = &x; // 注意上一个 x 还在,只是访问不到了。
println!("{}", x);
let s_ref: &String; // 利用大括号限制作用域。
{
let s = String::from("114514");
// s_ref = &s; // 编译错误:离开大括号后,家被拆了,不允许再敲门。
}
// println!("{}", s_ref); // 如果没有赋值,则不允许使用。
}
import std.core;
int main()
{
auto x_1 = std::make_unique<const std::string>("114514");
auto x_2 = x_1.get(); // 使用指针模拟行为,编译器不检查。
// 注意 x_2 的类型是 const std::string*。
// 由于是指针,所以需要手动解引用。
std::cout << std::format("{}", *x_2) << std::endl;
const std::string* s_ref;
{
auto s = std::make_unique<const std::string>("114514");
s_ref = s.get(); // 编译器不检查,可以敲拆了的门。
}
std::cout << std::format("{}", *s_ref) << std::endl; // 运行时错误。
}
可以看出,所有权借用机制有力地规避了悬挂指针问题,极大地提升了程序的安全性。这也为程序员提出了挑战:如何让 Rust 程序编译通过呢?这是我们接下来重点学习的内容。
C++ 中,引用只能在初始化时赋值,但从程序 10 可以看出,Rust 中引用可以在稍后赋值。这说明引用类型与原类型的地位是不同的,不能像 C++ 一样,处处像使用原类型那样使用引用类型。Rust 中的引用更像是 C++ 中指针和引用的结合体。
注意在等价的 C++ 程序 10 中,指针都是 const 型的。那么在 Rust 中如何声明可变的引用呢?使用 mut
关键字即可。
fn main() {
let mut s = String::from("114"); // 当然,必须是 mut。
let s_ref = &mut s;
s_ref.push_str("514");
println!("{}", s_ref);
}
import std.core;
int main()
{
auto s = std::make_unique<std::string>("114");
auto s_ref = s.get();
// 注意 s_ref 的类型是 std::string*。
s_ref->append("514");
std::cout << std::format("{}", *s_ref) << std::endl;
}
Rust 规定,不可变引用(形如 &i32
)的类型是读者,可变引用(形如 &mut i32
)是写者,并且:
如果以上规定始终成立,且我们只通过引用来实现变量值的读写,那么 Rust 的变量读写天生就是线程安全的。事实上,Rust 的线程安全是通过其他东西实现的,但以上规定是线程安全的基础。
fn main() {
let mut x = 114514;
let x_ref_1 = &x;
let x_ref_2 = &x;
println!("{} {}", x_ref_1, x_ref_2); // 可以同时存在多个读者。
let x_mut_ref_1 = &mut x; // 只要用 &mut 就是写者。
println!("{}", x_mut_ref_1);
let x_mut_ref_2 = &mut x;
// println!("{} {}", x_mut_ref_1, x_mut_ref_2); // 不可同时存在多个写者。
let x_ref_3 = &x;
// println!("{} {}", x_mut_ref_1, x_ref_3); // 读者和写者不可同时存在。
}
import std.core;
int main()
{
auto x = 114514;
const auto* x_ref_1 = &x;
const auto* x_ref_2 = &x;
std::cout << std::format("{} {}", *x_ref_1, *x_ref_2) << std::endl; // 没这些限制。
auto* x_mut_ref_1 = &x;
std::cout << std::format("{}", *x_mut_ref_1) << std::endl; // 没这些限制。
auto* x_mut_ref_2 = &x;
std::cout << std::format("{} {}", *x_mut_ref_1, *x_mut_ref_2) << std::endl; // 没这些限制。
const auto* x_ref_3 = &x;
std::cout << std::format("{} {}", *x_mut_ref_1, *x_ref_3) << std::endl; // 没这些限制。
}
理论上,Rust 中作用域的概念与 C++ 一样,变量名的生命在右大括号结束。但 Rust 在分析读者和写者是否存在时,变量名的生命在最后使用的地方结束,这种特性被称为非词汇生命周期(non-lexical lifetimes, NLL)。
Rust 检查引用变量是否合法离不开生命周期这个概念。检查引用变量是否合法由借用检查器(borrow checker)完成。
借用检查器的一大作用是完全消除悬挂指针(dangling pointer)。
fn main() {
let x_ref; // 定义变量时可以不指定类型,类型在首次赋值时确定。
{
let x = 114514;
x_ref = &x;
println!("{}", x_ref);
}
// println!("{}", x_ref); // 编译错误:此处 x_ref 已经是悬挂引用。
}
import std.core;
int main()
{
const int* x_ref;
{
const auto x = 114514;
x_ref = &x;
std::cout << std::format("{}", *x_ref) << std::endl;
}
std::cout << std::format("{}", *x_ref) << std::endl; // 运行时错误:此处 x_ref 已经是悬挂指针。
// 由于栈可能没有被改变,所以测试时可能不会发生运行时错误。
}
之所以借用检查器知道 x_ref
在第 8 行已经是悬挂引用,是因为它知道在第八行 x
所拥有的值已经死了,对应的引用 x_ref
比值 x
活得更久。
在函数体内追踪值和引用的生命周期是简单的:除了返回值,其他变量都在函数结束前被终结。问题是如何知道返回值的生命周期?
首先我们要明确,仅当返回值是引用时,计算生命周期才是有意义的。因为如果返回值是一个值,那么该值的所有权将由外层函数的一个变量名持有,与函数内的操作、调用函数前的代码都无关。而如果返回值是一个引用,则它一定来自与参数。
由于程序中可能存在分支,参数也可能是一个结构体的引用,所以编译器不可能在运行时准确知道返回值的生命周期到底等于哪个参数的生命周期,也很难在编译时知道返回值的生命周期至少是多少。
fn min_ref(x: &i32, y: &i32) -> &i32 {
if x < y {
return x;
}
return y;
}
fn main() {
let x = 114;
let y = 514;
let x_or_y_ref = min_ref(&x, &y);
println!("{}", x_or_y_ref);
}
import std.core;
const int* min_ref(const int* x, const int* y)
{
if (*x < *y)
return x;
return y;
}
int main()
{
const auto x = 114;
const auto y = 514;
const auto* x_or_y_ref = min_ref(&x, &y);
std::cout << std::format("{}", *x_or_y_ref) << std::endl;
}
Rust 程序 14 报出以下错误:
error[E0106]: missing lifetime specifier
--> src\main.rs:1:33
|
1 | fn min_ref(x: &i32, y: &i32) -> &i32 {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x`
or `y`
help: consider introducing a named lifetime parameter
|
1 | fn min_ref<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
意思是,编译器不知道函数 min_ref
的返回值的生命周期到底应该是多少。实际上,由于 min_ref
既有可能返回 x
,也有可能返回 y
,所以 min_ref
的生命周期应该是 x
和 y
中的最小者。只要参数中包含多个与返回值类型相同的引用,Rust 编译器就无法帮我确定,必须像上面的提示那样使用生命周期标记解决问题。
fn min_ref<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
if x < y {
return x;
}
return y;
}
fn main() {
let x = 114;
let y = 514;
let x_or_y_ref = min_ref(&x, &y);
println!("{}", x_or_y_ref);
}
import std.core;
const int* min_ref(const int* x, const int* y)
{
if (*x < *y)
return x;
return y;
}
int main()
{
const auto x = 114;
const auto y = 514;
const auto* x_or_y_ref = min_ref(&x, &y);
std::cout << std::format("{}", *x_or_y_ref) << std::endl;
}
其中,'a
是生命周期标记的名字(通常都取名为 'a
),它本身就代表了一个生命周期。对于参数(已知生命周期的引用),其生命周期至少和 'a
一样长(大于等于),称为输入生命周期;对于返回值(未确定生命周期的引用),其生命周期不能比 'a
长(小于等于),称为输出生命周期。
知道了返回值的生命周期,Rust 编译器就能在编译时帮我们在函数间追踪生命周期了,可以完全规避悬挂指针问题,如下所示。
fn min_ref<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
if x < y {
return x;
}
return y;
}
fn main() {
let x_or_y_ref;
let x = 114;
{
let y = 514;
x_or_y_ref = min_ref(&x, &y);
}
println!("{}", x_or_y_ref); // 编译失败:x_or_y_ref 的生命周期比生命周期最短的 y 还长。
}
import std.core;
const int* min_ref(const int* x, const int* y)
{
if (*x < *y)
return x;
return y;
}
int main()
{
const int* x_or_y_ref;
const auto x = 114;
{
const auto y = 514;
x_or_y_ref = min_ref(&x, &y);
}
std::cout << std::format("{}", *x_or_y_ref) << std::endl;
// 尽管这个程序没问题,但把 x 和 y 的值换一下就在逻辑上出错了。
}
虽然后面还会遇到更复杂的生命周期标记问题,但只需牢记,Rust 引入生命周期标记的目的是帮助编译器推断程序中所有变量的生命周期,从而完全规避悬挂指针问题。当你觉得编译器在某个地方不好自动推断生命周期时,就该在新的地方引入生命周期标记了。
生命周期是 Rust 的核心概念之一,在之后学习更多概念后,会不断补充生命周期相关的知识。
前面已经让大家熟悉了 Rust 常见整数类型的命名规范,下面的程序列出了所有的常见基本数据类型。
fn main() {
let x_1: i8 = 0b00000000; // 使用 0b 表示二进制。
let x_2: i16 = 0o007; // 使用 0o 表示八进制。
let x_3: i32 = 008; // 十进制可加前导零。
let x_4: i64 = 0xBEAF; // 使用 0x 表示十六进制。
let x_5: i128 = 114_514; // 使用下划线作为分隔符。
let x_6: isize = 0xDEADDEAD; // 大小与目标处理器架构的指针大小相同。
let x_7: u8; // = -1; // 编译错误:不允许超出范围。
let x_8: u16;
let x_9: u32;
let x_10: u64 = 2147483649; // 字面量超出 i32 范围时,必须显式指明类型。
let x_11: u128;
let x_12: usize;
let x_13: f32 = 114.514;
let x_14: f64 = 114.514; // 字面量默认是 f64。
let x_15: char = '字'; // UTF32。
let x_16: bool = true;
let x_17: (); // 称为单元类型。
}
import std.core;
int main()
{
// 省去 const。
int8_t x_1 = 0b00000000; // 使用 0b 表示二进制。
int16_t x_2 = 007; // 使用 0 表示八进制。
int32_t x_3 = 8; // 十进制不可加前导零。
int64_t x_4 = 0xBEAF; // 使用 0x 表示十六进制。
__int128_t x_5 = 114'514; // 使用单引号作为分隔符。
intptr_t x_6 = 0xDEADDEAD; // 大小与目标处理器架构的指针大小相同。
uint8_t x_7; // = -1; // 编译错误:不允许超出范围。
uint16_t x_8;
uint32_t x_9;
uint64_t x_10 = 2147483649; // 字面量超出 int 范围时,隐式转换为更大类型。
__uint128_t x_11;
uintptr_t x_12;
float x_13 = 114.514;
double x_14 = 114.514; // 字面量默认是 double。
char x_15 = 'A'; // 取决于环境的多字节编码。
bool x_16 = true;
struct unit_t {} x_17; // C++ 没有单元类型。
}
单元类型本身写作 ()
,其值也写作 ()
,其含义与 Python 中的 None
类似。
fn f1() {}
fn f2() -> () {
return;
}
fn f3() {
return ();
}
fn main() {}
void f1() {} // 不写 -> () 不对应 auto,对应 void。
void f2() {
return;
}
void f3() {
return void();
}
int main() {}
Rust 中,使用形如 114..514
的格式表示序列,用于 for
循环。
fn main() {
for i in 114..514 {
println!("{}", i);
}
}
import std.core;
int main()
{
for (auto i : std::views::iota(114, 514))
std::cout << std::format("{}", i) << std::endl;
}
Rust 中,亦可在 ..
右端加等号,表示包含右侧数值,如 114..=514
。
得益于引入了单元类型 ()
,Rust 中的语句块也是表达式:前面我们看到的语句块对应的表达式的类型都是 ()
。语句块的值取决于最后一条没有分号的语句。
fn iiyo_koiyo() -> i32 {
114514
}
fn main() {
let x = {
let y = iiyo_koiyo();
y
};
}
int iiyo_koiyo()
{
return [&] {
return 114514; // 应当认为总是进行内联优化,没有函数调用。
}();
}
int main()
{
const int x = [&] {
const int y = iiyo_koiyo();
return y;
}();
}
要注意,定义变量语句不是表达式,但 return
语句是表达式,所以 return
可以省略分号;但不推荐这样做,格式化代码时也会帮你加上分号。
Rust 的枚举类型(enumeration)是 C++ 中的 variant
。
fn main() {
enum MessageType {
EmptyMessage,
KeybdDown(i32), // 括号内写单个类型,是原类型。
MouseDown(i32, i32), // 括号内写多个类型,是元组,相关语法在之后介绍。
MouseUp { x: i32, y: i32 }, // 用大括号,是结构体,相关语法在之后介绍。
} // 无需分号。
let msg1 = MessageType::EmptyMessage;
let msg2 = MessageType::KeybdDown(65);
let msg3 = MessageType::MouseDown(114, 514);
let msg4 = MessageType::MouseUp { x: (114), y: (514) };
}
import std.core;
struct unit_t {}; // C++ 没有单元类型。
int main()
{
enum MessageType_enum
{
EmptyMessage,
KeybdDown,
MouseDown,
MouseUp,
};
struct MouseUp_struct
{
int32_t x;
int32_t y;
};
using MessageType = std::variant<
unit_t,
int32_t,
std::tuple<int32_t, int32_t>,
MouseUp_struct
>;
const auto msg1 = MessageType::variant(std::in_place_index<EmptyMessage>);
const auto msg2 = MessageType::variant(std::in_place_index<KeybdDown>, 65);
const auto msg3 = MessageType::variant(std::in_place_index<MouseDown>, 114, 514);
const auto msg4 = MessageType::variant(std::in_place_index<MouseUp>,
MouseUp_struct{ .x = 114, .y = 514 });
}
可以看出,Rust 在语法上支持 variant
,比 C++ 简洁许多。
if
语句的语法为:
::=
| ...
::= "if" "else"
需要注意:
if
语句本身也是表达式,其值等于某个分支的语句块对应的值。注意,前面讲过语句块也是表达式。if
语句的表达式身份实现三目运算符。此时,两个分支对应的语句块的返回值类型要么一致,要么有的分支进行了跳转。由于语句块的大括号不可省略,所以 else if
不能像 C++ 那样视为 else
和 if
组合,而要将其视为单独的语法。
fn main() {
let x = if true { 114 } else { 514 };
}
int main()
{
const auto x = true ? 114 : 514;
}
应用于枚举类型时,Rust 的 match 语句类似于 C++ 中的 visit
,但完全按照枚举编号访问函数。
fn main() {
enum MessageType {
EmptyMessage,
KeybdDown(i32),
MouseDown(i32, i32),
MouseUp { x: i32, y: i32 },
}
let msg = MessageType::KeybdDown(65);
let msg_result = match msg {
MessageType::EmptyMessage => 1,
MessageType::KeybdDown(_) => {
// _ 表示不关心该参数。
println!("Some key is pressed."); // _ 特殊:不可使用。
1 // 所有分支的类型必须一样。
}
MessageType::MouseUp { x, y } => {
println!("Mouse up (x = {}, y = {})", x, y);
1
}
_ => 0, // _ 特殊:其他类型均属于该分支。
};
}
import std.core;
struct unit_t {}; // C++ 没有单元类型。
int main()
{
enum MessageType_enum
{
EmptyMessage,
KeybdDown,
MouseDown,
MouseUp,
};
struct MouseUp_struct
{
int32_t x;
int32_t y;
};
using MessageType = std::variant<
unit_t,
int32_t,
std::tuple<int32_t, int32_t>,
MouseUp_struct
>;
const auto msg = MessageType::variant(std::in_place_index<KeybdDown>, 65);
const auto msg_result = [&]
{
if (msg.index() == EmptyMessage)
return 1;
else if (msg.index() == KeybdDown)
{
const auto _ = std::get<KeybdDown>(msg); // 不是引用,而是新的变量。
// 所以在 Rust 中,所有权会被拿走!
std::cout << std::format("Key {} is pressed.", _) << std::endl; // _ 没什么特殊的。
return 1; // 所有分支的类型必须一样。
}
else if (msg.index() == MouseDown)
{
const auto [x, y] = std::get<MouseDown>(msg);
std::cout << std::format("Mouse up (x = {}, y = {})", x, y) << std::endl;
return 1;
}
else // 对应 _。参数不可访问。
{
// 也可以执行其他代码。
return 0;
}
}();
}
如同 C++ 中的 visit
必须支持 variant
的所有类型,Rust 中的 match
必须支持 enum
的所有类型。特别地,我们不关心的类型用 _
这个特殊的符号跳过。
要注意,参数会转移所有权。如果不希望转移所有权,需要在 match
的对象前加上 &
,即改为 match &msg
。
C++ 程序 23 中,我们其实没有使用 visit
,是因为按序号进行索引更符合 Rust 的行为;以 visit
为参照只是为了说明 Rust 中 match
语句必须涵盖 enum
的所有类型。另外,我们也没有使用 C++ 中的 switch
,这是因为 Rust 中的 match 语句的行为在本质上是后面马上要讲到的模式匹配,在 C++ 中用 if 语句进行模拟更为合适。
当我们只关心枚举类型是否与一种具体类型相匹配时,我们无需使用 match
语句,而应当使用 if let
语句。
fn main() {
enum MessageType {
EmptyMessage,
KeybdDown(i32),
MouseDown(i32, i32),
MouseUp { x: i32, y: i32 }, // 结构体,相关语法在之后介绍。
}
let msg = MessageType::KeybdDown(65);
let msg_result = match &msg {
// 不转移所有权。
MessageType::KeybdDown(key) => {
println!("Key {} is pressed.", key);
1
}
_ => 0,
};
let msg_result = if let MessageType::KeybdDown(key) = &msg {
println!("Key {} is pressed.", key);
1
} else {
0
}; // 与前面的 match 完全等价。
}
import std.core;
struct unit_t {}; // C++ 没有单元类型。
int main()
{
enum MessageType_enum
{
EmptyMessage,
KeybdDown,
MouseDown,
MouseUp,
};
struct MouseUp_struct
{
int32_t x;
int32_t y;
};
using MessageType = std::variant<
unit_t,
int32_t,
std::tuple<int32_t, int32_t>,
MouseUp_struct
>;
const auto msg = MessageType::variant(std::in_place_index<KeybdDown>, 65);
const auto msg_result_1 = [&]
{
if (msg.index() == KeybdDown)
{
const auto& key = std::get<KeybdDown>(msg); // 注意使用了 &,是引用。
// 所以在 Rust 中,所有权不会改变。
std::cout << std::format("Key {} is pressed.", key) << std::endl;
return 1;
}
else
return 0;
}();
const auto msg_result_2 = [&]
{
if (msg.index() == KeybdDown)
{
const auto& key = std::get<KeybdDown>(msg);
std::cout << std::format("Key {} is pressed.", key) << std::endl;
return 1;
}
else
return 0;
}(); // 与前面的 match 完全等价。
}
要注意,if let 语句的语法为:
"if" "let" "=" ...
其中的 =
应该翻译为“匹配”,而非“等于”或者“赋值”。模式匹配的含义将在之后讲解。
Rust 中的 Option
是一个枚举类型,也就是说 Rust 用 C++ 中的 variant
来定义 optional
。
Rust 中的 Option
是一个泛型,类似于 C++ 中的:
struct unit_t {}; // C++ 没有单元类型。
enum Option_enum
{
Some,
None,
};
template <typename T>
using Option = std::variant<T, unit_t>; // 前者称为 Some,后者称为 None。
由于 Option
十分有用、十分常用,所以 Rust 中无需写 Option
、Option
,直接写 Some(...)
、None
即可。但不要忘记 Option
本身是枚举类型,所以一般使用 if let 语句处理 Option
。
fn main() {
let mut optional_integer: Option<i32> = None; // 无法推断,不可省略类型。
let mut optional_string = None; // 可根据后文推断,可省略类型。
optional_string = Some(String::from("114514")); // Some 不可省略。
if let Some(value) = optional_string {
println!("{}", value);
}
if let None = optional_integer {
println!("No integer value.");
}
}
import std.core;
int main()
{
std::optional<int32_t> optional_integer = std::nullopt; // 无法推断,不可省略类型。
std::optional<std::string> optional_string = std::nullopt; // C++ 无法根据下文推断类型。
optional_string = std::string("114514"); // C++ 为 optional 赋值时可省略 optional。
if (optional_string)
{
const auto value = *optional_string;
std::cout << std::format("{}", value) << std::endl;
}
if (!optional_integer)
{
std::cout << std::format("No integer value.") << std::endl;
}
}
终于到了模式匹配。前面提到 match 语句和 if let 语句都是模式匹配,这里说的模式匹配实际上与 Python 中的模式匹配类似,C++ 不支持。
fn main() {
let x = 114;
let y = 514;
match (x, y) {
(114, 514) => println!("On point."),
(114, another_y) => println!("On x (y = {}).", another_y),
(another_x, 514) => println!("On y (x = {}).", another_x),
_ => println!("Off."),
}
}
x = 114;
y = 514;
match (x, y):
case (114, 514):
print("On point.")
case (114, another_y):
print("On x (y = {}).", another_y)
case (another_x, 514):
print("On y (x = {}).", another_x)
case default:
print("Off.")
以防有人不懂 Python 的模式匹配,下面再给出 C++ 的等价版本。
import std.core;
int main()
{
const auto x = 114;
const auto y = 514;
if (x == 114 && y == 514)
std::cout << std::format("On point.") << std::endl;
else if (x == 114)
{
const auto another_y = y;
std::cout << std::format("On x (y = {}).", another_y) << std::endl;
}
else if (y == 514)
{
const auto another_x = x;
std::cout << std::format("On y (x = {}).", another_x) << std::endl;
}
else
std::cout << std::format("Off.") << std::endl;
}
除了 match 语句和 if let 语句,Rust 的模式匹配还能用在其他地方,例如定义变量时。
fn main() {
let (x, y) = (114, 514);
}
import std.core;
int main()
{
const auto [x, y] = std::tuple(114, 514);
}
可以认为模式匹配就是一种解包。Rust 中,模式匹配分为两种:
不可驳模式匹配(irrefutable pattern)。要求模式(x, y
)完全匹配右侧表达式((114, 514)
)对应类型((i32, i32)
)的所有取值。
let 语句是典型的不可驳模式匹配。
可驳模式匹配(refutable pattern)。允许模式(例如 Some(x)
)不匹配表达式(例如 an_option
)对应类型(例如 Option
)的所有取值(例中包括 Some(...)
和 None
)。
if let 语句是典型的可驳模式匹配。
模式还有很多形式,也还有很多场景可以使用模式匹配。
while
语句的语法为:
::=
| ...
::= "while"
while
语句在语法上充当表达式的角色,但其值永远都是 ()
。
loop
就是 while true
,但由于它只可能通过 break
结束,所以 loop
循环本身是可以有值的,这个值通过 break
语句传递。如果要使用 while true
,总是应该替换为 loop
。
fn main() {
const RESULT: i64 = {
let mut i = 0;
let mut j: i64 = 1;
loop {
i = i + 1;
j = j << 1;
if i == 63 {
break j;
}
}
};
println!("{}", RESULT);
}
import std.core;
int main()
{
constexpr int64_t RESULT = []
{
int i = 0;
int64_t j = 1;
int64_t _while_result;
while (true)
{
i = i + 1;
j = j << 1;
if (i == 63)
{
_while_result = j;
break;
}
}
return _while_result;
}();
std::cout << std::format("{}", RESULT) << std::endl;
}
要注意,程序 28 中,无论是 Rust 还是 C++,RESULT
都是在编译时计算得出的。
break
语句只能在 loop
循环中才能把值接在后面,在 while
循环中是不可的。
与 C++ 一样,Rust 的 for 循环作用于可迭代对象。与 match 语句、if let 语句一样,使用 for 循环时需要注意所有权的转移。
fn main() {
let mut array = [1, 1, 4, 5, 1, 4]; // 数组,将在之后讲解。
for v in array { // v: i32
println!("{}", v);
}
for v in &array { // v: &i32
println!("{}", v);
}
for v in &mut array { // v: &mut i32
*v -= 1; // 运算时需要解引用。
}
for v in 114..514 {}
}
import std.core;
int main()
{
auto array = std::array{ 1, 1, 4, 5, 1, 4 };
for (const auto v : array) // 不是引用,转移所有权。
std::cout << std::format("{}", v) << std::endl;
for (const auto& v : array) // 不转移所有权。
std::cout << std::format("{}", v) << std::endl;
for (auto& v : array) // 不转移所有权。
v -= 1;
for (const auto v : std::views::iota(114, 514));
}
Rust 在语法上支持元组。
fn main() {
let tup = (114, 5.14, "114514"); // 使用小括号表示元组。
let (x, y, z) = tup; // 复习模式匹配。
let e0 = tup.0;
let e1 = tup.1;
let e2 = tup.2;
}
import std.core;
int main()
{
const auto tup = std::tuple(114, 5.14, "114514"); // 类型不确定时,必须显式写出 tuple。
const auto [x, y, z] = tup; // 复习结构化绑定。
const auto e0 = std::get<0>(tup);
const auto e1 = std::get<1>(tup);
const auto e2 = std::get<2>(tup);
}
使用元组时,无需引入新的生命周期知识,因为元组是匿名的,这意味着:
fn min_ref<'a>((x, y): (&'a i32, &'a i32)) -> &'a i32 { // 使用小括号表示元组类型。在元素类型中指明生命周期。
// 此处 x, y 是不可驳模式匹配。
if x < y {
return x;
}
return y;
}
fn main() {
let x = 114;
let y = 514;
let x_or_y_ref = min_ref((&x, &y)); // 只有一个参数,类型是元组。
println!("{}", x_or_y_ref);
}
import std.core;
const int* min_ref(std::tuple<const int*, const int*> _tuple) // 使用 tuple 类型表示元组类型。
{
auto& [x, y] = _tuple;
if (*x < *y)
return x;
return y;
}
int main()
{
const auto x = 114;
const auto y = 514;
const auto* x_or_y_ref = min_ref({ &x, &y }); // 类型确定时,可以使用聚合初始化。
std::cout << std::format("{}", *x_or_y_ref) << std::endl;
}
程序 31 和程序 15 没有什么本质上的差别,只是引入了一点点关于元组的新语法。
可以预见,由于结构体不是匿名的,所以会出现如何标记生命周期的问题,学习起来比元组稍微困难一点,所以我们在第六章再学习结构体。原理上,元组和结构体是一样的,都是一系列数据类型的复合。以如何标记生命周期这一问题为分界线,可以认为 Rust 中结构体和元组最大的区别是写出元组类型时相当于必须写出每个元素的类型,而写出结构体类型时只写一个名字,因此,第六章介绍的**元组结构体(tuple struct)**应当被视为一个结构体,尽管引入它的目的只不过是为元组类型定义一个别名。
Rust 中,使用 array
表示静态数组(之后简称为数组),使用 Vec
表示动态数组,与 C++ 完全一样。因此,本节只介绍静态数组 array
,动态数组 Vec
留在标准库那章介绍。
可以说,Rust 的 array
与 C++ 的 std::array
完全一样,但 Rust 在语法上支持 array
,写起来比 C++ 更简单。
fn main() {
let a1 = [1, 1, 4, 5, 1, 4];
let a2: [i32; 6] = [1, 1, 4, 5, 1, 4]; // 指明类型时,初始化列表长度必须和数组长度一样。
// 由于 Rust 不支持隐式类型转换,所以指明类型而不指明长度的数组是没有意义的。
let v1 = a1[0]; // 使用中括号访问数组元素。
let v2;
// v2 = a1[6]; // 默认情况下,编译和运行时均检查数组越界。
unsafe {
v2 = a1.get_unchecked(6); // 要不检查数组越界,代码是 unsafe 的。
}
let a3: [[i32; 2]; 3] = [[1, 2], [3, 4], [5, 6]]; // 多维数组只能用数组的数组表示,方括号一个也不能省。
let v3 = a3[1][2]; // 使用连续的方括号访问多维数组。
}
import std.core;
int main()
{
const auto a1 = std::array{ 1, 1, 4, 5, 1, 4 }; // 根据初始化列表推导全部模板。
const std::array<int, 6> a2 = { 1, 1, 4 }; // 指明类型时,初始化列表长度可以和数组长度不一样,剩余元素补 0(默认构造函数)。
// const auto a3 = std::to_array({ 1, 1, 4 }); // 使用 to_array 可以在指明类型的同时自动推导长度。
const auto v1 = a1.at(0); // 使用 at 函数进行带下标检查的元素访问。
const auto v2 = a1[6]; // 使用中括号访问数组元素。默认情况下,编译和运行时不检查数组越界。
// 编译器可能会警告。Debug 模式下可能会运行时错误。
const std::array<std::array<int, 2>, 3> a3 = { 1, 2, 3, 4, 5, 6 }; // int a3[3][2];
// 大括号一个也不能多。
const auto v3 = a3[2][1]; // 使用连续的方括号访问多维数组。注意顺序。
}
C++ 中,数组和单个数一样,作局部变量时允许不在定义时初始化。作为一门安全的语言,Rust 编译器要如何进行初始化检查?由于追踪每个元素是不可能的,所以 Rust 只承认对整个数组的初始化操作,只有对整个数组赋值才算完成数组的初始化。为此,Rust 引入了一个简单的语法,表示含有 n
个值为 v
的元素。
fn main() {
const n: usize = 114; // 不能用 let。注意类型是 usize。
let v = 514;
let a = [v; n]; // 将含有 114 个值为 514 的数组赋值给 a。
// 实际上没有数组移动的操作,只是让 a 拥有了数组的所有权。
}
import std.core;
int main()
{
constexpr size_t n = 114; // 可以用 const,但不推荐。
const auto v = 514;
std::array<std::decay_t<decltype(v)>, n> a;
a.fill(v);
}
以上 Rust 程序中,[v; n]
表示将 v
Copy n
次,所以 v
对应的类型必须具有 Copy 特征,否则无法通过编译。对于没有 Copy 特征的复杂类型(例如 String
),我们首先需要关注所有权问题和初始化问题。
fn main() {
const n: usize = 114;
let v = "514";
let a: [String; n] = std::array::from_fn(|_| String::from(v));
for s in a {
break;
}
// a 已经失去对数组的所有权。
}
import std.core;
int main()
{
const size_t n = 114; // 可以用 const,但不推荐。
const auto v = "514";
auto a = std::make_unique<std::array<std::string, n>>();
std::for_each(a->begin(), a->end(), [&](std::string& s)
{
// _ 是下标。
s = v;
});
for (auto t = std::move(a); const auto &s : *t)
break;
// a 已经失去对数组的所有权。
}
对比程序 33 和程序 34 可以看出,如果数组元素的类型具有 Copy 特征,那么数组本身就是具有 Copy 特征的,使用时无需考虑所有权问题,因为每个变量名都会拥有一个值。反之,如果数组元素的类型不具有 Copy 特征,那么数组本身也不具有 Copy 特征,需要考虑所有权。
鉴于所有权的概念与 C++ 中 std::unique_ptr
的区别日渐凸显,今后的程序不再使用 std::unique_ptr
解释所有权。
数组切片其实就是 C++ 中的 std::span
,只不过 Rust 在语法层面上支持它。
fn main() {
const N: usize = 114;
let v = 514;
let mut a = [v; N]; // 复习:[v; N],类型是 [T; N]。
let a1 = &a; // 类型为 &[i32; 114]。
let a2 = &mut a[1..2]; // 类型为 &mut [i32],左闭右开。
a2[0] = 114514;
println!("{}", a[1]);
}
import std.core;
int main()
{
constexpr size_t N = 114;
const auto v = 514;
std::array<std::decay_t<decltype(v)>, N> a;
a.fill(v);
const auto& a1 = a; // 类型为 const std::array<...>&。
auto a2 = std::span(a.begin() + 1, a.begin() + 2); // 类型为 std::span,左闭右开。
a2[0] = 114514;
std::cout << std::format("{}", a[1]) << std::endl;
}
Rust 中,切片其实是切片引用的简称,类型记为 &[i32]
。实际上 [i32]
才是切片,但这种切片表示一个数据块,在代码中是不允许直接访问的,就像 C++ 中 new int[114514]
只能用指针指向,不存在一个长度为 sizeof(int) * 114514
的类型包含它。之后,我们总是用“切片”指代“切片引用”。
原理上,要实现切片,只需在编译时知道元素类型,在运行时知道起始地址和结尾地址(或元素个数),C++ 的 std::span
便是如此。虽然从 Rust 的语法看来,切片是一个引用,但其实它内部保存的信息和 C++ 的 std::span
一样,而不仅仅只是一个指针。可见,Rust 的引用不止可以表示一个指针,还可以带有更复杂的信息,这样的引用常被称为胖指针。
最后,我们讨论 &[i32; 114]
和 &[i32]
的区别。与 C++ 一样,前者是一个数组的引用,编译器明确知道数组的元素类型、大小,也明确地知道整个数组在哪里;而后者只在编译时知道元素类型,在运行时记录起始地址和终止地址,其余内容,包括原数组地址、原数组大小,都一概不知。
我们很早以前就接触过 String
:为了讲解所有权,我们用到了 String
这一“复杂类型”。事实上,String
就是 C++ 中的 std::string
。回忆,我们构造 String
的写法是:
String::from("114514")
既然如此,我们自然知道了 "114514"
和 String::from("114514")
不是同一个类型。如果以 C++ 的角度来看,字面量 "114514"
的类型是 const char*
。事实上,Rust 与之类似,不过更高级:Rust 中的字面量的类型等价于 C++ 中的 std::string_view
。
fn main() {
let l: &str = "114514"; // 字面量的类型是 &str。
println!("{}", l.len()); // &str 提供了一系列实用方法。
let mut s = String::from(l);
s.push_str("114514"); // 该方法的参数类型是 &str。
println!("{}", s);
}
import std.core;
int main()
{
const std::string_view l = "114514"; // 调用 std::string_view 的 const char* 构造函数。
std::cout << std::format("{}", l.length()) << std::endl; // std::string_view 提供了一系列实用方法。
auto s = std::string(l);
s.append("114514"); // 该方法的参数类型是 const char*。
std::cout << std::format("{}", s) << std::endl;
}
从程序 36 中,我们又一次感受到了 Rust 的高级,字符串字面量的类型自然就相当于 C++ 中更抽象的 std::string_view
(事实上是 std::u8string_view
,为了让 C++ 程序便于通过编译,此处略去),不会涉及无意义的 const char*
。事实上,不同于 C 或 C++,Rust 中所有的字符串都不应当看作以 \0
结尾,而应当天然地看作起始地址加长度的组合。
从 Rust 的语法来看,String
、&str
与 Vec
(动态数组)、&[T]
(切片)的原理是相同的,&str
在语法上就应当看作一个切片。进而,str
和 [T]
的原理也相同,我们不能定义一个类型为 str
的变量,就像在 C++ 中我们不能用非指针类型保存 new char[114514]
的结果。既然 String
和 &str
在原理上与 Vec
和 &[T]
相同,那为什么在基础语法中就要单独定义 String
和 &str
?这是因为字符串作为最常用的类型之一,需要抽象为一个单独的类型。
字符串的一个重要抽象特征是字符串编码。Rust 中,字符串的编码是 UTF-8,所以不可以使用索引处理字符串或字符串切片本身,需要用到 String
或 &str
的一些方法,甚至一些第三方库,才能正确地处理字符串。例如,可以用 as_bytes()
方法得到 &[u8]
类型的切片,表示字符串内部保存的字节,这样就可以访问某一特定字节了。
虽然不能索引字符串,但是可以对字符串进行切片,不过一旦切片的位置错误,就会出现运行时错误。要正确处理存放自然语言的字符串,首先需要良好地定义字符类型。Rust 定义了字符类型 char
,表示一个 UTF-32 字符。字符字面量使用单引号表示。
fn main() {
let utf8_string = "いいよこいよ";
println!("Length: {}", utf8_string.len()); // 18。
for byte in utf8_string.bytes() {
// byte 的类型是 &u8。
print!("{} ", byte);
}
println!("");
for ch in utf8_string.chars() {
// ch 的类型是 char。
print!("{}", ch);
}
println!("");
}
import std.core;
int main()
{
const std::u8string_view utf8_string = u8"いいよこいよ";
std::cout << std::format("Length: {}", utf8_string.length()) << std::endl; // 18。
for (const auto& byte : utf8_string)
{
// byte 的类型是 const char8_t&。
// std::cout << std::format("{} ", byte);
// 不支持。
}
std::cout << std::endl;
for (auto ch : std::filesystem::path(utf8_string).u32string()) // 转换为 UTF-32 字符串。
{
// ch 的类型是 char32_t。
// std::cout << std::format("{}", ch);
// 不支持。
}
std::cout << std::endl;
}
在程序 37 中,值得注意的是 Rust 的 utf8_string.chars()
,它并没有构造了一个完整的 UTF-32 字符串,而是产生了一个迭代器(事实上 bytes()
方法也是如此)。由于 C++ 处理字符串编码的能力较弱,所以 C++ 程序 37 很难正确反映 Rust 程序 37。
看完程序 36 和程序 37,我们关注一下一些常用的字符串方法。
String::from(...)
:很早就见过。可以从字符串字面量、字符串切片、其他字符串构造新的字符串。len()
:获取字符串的字节数。push_str(...)
:向字符串尾部附加其他字符串。bytes()
:返回一个迭代器,逐字节迭代。chars()
:返回一个迭代器,逐字符迭代。as_bytes()
:将字符串视为一个 &[u8]
切片,便于逐字节访问。push(...)
:向字符串尾部附加一个字符。pop()
:删除字符串最后那个字符。String/&str + &str
:拼接字符串。注意右操作数必须是字符串切片。如果左操作数是 String
,则之后失去所有权。表达式的结果是一个新的 String
。这就是运算符重载。mut String += &str
:等价于 mut String = String + &str
。最后,我们学习一下 Rust 中字符串字面量的相关语法,重点在转义符和原始字符串。
fn main() {
let s1 = "\x31\x31\x34\x35\x31\x34"; // ASCII 字符使用 \x 转义。
println!("{}", s1);
let s2 = "\u{211D}"; // UCS 字符使用 \u{} 转义。
println!("{}", s2);
let s3 = r##"C:\Windows\System32\"##; // 使用 r#""# 表示原始字符串。可以加任意多井号。
println!("{}", s3);
}
import std.core;
int main()
{
const auto s1 = "\x31\x31\x34\x35\x31\x34"; // ASCII 字符使用 \x 转义。
std::cout << s1 << std::endl;
// 无法转义 UCS 字符。
const auto s3 = R"114514(C:\Windows\System32\)114514"; // 使用 R"()" 表示原始字符串。可以在括号和引号间同时加上任意字符串。
std::cout << s3 << std::endl;
}
我们已经写出了不少函数,下面以一个例子简单复习下定义函数的语法。
fn main() {
println!("{}", iiyo_koiyo(0xDEADBEAF)); // 字面量超出 i32 范围,不允许用于 i32 类型的参数。
}
fn iiyo_koiyo(_x: u32) -> i32 { // 实现可以在使用之后。
114514 // 注意复习语句块作为表达式的语法。
}
import std.core;
int32_t iiyo_koiyo([[maybe_unused]] uint32_t _x) // 先声明,后使用。下划线开头的名字表示可以不用。
{
return 114514;
}
int main()
{
std::cout << std::format("{}", iiyo_koiyo(0xDEADBEAF)) << std::endl; // 允许字面量决定类型。
}
另外,可以参见程序 18,复习返回值类型、单元类型的相关语法。
**发散函数(diverge function)**表示永不返回的函数。例如,如果调用某函数一定会发生错误导致程序终止,则该函数属于发散函数。又例如,如果某个函数是永不跳出的死循环,则该函数也是发散函数。
发散函数的语法是将返回值类型写为 -> !
。
fn dead_beaf() -> ! {
loop {}
}
fn main() {
dead_beaf();
}
import std.core;
[[noreturn]] void dead_beaf()
{
while (true);
}
int main()
{
dead_beaf();
std::unreachable(); // 如果发散函数返回,则行为不确定(UB)。
}
Rust 会在编译时检查发散函数是否一定发散,如果不是,则发生编译错误。所以正常情况下发散函数真的不可能返回。
很多编程语言中,使用**异常(exception)处理错误。异常本身是一个很深奥的话题,因为它涉及了跨函数的控制,需要编译器和操作系统做很多工作。例如,在 Windows 中,C++ 的异常系统就是利用 Windows 的结构化异常处理(structured exception handling, SEH)**实现的。
Rust 没有异常系统,而是将问题分为两类:
可恢复错误相对简单,因为本质上它只是多做几次判断,我们首先学习它。不可恢复错误涉及程序需要终止时的行为,我们之后再学习。
Result
类型可恢复错误使用 Result
类型处理。Result
类型在成功时保存结果值,在失败时保存错误代码,所以它是一个枚举类型。
enum Result<T, E> {
Ok(T),
Err(E),
}
要处理 Result
,可以使用 match
语句。
enum error_code_t {
FileNotFound,
PasswordError,
}
fn read_file(file_name: &str, password: &str) -> Result<String, error_code_t> {
if file_name != "114514" {
return Err(error_code_t::FileNotFound); // Err 是枚举类型 Result 的成员。
}
if password != "114514" {
return Err(error_code_t::PasswordError);
}
return Ok(String::from("114514")); // Ok 也是枚举类型 Result 的成员。
}
fn main() {
match read_file("114514", "114514") {
Ok(value) => {
println!("File content: {}", value);
}
Err(error_code) => match error_code {
error_code_t::FileNotFound => {
println!("Error: File not found!");
}
error_code_t::PasswordError => {
println!("Error: Password error!");
}
},
}
}
import std.core;
enum class error_code_t
{
FileNotFound,
PasswordError,
};
std::expected<std::string, error_code_t> read_file(
const std::string& file_name,
const std::string& password)
{
if (file_name != "114514")
return std::unexpected{ error_code_t::FileNotFound }; // std::unexpected 不可省略。
if (password != "114514")
return std::unexpected{ error_code_t::PasswordError };
return "114514";
}
int main()
{
if (const auto value = read_file("114514", "114514"))
{
std::cout << std::format("File content: {}", *value) << std::endl;
}
else
{
switch (value.error())
{
case error_code_t::FileNotFound:
std::cout << "Error: File not found!" << std::endl;
break;
case error_code_t::PasswordError:
std::cout << "Error: Password error!" << std::endl;
break;
default:
std::unreachable();
}
}
}
所谓传播,是指将调用函数得到的 Err
类型返回值继续向外返回。当然可以直接编写如下代码:
match read_file("114514", "114514") {
Ok(value) => {
println!("File content: {}", value);
}
Err(error_code) => return Err(error_code), // 假设函数返回值类型为 Result。
}
但由于错误传播使用得很广,所以当我们希望在调用函数出现错误就直接返回 Err
时,可以直接在函数后加上 ?
。如果成功,将直接得到 T
类型的结果,否则自动返回 Err
,如下所示。
let value = read_file("114514", "114514")?;
println!("File content: {}", value);
要注意,只有可以进行错误传播时,才能用 ?
,若当前函数的返回类型不是 Result
(或 Option
;可以将 Option
看作特殊的 Result
),则不能用 ?
。
之前,我们所有的 main
函数都返回单元类型。如何指定 main
函数的返回值呢?可以将 main
函数的返回值类型指定为 Result<(), E>
,如下所示。事实上,只有能够满足“能够从类型中提取出程序退出代码”的特征,就能作为 main
函数的返回值类型。
fn read_file(file_name: &str, password: &str) -> Result<String, i32> {
if file_name != "114514" {
return Err(1); // Err 是枚举类型 Result 的成员。
}
if password != "114514" {
return Err(2);
}
return Ok(String::from("114514")); // Ok 也是枚举类型 Result 的成员。
}
fn main() -> Result<(), i32> {
let value = read_file("?", "114514")?; // 注意结果是 T,而不是 Result。
println!("File content: {}", value); // 因为上面的结果是 T,所以可以直接用。
return Ok(()); // 不可省略,因为即使返回 (),也要显式写为 Ok(())。
}
要注意,程序 42 中将 i32
错误代码并不是一个标准而正确的做法,此处只是为了解释 ?
的语法。
最后,我们提一下 ?
的独有优势。?
能够自动将返回值中的 Err
类型转换为当前函数返回值对应的 Err
,只要满足“E1
能够转换为 E2
”的特征。
panic!
发生不可恢复错误程序就该崩溃了,在 Rust 中被称为 panic。首先,panic 可以由其他函数触发。
fn main() {
let v = vec![1, 1, 4, 5, 1, 4];
v[6];
}
import std.core;
int main()
{
const auto v = std::vector{ 1, 1, 4, 5, 1, 4 };
v.at(6); // Rust 方括号访问自带下标检查。
}
其次,也可以主动调用 panic!
宏。
fn main() {
panic!("Basketball code {}.", 114514); // 可格式化。
}
import std.core;
int main()
{
std::abort(); // 记住 panic! 是异常结束。
}
如果程序只有一个主线程,则可以按上述方式理解。但子线程中的 panic 只会导致单个子线程被安全销毁。
除了 panic!
,还有其变体 unimplemented!
、todo!
等,它们可以在还没有实现的函数内使用,以让程序具有更强的语义。
如果某个函数专用于 panic,则应该将那个函数的返回值类型设置为 !
。
默认情况下,Rust 程序发生 panic 时会自动输出栈帧信息。通过配置清单文件可以让程序只崩溃,不输出栈帧信息,此处不做详细介绍。
C++ 中,默认不会输出栈帧信息。但可以通过 stacktrace
类手动获取栈帧信息。
fn main() {
panic!();
}
import std.core;
int main()
{
std::cout << std::stacktrace::current() << std::endl;
std::abort();
}
最后,我们来看 Rust 中正确运用错误系统的常用范式。
如果开发者知道一个操作一定成功。例如,对于将字符串转为整数的函数,开发者可以知道输入 "114514"
一定可以成功。这种情况下,可以使用 unwrap
函数直接提取结果,不需要判断是否出错。当然,如果出错,则直接 panic。
如果开发者知道一个操作失败后就必须 panic。例如,对于输入文件名,如果找不到文件,程序就该退出,并且还应该告诉用户一些信息。这种情况下,可以使用 expect
函数尝试直接提取结果。expect
和 unwrap
看上去只相差一条用户提示信息。
fn read_file(file_name: &str, password: &str) -> Result<String, i32> {
if file_name != "114514" {
return Err(1);
}
if password != "114514" {
return Err(2);
}
return Ok(String::from("114514114514"));
}
fn main() {
let password = read_file("114514", "114514").unwrap();
println!("{}", password);
let user_value = read_file("114514", &password).expect("Password error!");
println!("{}", user_value);
}
import std.core;
std::expected<std::string, int> read_file(
const std::string& file_name,
const std::string& password)
{
if (file_name != "114514")
return std::unexpected{ 1 };
if (password != "114514")
return std::unexpected{ 2 };
return "114514114514";
}
int main()
{
const auto password = read_file("114514", "114514").value();
std::cout << std::format("{}", password) << std::endl;
// 不支持。
}
注意在 Rust 程序 46 中,错误代码选用了 i32
类型,而没有选用此前定义过的 error_code_t
,是因为 error_code_t
缺少一些特征,暂时不太适合作为错误代码类型,我们在下一章类型系统中会再讨论这个问题。
如果开发者要对一个 Result
或 Option
作链式处理,可以选用组合器模式。此处不再详细讲解。
Rust 使用 struct
关键字定义结构体。定义的最后无需使用分号。构造结构体时,需要具名给出所有的成员初始值。
fn main() {
struct User {
user_name: String,
password_hash: String,
}
let user1: User;
let user2 = User {
password_hash: String::from("114514"),
user_name: String::from("114514"),
};
}
import std.core;
int main()
{
struct User
{
std::string user_name;
std::string password_hash;
};
User user1;
const auto user2 = User{ // 不同于 Rust,C++ 中要求必须有序。
.user_name = std::string("114514"),
.password_hash = std::string("114514"),
};
}
以上定义语法还可以再简化。
struct User {
user_name: String,
password_hash: String,
}
fn main() {
let password_hash = String::from("114514");
let user1 = User {
password_hash, // 只用写一次名字。注意所有权的转移。
user_name: String::from("114514"),
};
let user2 = User {
user_name: String::from("lbwnb"),
..user1 // 结构体更新语法。只能放在最后,不可再加逗号。注意所有权的转移。
};
}
最后提醒,结构体的可变性是整体的。不能为结构体中的单个成员指定可变性。
最基本的原则是,结构体中各个成员的生命周期是单独管理的,这与前面讲解元组时不矛盾。然而,前面讲解元组时提出,结构体在定义后可以反复使用,所以需要为其中的引用类型标记生命周期。在继续下面的内容前,请先反复复习第二章第 5 节的内容。
与函数类似,为结构体打上生命周期标记分为两步:
'a
,表示结构体本身的生命周期不能比 'a
长(小于等于)。'a
,表示结构体中引用类型的生命周期至少和 'a
一样长(大于等于)。这样,就显式指明了结构体中引用类型的生命周期必须比结构体本身的生命周期长。对于存在引用类型的结构体,必须显式指明其生命周期。
struct StringView<'a> {
string: &'a str,
}
fn main() {
let literal = "114514"; // 本身就是 &str,理论上存储在程序只读区,生命周期与程序本身相同。
let string_view = StringView { string: literal };
println!("{}", string_view.string);
let string_view;
{
let string_value = String::from("114514"); // String 才能保证在右大括号结束生命。
string_view = StringView {
string: &string_value,
};
}
// println!("{}", string_view.string); // 不能再使用 string 成员。
}
为什么不直接让结构体中的所有引用类型成员活得比结构体本身更长,这样就无需额外打标记了?这是因为在其他地方还会用到结构体的生命周期标记,例如为结构体实现方法时。应当将生命周期标记看作结构体名字的一部分。
目前,关于生命周期的知识我们暂时学到这儿,之后还有很多关于生命周期的内容。
元组结构体相当于为元组起别名,而单元结构体就是一个空的结构体。
struct TupleStruct<'a>(i32, &'a str);
struct UnitStruct;
fn main() {
// 语法:在元组的前面加上元组结构体的名字。
let t = TupleStruct(114514, "114514");
// 语法:直接只写名字。
let u = UnitStruct;
}
import std.core;
using TupleStruct = std::tuple<int32_t, std::string_view>;
struct UnitStruct {};
int main()
{
const auto t = TupleStruct(114514, "114514");
const auto u = UnitStruct();
}
Rust 中,方法的定义和实现与结构体的定义是完全分开的。方法放在 impl
块中。
struct Rect {
left: i32,
top: i32,
right: i32,
bottom: i32,
}
// impl 块与 struct 完全分离,可以存在多个。
impl Rect {
// 不以 self 开头的方法称为关联方法(即 C++ 中的静态方法)。约定俗成以 new 为构造器的名称。
// Self 是关键字,表示当前类型。
fn new(left: i32, top: i32, width: i32, height: i32) -> Self {
Rect {
left,
top,
right: left + width,
bottom: top + height,
}
}
// 以 self 开头的方法为非静态方法。self 是关键字。另外,成员函数可以和成员变量同名。
fn width(&self) -> i32 {
self.right - self.left
}
// 将 self 指定为可变,可以修改 self。
fn set_width(&mut self, new_width: i32) {
self.right = self.left + new_width;
}
}
fn main() {
let mut r = Rect::new(114, 514, 114, 514);
r.set_width(114514);
println!("{}", r.width());
}
import std.core;
struct Rect
{
int32_t left;
int32_t top;
int32_t right;
int32_t bottom;
static Rect _new(int32_t left, int32_t top, int32_t width, int32_t height)
{
return Rect{ .left = left, .top = top, .right = left + width, .bottom = top + height };
}
int32_t width(this const Rect& self) // C++23。旨在说明 Rust 中 &self 的含义。
{
return self.right - self.left;
}
void set_width(this Rect& self, int32_t new_width)
{
self.right = self.left + new_width;
}
};
int main()
{
auto r = Rect::_new(114, 514, 114, 514);
r.set_width(114514);
std::cout << std::format("{}", r.width()) << std::endl;
}
对于非静态方法,也可以将第一个参数记为 self
,这样会发生所有权的转移,目前我们暂时没有这种情况的应用场景,也就不讨论了。
但由此可以注意到,方法的第一个参数 &self
或 &mut self
是引用,则必然需要讨论生命周期问题。对于 self
相关的生命周期,编译器在自动推导时会进行特殊处理。先回忆:对于一般的函数,如果返回值是引用类型且只存在一个引用类型的参数,则无需生命标记;但如果存在多个引用类型的参数,则需要手动标记生命周期。事实上,对于一般的函数,编译器帮我们做了下列工作:
因此,对于编译器自动标记生命周期的情况,当存在多个引用类型的参数时,编译器会告诉你它不知道返回的引用应该借用自谁,这时就需要手动指定生命周期标记。
而对于非静态的方法,编译器帮我们做的工作有所不同:
&self
或 &mut self
,则将 &self
或 &mut self
的生命周期赋给所有输出生命周期。这样的设计思路是,对于非静态方法,默认就认为返回的引用类型借用自 self
对象。如果并非如此,则需要手动指定生命周期。
struct Student {
name: String,
}
impl Student {
// 有两个引用类型的参数,但仍然不会报错。
fn name(&self, _: &str) -> &str {
&self.name
}
// 借用自其他参数,必须手动标记生命周期。
fn append_name_to<'a>(&self, another: &'a mut String) -> &'a str {
another.push_str(&self.name);
return another;
}
}
fn main() {
let student = Student {
name: String::from("马老师复活了"),
};
println!("{}", student.name("dummy"));
let mut s = String::from("114514: ");
println!("{}", student.append_name_to(&mut s));
println!("{}", s);
}
至此,我们已经明确学完了编译器自动标注生命周期的原则。之后关于生命周期的难点只剩下手动标记生命周期的方法。
Rust 中的特征(trait)基本上就是 C++ 中的概念(concept),但为了适合 Rust 的编程范式,Rust 中的特征还发挥着 C# 中接口(interface)的作用。虽然应用特征时往往离不开泛型(generics),但我们可以先较为完整地介绍特征,再仔细地了解泛型,以尽早掌握特征带来的抽象能力。事实上,我们很早之前在程序 9 就接触过特征及其少量语法了。当时,我们学习了 Copy 特征;我们完全不需要了解 Copy 特征是什么,只需要知道一个满足 Copy 特征的类型是不用理会所有权转移的。虽然本节中我们将了解部分特征的本质,但使用特征的正确姿势应该是按特征的字面意思理解即可——否则这个特征的设计就是有问题的。
关于特征的学习,我们分为两个部分:
我们首先关注 Rust 中如何定义特征。
trait quackable // 名为 quackable 的特征。
{
fn quack(&self) -> ();
} // 必须具有 self.quack() 方法,且返回值类型为 ()。
fn f(t: impl quackable) // 要求 t 的类型具有该特征。
{
t.quack();
}
fn main() {
// f(114514); // 不满足特征,编译错误。
}
import std.core;
template <typename T>
concept quackable = requires(T t) // 名为 quackable 的特征。
{
{t.quack()} -> std::same_as<void>;
}; // 对于类型为 T 的值,必须具有 quack() 方法,且返回值类型为 void。
void f(quackable auto t) // 要求 t 的类型具有该特征。
{
t.quack();
}
int main()
{
// f(114514); // 不满足特征,编译错误。
}
相比 C++,Rust 定义特征的语法更友好,其中的格式类似于函数声明,比 C++ 的语法更加简单易懂。
如何要求一个特征包含另一个特征?C++ 在定义概念时可以直接在约束表达式中写出希望包含的其他概念,但 Rust 不能如此。在这一点上,Rust 的特征更像 C# 中的接口,通过“继承”其他特征来实现特征的包含。事实上,这种写法被称为特征定义中的特征约束(supertrait),即要求先具有其他特征才有可能具有这个特征。
注
此处的约束不表示真正的继承。例如,设
trait B : A
,一个对象要具有B
特征只需要实现B
要求的方法,而无需实现A
要求的方法;假设只有A
特征没有被实现,编译器的报错将会是“未实现要求的A
特征”,而非“未实现要求的B
特征”。很快我们就会看到这一概念对我们所写程序的影响。
如果一个特征在语义上不应该包含另一个特征(例如 Copy
和 Display
,毫不相干),那么在使用时,如何要求某个类型同时具有多个特征呢?可以使用 +
连接。
trait Hund {
fn x(&self); // 不要忘记 self。
}
// 要具有猫特征,必须具有狗特征。
trait Katze: Hund {
fn y(&self);
}
fn f(cat: impl Katze) {
cat.x();
cat.y();
}
// 注意此处语法。引用符号在括号外,括号内以 impl 开头,使用 + 连接各特征。
fn g(dog_and_cat: &(impl Hund + Katze)) {
dog_and_cat.x();
dog_and_cat.y();
}
fn main() {
// f(114514); // 不满足特征,编译错误。
}
import std.core;
template <typename T>
concept Hund = requires(T t) {
{t.x()} -> std::same_as<void>;
};
template <typename T>
concept Katze = requires(T t) {
Hund<T>; // 要满足猫这个概念,需先满足狗这个概念。
{t.y()} -> std::same_as<void>;
};
void f(Katze auto cat)
{
cat.x();
cat.y();
}
// C++ 不支持在参数列表中用 && 组合概念。
template <typename T>
concept _temp = Hund<T> && Katze<T>;
void g(const _temp auto& dog_and_cat) {
dog_and_cat.x();
dog_and_cat.y();
}
int main()
{
// f(114514); // 不满足特征,编译错误。
}
要进一步使用特征,离不开泛型:C++ 在定义概念(concept)时就必须写一个 template
。但在学习 Rust 的泛型之前,我们先了解一下 Rust 中内置的一些常用特征,以更好地了解 Rust 程序的行为。
程序 8 中,我们使用 clone()
方法克隆了对象。事实上,可以克隆的对象被定义为具有 Clone
特征,Rust 标准库定义 Clone
特征为:
// 摘自标准库。略作修改,仅表示语义。
trait Clone {
fn clone(&self) -> Self; // Self 表示 self 的类型,此前没有介绍。
fn clone_from(&mut self, source: &Self) // 不使用分号表示默认有以下实现,在第八章介绍。
{
*self = source.clone()
}
}
也就是说,只需要为一个类型实现 Clone
特征中的 clone
方法,即可让一个类型的对象可被克隆。此处并没有规定 clone
的具体实现,但一个正常的程序一定是满足语义和规约的。
注
看到这儿,很容易想到,我在我的源代码里为
String
实现一个Copy
特征,String
不就能Copy
了吗?Rust 不允许这么做,称为孤儿规则:为类型A
实现特征T
时,要求A
和T
至少有一个在当前作用域定义。
我们在程序 9 就接触了复制特征 Copy
。当时,我们说 Copy
的含义是“赋值时不会发生所有权转移,而是会隐式生成一份额外拷贝”。事实确实如此,Copy
就表示这个语义,它在 Rust 中不过是一个标记:
// 摘自标准库。
pub trait Copy: Clone {
// Empty.
}
需要注意,此处的“额外拷贝”指的是逐字节拷贝,正如标准库中注释所说:
/// Types whose values can be duplicated simply by copying bits.
/// Copies happen implicitly, for example as part of an assignment `y = x`. The behavior of
/// `Copy` is not overloadable; it is always a simple bit-wise copy.
也就是说,Copy
特征会直接影响编译器的行为,在赋值时直接发生逐字节拷贝,与其父特征 Clone
的实现无关。但由于 Clone
是其父类型,所以必须实现:
/// [`Clone`] is a supertrait of `Copy`, so everything which is `Copy` must also implement
/// [`Clone`]. If a type is `Copy` then its [`Clone`] implementation only needs to return `*self`
/// (see the example above).
前面的程序 46 讨论了可恢复错误的处理方式,使用了 Result
的 unwrap
和 expect
方法。当时提到,由于此前自定义的错误代码枚举类型缺少一些特征,所以无法作为 Result
的 E
类型。事实上,问题主要出在无法使用 unwrap
和 expect
方法,这两个方法要求错误代码类型具有调试特征。
调试特征的名字是 Debug
,全称是 std::fmt::Debug
,其定义如下:
pub trait Debug {
fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}
标准库中的介绍如下:
/// `?` formatting.
///
/// `Debug` should format the output in a programmer-facing, debugging context.
///
/// Generally speaking, you should just `derive` a `Debug` implementation.
///
/// When used with the alternate format specifier `#?`, the output is pretty-printed.
///
/// For more information on formatters, see [the module-level documentation][module].
///
/// [module]: ../../std/fmt/index.html
///
/// This trait can be used with `#[derive]` if all fields implement `Debug`. When
/// `derive`d for structs, it will use the name of the `struct`, then `{`, then a
/// comma-separated list of each field's name and `Debug` value, then `}`. For
/// `enum`s, it will use the name of the variant and, if applicable, `(`, then the
/// `Debug` values of the fields, then `)`.
简而言之:该特征的作用是输出适合调试时查看的字符串,能够反应对应类型的状态,具体的输出内容由实现的 fmt
方法决定。可以想到,当 unwrap
或 expect
的断言失败时,程序要输出相应对象的状态才能便于我们调试,所以这两个方法要求类型具有 Debug
特征也就不奇怪了。如果我们要手动输出其中的状态,格式说明符应当记为 {:?}
或 {:#?}
,后者会自动换行,更便于查看。
但是,难道我们要手动为每个类型实现 fmt
方法?这太麻烦了。Rust 中,可以用下面的方法快速实现 Debug
特征。
#[derive(Debug)] // 在 enum 或 struct 前加上这句话即可。
enum ErrorType {
FilenameError,
PasswordError,
}
fn read_file(file_name: &str, password: &str) -> Result<String, ErrorType> {
if file_name != "114514" {
return Err(ErrorType::FilenameError);
}
if password != "114514" {
return Err(ErrorType::PasswordError);
}
return Ok(String::from("114514114514"));
}
fn main() {
let password = read_file("114514", "114514").unwrap();
println!("{}", password);
let user_value = read_file("114514", &password).expect("Password error!");
println!("{}", user_value);
}
程序 54 中,#[derive(...)]
是一个宏,作用是自动实现 Rust 默认提供给我们的特征,称为派生特征。有关宏的知识我们会在第七章进行学习。很容易想到,并不是所有特征都能够被自动实现,只有下面这些特征能用这种语法让 Rust 帮我们实现:
Debug
特征。Clone
特征。必须所有成员都可以 Clone
时才能自动克隆。Copy
特征。必须所有成员都可以 Copy
时才能自动复制。基于此,我们也就能用 #[derive(Clone)]
、#[derive(Copy)]
来分别实现 Clone
和 Copy
特征。需要注意,尽管 Clone
特征是 Copy
特征的“父特征”,但使用 #[derive(...)]
宏时,想要让类型具有 Copy
特征,必须同时派生 Clone
特征,因为 trait B : A
语法并不表示“继承”,而是表示“约束”。
#[derive(Debug, Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 114, y: 514 };
println!("{:#?}", p1); // 实现了 Debug 特征,可行。
let p2 = p1.clone(); // 实现了 Clone 特征,可行。
let mut p3 = p1;
p3.x = 114514;
p3.y = 114514;
println!("{:#?}", p1); // 实现了 Copy 特征,可行。
println!("{:#?}", p3);
}
程序 55 中,删除任一派生特征都会让程序无法通过编译。
泛型就是 C++ 中的模板,其重要性不言而喻。
Rust 中,定义泛型时,一般直接在名字后面用尖括号包含泛型形参列表;而使用泛型时,一般在名字后先写 ::
,再用尖括号包含泛型实参列表。
#[derive(Clone, Copy)]
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer_point = Point::<i32> { x: 114, y: 514 }; // 注意类型名后的 ::。
let float_point = Point::<f32> {
x: 114.514, // Rust 不要求 32 位浮点字面量额外加上后缀。
y: 514.114, // 其类型类似于 {float},而非具体类型。
};
let auto_point = Point {
x: 114514,
y: 114514,
};
println!("{}{}", integer_point.x, integer_point.y);
println!("{} {}", float_point.x, float_point.y);
println!("{} {}", auto_point.x, auto_point.y);
}
import std.core;
template<typename T>
struct Point
{
T x;
T y;
};
int main()
{
const auto integer_point = Point<int32_t>{ .x = 114, .y = 514 };
// C++ 要求为 float 类型字面量加上 f 后缀。
const auto float_point = Point<float>{ .x = 114.514f, .y = 514.114f };
// C++17 自动推导结构体模板类型时,不能指定成员变量名。
const auto auto_point = Point{ 114514, 114514 };
std::cout << std::format("{}{}", integer_point.x, integer_point.y) << std::endl;
std::cout << std::format("{} {}", float_point.x, float_point.y) << std::endl;
std::cout << std::format("{} {}", auto_point.x, auto_point.y) << std::endl;
}
泛型亦可用于枚举,例如 Option
的实现为:
enum Option<T> {
Some(T),
None,
}
与 C++ 不同,Rust 在实例化泛型函数前就会检查方法调用是否合理,因此 Rust 的泛型函数总是离不开特征。
trait Quackable {
fn quack(&self);
}
// T 必须具有 Quackable 特征,才能调用 quack 方法。
fn quack<T: Quackable>(obj: &T) {
obj.quack();
}
// T 必须具有 Quackable 特征,才能调用 quack 方法。
fn quack_twice<T>(obj: &T)
where
T: Quackable,
{
obj.quack();
obj.quack();
}
struct Duck;
impl Quackable for Duck {
fn quack(&self) {
println!("Quack!");
}
}
fn main() {
let duck = Duck {};
quack(&duck);
duck.quack();
quack_twice(&duck);
}
import std.core;
template <typename T>
concept Quackable = requires(T t) {
{t.quack()} -> std::same_as<void>;
};
// T 无需有 Quackable 特征,就能调用 quack 方法。
// 加上可以增加可读性和可维护性。
template <Quackable T>
void quack(const T& obj) {
obj.quack();
}
// T 无需有 Quackable 特征,就能调用 quack 方法。
// 加上可以增加可读性和可维护性。
template <typename T>
void quack_twice(const T& obj) {
obj.quack();
obj.quack();
}
struct Duck {
void quack() const {
std::cout << std::format("Quack!") << std::endl;
}
};
int main() {
const auto& duck = Duck{};
quack(duck);
duck.quack();
quack_twice(duck);
}
Rust 程序 57 中,出现了一个新的关键字 where
,其含义和适用条件完全等价于 C++ 程序 57 中的第二个 requires
关键字。
为泛型结构体提供实现时,注意需要写两次模板,其原因可以参见下面的 C++ 程序。
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
return Self { x, y }; // 复习:定义结构体。
}
}
fn main() {
let p = Point::new(114, 514);
}
import std.core;
template <typename T>
struct Point {
T x;
T y;
static Point<T> _new(T x, T y);
};
template<typename T>
Point<T> Point<T>::_new(T x, T y) {
return Point{ .x = x, .y = y };
}
int main() {
const auto& p = Point<int>::_new(114, 514);
}
Rust 的泛型亦可特化。由于 Rust 中结构体的定义和实现完全分离,所以泛型结构体的偏特化比 C++ 优美。
struct Point<T> {
x: T,
y: T,
}
// 特化 T = i32,实现 new 函数。
impl Point<i32> {
fn new(x: i32, y: i32) -> Self {
return Self { x, y };
}
}
fn main() {
let p = Point::new(114, 514);
}
import std.core;
template <typename T>
struct Point {
T x;
T y;
};
// 特化 T = int,实现 _new 函数。
// 由于定义与实现不分离,所以必须书写两次。
template <>
struct Point<int> {
int x;
int y;
static Point<int> _new(int x, int y) {
return Point{ .x = x, .y = y };
}
};
int main() {
const auto& p = Point<int>::_new(114, 514);
}
程序 59 是全特化。还可以利用特征对一系列希望有实现的类型进行偏特化。
struct Point<T> {
x: T,
y: T,
}
// 偏特化浮点数,实现 new 函数。
// 需添加依赖项 num = "*"。
impl<T: num::Float> Point<T> {
fn new(x: T, y: T) -> Self {
return Self { x, y };
}
}
fn main() {
let p = Point::new(114.514, 514.114);
}
import std.core;
template <typename T>
struct Point {
T x;
T y;
};
// 偏特化浮点数,实现 new 函数。
template <std::floating_point T>
struct Point<T> {
T x;
T y;
static Point<T> _new(T x, T y) {
return Point{ .x = x, .y = y };
}
};
int main() {
const auto& p = Point<double>::_new(114.514, 514.114);
}
我们可以不显式地写出返回值的类型,而只指定返回值应当具有的特征。但由于 Rust 中不会发生隐式类型转换,所以返回值的类型实际上是由 return
语句决定的,函数签名只是提供了一个显式的参考。那为返回值使用泛型有什么用呢?答案是:以丢失具体类型为代价,使返回值的类型写起来更短。
trait Point {
fn norm(&self) -> i32;
}
struct PointTypeWithTwoDimensions {
x: i32,
y: i32,
}
impl Point for PointTypeWithTwoDimensions {
fn norm(&self) -> i32 {
return self.x * self.x + self.y * self.y;
}
}
fn make_point(x: i32, y: i32) -> impl Point {
PointTypeWithTwoDimensions { x, y }
}
fn main() {
let p = make_point(114, 514);
// 只知道 p 是 Point,哪怕编译器知道其实 p 是 PointTypeWithTwoDimensions。
println!("{}", p.norm());
}
import std.core;
template <typename Self>
concept Point = requires(const Self& self) {
{ self.norm() } -> std::same_as<int>;
};
struct PointTypeWithTwoDimensions {
int x, y;
int norm() const {
return x * x + y * y;
}
};
static_assert(Point<PointTypeWithTwoDimensions>);
Point auto make_point(int x, int y) {
return PointTypeWithTwoDimensions{ x, y };
}
int main() {
const auto p = make_point(114, 514);
// 你和编译器都知道 p 是 PointTypeWithTwoDimensions。
std::cout << std::format("{}", p.norm()) << std::endl;
}
const 泛型就是 C++ 中的 template
。
struct MyArray<T, const N: usize> {
data: [T; N],
}
fn main() {
let a = MyArray::<i32, 10> { data: [0; 10] };
}
template <typename T, size_t N>
struct MyArray {
T data[N];
};
int main() {
const auto a = MyArray<int, 10>{
.data = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
};
}
虽然 Rust 不是面向对象的语言,但它用其独特的语法为面向对象的概念提供了部分支持。
第六章介绍了 Rust 中的特征,并将其与 C++ 中的概念作了比较。但显然,Rust 的特征与 C++ 的概念不完全相同:Rust 中的特征只能指定一系列函数,而 C++ 的约束表达式(require expression)指定的却是一系列表达式。从这一点来讲,Rust 的特征更类似于 Java 中的接口(interface)。
接口的一个重要特征便是允许具有默认实现。下面用 Rust、C++、C# 作演示(也可以把 C# 换成 Java, Kotlin 等等)。
trait Runnable {
fn run(&self) {
println!("I'm running.")
}
}
struct Runner; // 复习:单元结构体。
impl Runnable for Runner {
fn run(&self) {
println!("As a runner, I'm running.")
}
}
struct Orange;
impl Runnable for Orange {}
fn main() {
// 复习:单元结构体的初始化可以只写名字,也可以写空的大括号。
let runner = Runner;
let orange = Orange {};
runner.run();
orange.run();
}
// 2023-6-21,MSVC 已经支持 import std;
import std;
// C++ 中没有接口,使用类模拟接口的行为。注意虽然定义了虚函数,但并没有用到多态。
struct Runnable {
virtual void run() const {
std::cout << std::format("I'm running") << std::endl;
}
};
struct Runner : Runnable {
void run() const override {
std::cout << std::format("As a runner, I'm running.") << std::endl;
}
};
struct Orange : Runnable {};
int main() {
const auto runner = Runner{};
const auto orange = Orange{};
runner.run();
orange.run();
}
// 使用顶级语句。
// C# 中,接口与类分离,只能利用多态。即:Runner 是不会 run 的,只有 Runnable 会。
Runnable runner = new Runner();
Runnable orange = new Orange();
runner.run();
orange.run();
interface Runnable
{
void run()
{
Console.WriteLine("I'm running.");
}
}
class Runner : Runnable
{
// 重写默认实现时,需要用 <接口名>.<方法名> 显式指定。
void Runnable.run()
{
Console.WriteLine("As a runner, I'm running.");
}
}
class Orange : Runnable { }
要注意,Rust 中并不支持继承。数据的继承可以用内部类模拟,而方法的继承则可以用特征的特征约束(supertrait)模拟。在涉及多个接口的实现时,会优先调用类自己的方法,如果不存在才会调用所实现的特征的方法(不像 C# 程序 63,不允许隐式调用接口的方法)。如果总是希望调用所实现的特征的方法,需要指定具体特征名。当然,真实的程序中一定要避免这种情况。
trait IFoo {
fn name(&self) -> &str;
}
trait IBar {
fn name(&self) -> &str;
}
struct Orange {
name: String,
}
// 实现特征的方法。
impl IFoo for Orange {
fn name(&self) -> &str {
"Orange.IFoo"
}
}
impl IBar for Orange {
fn name(&self) -> &str {
"Orange.IBar"
}
}
// 实现结构体自己的方法。
impl Orange {
fn name(&self) -> &str {
&self.name
}
}
fn main() {
let orange = Orange {
name: String::from("Orange"),
};
// 调用类自身的方法。
println!("{}", orange.name());
// 显式调用特征的方法。
println!("{}", IFoo::name(&orange));
println!("{}", IBar::name(&orange));
}
import std;
struct IFoo {
virtual std::string_view name() const = 0;
};
struct IBar {
virtual std::string_view name() const = 0;
};
struct Orange : IFoo, IBar {
// C++ 中,不允许成员变量和成员函数同名。
std::string _name;
// 存在虚函数,无法聚合初始化。
Orange(std::string_view name = {}) : _name(name) {}
// 无法定义与虚函数同名的非虚函数。
// 实现基类的方法。
std::string_view IFoo::name() const override
{
return "Orange.IFoo";
}
std::string_view IBar::name() const override
{
return "Orange.IBar";
}
};
int main() {
const auto orange = Orange{ "Orange" };
// 命名冲突时,C++ 中只能通过将对象转换为基类解决冲突。
// 注意,因为调用的是虚函数,所以此处涉及查找虚函数表。
std::cout << std::format("{}", static_cast<const IFoo&>(orange).name()) << std::endl;
std::cout << std::format("{}", static_cast<const IBar&>(orange).name()) << std::endl;
}
// 使用顶级语句。
var orange = new Orange("Orange");
// 调用类自身的方法。
Console.WriteLine(orange.name());
// C# 中总是只能通过将对象转换为接口来调用接口的方法。
Console.WriteLine(((IFoo)orange).name());
Console.WriteLine(((IBar)orange).name());
interface IFoo
{
string name();
}
interface IBar
{
string name();
}
class Orange : IFoo, IBar
{
string _name;
// C# 中,需显式定义构造函数。
public Orange(string name)
{
_name = name;
}
// 实现类自己的方法。
public string name() => _name;
// 实现特征的方法。
string IFoo.name() => "Orange.IFoo";
string IBar.name() => "Orange.IBar";
}
通常,我们称多态的函数调用为动态分发(dynamic dispatch),相对的概念则称为静态分发(static dispatch)。C++ 中,(常规的)运行时多态必须借由虚函数表实现,Rust 中也是如此。要在 Rust 中实现动态分发,需要使用 dyn
关键字。
trait Runnable {
fn run(&self);
}
struct Orange;
impl Runnable for Orange {
fn run(&self) {
println!("Orange is running.");
}
}
// 复习:Rust 程序 6。
fn static_run(runnable: &impl Runnable) {
runnable.run();
}
fn dynamic_run(runnable: &dyn Runnable) {
runnable.run();
}
fn main() {
let orange = Orange;
static_run(&orange);
dynamic_run(&orange);
}
import std;
template <typename Self>
struct RunnableTrait {
void run() const {
static_cast<const Self*>(this)->run();
}
};
struct RunnableInterface {
virtual void run() const = 0;
};
struct StaticOrange : RunnableTrait<StaticOrange> {
void run() const {
std::cout << std::format("StaticOrange is running.") << std::endl;
}
};
struct Orange : RunnableInterface {
void run() const override {
std::cout << std::format("Orange is running.") << std::endl;
}
};
template <typename T>
void static_run(const RunnableTrait<T>& runnable) {
runnable.run();
}
void dynamic_run(const RunnableInterface& runnable) {
runnable.run();
}
int main() {
const auto static_orange = StaticOrange{};
const auto orange = Orange{};
static_run(static_orange);
dynamic_run(orange);
static_orange.run();
orange.run();
}
程序 65 清楚地说明了 Rust 的特征具有概念和接口两重属性,无需多解释了。
那如何存储对象使得它们表现出多态呢?难点在于,特征作为接口时,它并不是一个类型,不能直接以特征为类型声明变量。解决方法是使用智能指针 Box
。
trait ExclaimTrait {
fn exclaim(&self);
}
struct Foo;
impl ExclaimTrait for Foo {
fn exclaim(&self) {
println!("Foo!");
}
}
struct Bar;
impl ExclaimTrait for Bar {
fn exclaim(&self) {
println!("Bar!");
}
}
fn exclaim_static(x: &impl ExclaimTrait) {
x.exclaim();
}
fn exclaim_1(x: &dyn ExclaimTrait) {
x.exclaim();
}
// 不能写 &Box。
// 从逻辑上来说这就是不对的,暂时不讨论 Rust 语法是如何限制这一点的。
fn exclaim_2(x: Box<dyn ExclaimTrait>) {
(*x).exclaim();
}
fn main() {
let known_foo = Box::new(Foo);
let known_bar = Box::new(Bar);
// 具体类型可以自动转换为特征对象。
let unknown_foo: Box<dyn ExclaimTrait> = Box::new(Foo);
let unknown_bar: Box<dyn ExclaimTrait> = Box::new(Bar);
exclaim_static(&*known_foo);
exclaim_static(&*known_bar);
exclaim_1(&*known_foo);
exclaim_1(&*known_bar);
// 不允许。语法上的具体错误此处暂不讨论。
// exclaim_static(&*unknown_foo);
exclaim_1(&*unknown_foo);
exclaim_1(&*unknown_bar);
// 不能借用 Box,不会自动转换类型。
// exclaim_borrow_box(&known_foo);
// 转移 Box 所有权。
exclaim_2(known_foo); // 自动转换类型。
exclaim_2(unknown_foo);
}
import std;
struct ExclaimTrait {
virtual void exclaim() const = 0;
};
struct Foo : ExclaimTrait {
void exclaim() const override {
std::cout << std::format("Foo!") << std::endl;
}
};
struct Bar : ExclaimTrait {
void exclaim() const override {
std::cout << std::format("Bar!") << std::endl;
}
};
// 静态分发的展示见程序 C++ 程序 65。
void exclaim_1(const ExclaimTrait& x) {
x.exclaim();
}
// 不能写 const std::unique_ptr&。
// 从逻辑上来说这就是不对的。
void exclaim_2(std::unique_ptr<ExclaimTrait> x) {
x->exclaim();
}
int main() {
auto known_foo = std::make_unique<Foo>();
auto known_bar = std::make_unique<Bar>();
// 具体类型可以自动转换为基类。
std::unique_ptr<ExclaimTrait> unknown_foo = std::make_unique<Foo>();
std::unique_ptr<ExclaimTrait> unknown_bar = std::make_unique<Bar>();
exclaim_1(*known_foo);
exclaim_1(*known_bar);
// 不能传递 unique_ptr 引用,不会自动转换类型。
// exclaim_ref_unique_ptr(known_foo);
// 转移 unique_ptr。
exclaim_2(std::move(known_foo));
exclaim_2(std::move(unknown_foo));
}
程序 66 清楚地说明了 Rust 的 Box
就是 C++ 中的 std::unique_ptr
,无需多解释了。Rust 智能指针的语法原理可以参见本章第 3 节 RAII 范式。
需要特别注意的是,前面我们曾用 std::unique_ptr
解释 Rust 中的所有权机制,但学到一定水平后,我们也清楚地指出,Rust 的所有权不是智能指针,之后的程序也不会再用 C++ 的 std::unique_ptr
来反映 Rust 的所有权机制。在这里学到 Box
后,我们可以知道 Rust 中也存在智能指针,并且智能指针和所有权机制确实不是一个东西。分析程序 66 时,需要注意哪些地方涉及所有权,哪些地方涉及智能指针。
Rust 中,没有原生的语法支持继承。使用 Rust 进行面向对象程序设计时,只需要时时刻刻考虑每个类型所能提供的特征。在明确了一个类型应当具有的特征后,再通过内部类的形式实现代码重用。
没有继承的坏处便是需要全部重写父类型的方法。幸运的是,Rust 的宏编程(见本章第 4 节)功能非常强大,有一些库可以稍微减少由此导致的无意义代码。
函数式编程最主要的特征是允许将函数作为参数、返回值,或赋值给变量。
首先我们来看一下嵌套函数。
fn main() {
fn local_function(s : &str) {
println!("{}, I'm local function.", s);
}
let mut another = local_function;
let yet_another = &local_function; // 函数类型总具有 Copy 特征。
local_function("114");
another("514");
yet_another("114514");
}
import std;
int main() {
constexpr auto local_function = [](std::string_view s) {
std::cout << std::format("{}, I'm local function.", s) << std::endl;
};
auto another = local_function;
const auto& yet_another = local_function;
local_function("114");
another("514");
yet_another("114514");
}
据此我们可以知道,Rust 的嵌套函数仅仅将函数的作用域限定在另一个函数内,并不能起到捕获变量的作用;如果尝试将函数赋值给变量,则变量的类型与该函数紧密联系,就像 C++ 程序 67 一样,任何其他函数都不能赋值给 another
。
如果希望用一个变量保存任何满足指定签名的函数,应该如何实现?Rust 中需要利用 Fn
特征来存储这样的函数,因为所有用 fn
定义的函数都满足 Fn
特征。Fn
特征是一个具有模板的特征,使用方法如程序 68 所示。
// 复习泛型:Rust 中,定义泛型列表时往往直接在名字后面写。
type FunctionType<T> = dyn Fn(T, T) -> T;
fn main() {
fn add(x: i32, y: i32) -> i32 {
x + y
}
fn mul(x: i32, y: i32) -> i32 {
x * y
}
let mut func: Box<FunctionType<i32>>;
func = Box::new(add);
println!("{}", func(114, 514));
func = Box::new(mul);
println!("{}", func(114, 514));
}
import std;
template <typename T>
using FunctionType = T(T, T);
int main() {
constexpr auto add = [](int x, int y) {
return x + y;
};
constexpr auto mul = [](int x, int y) {
return x * y;
};
std::function<FunctionType<int>> func;
func = std::function(add);
std::cout << std::format("{}", func(114, 514)) << std::endl;
func = std::function(mul);
std::cout << std::format("{}", func(114, 514)) << std::endl;
}
需要提前注意,之所以说 Box
类似于 std::function
,是因为前者和后者一样,也可以存储捕获了变量的闭包(马上讲解);但 Rust 中,与 Fn
同属一个系列的特征还有两个,以应对闭包带来的借用检查问题,这比 std::function
复杂许多。
我们其实在程序 34 中见过闭包的基本语法。因此,下面我们重点关注闭包带来的变量捕获问题。
fn demo() {
let mut s = String::from("114514");
// print 的类型是 impl Fn() -> i32。
let print = || -> i32 {
println!("{}", s);
114514
};
print();
}
fn main() {
demo();
}
import std;
void demo() {
auto s = std::string("114514");
const auto print = [&]() -> int {
std::cout << std::format("{}", s) << std::endl;
return 114514;
};
print();
}
int main() {
demo();
}
Rust 中,闭包总是自动按引用捕获所有变量。因此,当我们尝试将闭包作为返回值时,需要考虑各个变量的生命周期,以下代码是不被允许的:
fn demo() -> impl Fn() -> i32 {
let mut s = String::from("114514");
// print 的类型是 impl Fn() -> i32。
let print = || -> i32 {
println!("{}", s);
114514
};
print // error[E0373]: closure may outlive the current function, but it borrows `s`, which is owned by the current function
}
fn main() {
demo()();
}
这样的问题在 C++ 中也会同样出现,但 C++ 的编译器却不帮我们做这个检查。
import std;
auto demo() {
auto s = std::string("114514");
const auto print = [&]() -> int {
std::cout << std::format("{}", s) << std::endl;
return 114514;
};
return print;
}
int main() {
demo()(); // 运行时什么也没输出,不合预期。
}
因此,当把闭包作为返回值时,如果涉及局部变量的捕获,一定需要取得局部变量的所有权。在 Rust 中,通过在闭包定义前加上 move
关键字来实现所有权的转移。
fn demo() -> impl Fn() -> i32 {
let mut s = String::from("114514");
// print 的类型是 impl Fn() -> i32。
let print = move || -> i32 {
// 使用到 s,于是编译器将 s 的所有权转移到闭包中。
println!("{}", s);
114514
};
// s 已失去所有权。
print
}
fn main() {
demo()();
}
import std;
auto demo() {
auto s = std::string("114514");
const auto print = [=]() -> int {
// 使用到 s,于是编译器将 s 复制赋值到闭包中。
std::cout << std::format("{}", s) << std::endl;
return 114514;
};
// s 已被复制。
return print;
}
int main() {
demo()();
}
当然,Rust 程序 70 中,所有权转移并不会发生 clone
,而 C++ 程序 70 中发生了一次复制赋值。如果把这个复制赋值换成移动赋值,C++ 程序 70 就和 Rust 程序 70 基本一样了。
Rust 严格规定,如果闭包会修改任何捕获变量,则这个闭包就是可变的,声明变量时需要使用 mut
。具有修改行为闭包不再满足 Fn
特征,而是会满足名为 FnMut
的特征,后者是前者的必要条件。
fn demo() {
let mut s = String::from("114514");
// print 的类型是 impl FnMut() -> i32。
// 此处的 mut 关键字不能省略。
let mut print = || -> i32 {
s.push_str("1919810");
println!("{}", s);
114514
};
print();
print();
}
fn main() {
demo();
}
import std;
auto demo() {
auto s = std::string("114514");
// C++ 中,此处 const 可以继续保留,因为 C++ 认为修改的是引用指向的内容。
const auto print = [&]() -> int {
s.append("1919810");
std::cout << std::format("{}", s) << std::endl;
return 114514;
};
print();
print();
}
int main() {
demo();
}
程序 71 是在局部区域内复用代码的常见方法,因此对比 Rust 程序 71 和 C++ 程序 71 便可能觉得 Rust 要求 print
闭包必须是 mut
有点奇怪。但如果同时用 move
关键字让闭包取得捕获变量的所有权,这个要求就一点也不奇怪了,因为调用结构体的 &mut self
方法当然要求结构体是 mut
的。
fn demo() -> impl FnMut() -> i32 {
let mut s = String::from("114514");
// print 的类型是 impl FnMut() -> i32。
// 此处的 mut 关键字可以省略,仅当调用了它才需要可变。
// 注意,Rust 中所有权的转移不涉及是否可变,这不是返回引用。
let print = move || -> i32 {
s.push_str("1919810");
println!("{}", s);
114514
};
print
}
fn main() {
// 此处 mut 不可省略,demo 的返回值只满足 FnMut 特征,调用时必须是 mut 的。
let mut t = demo();
t();
t();
}
import std;
auto demo() {
auto s = std::string("114514");
// 此处的 mutable 关键字不可省略,否则捕获变量不可变。
// 此处可以有 const 关键字,仅当调用了它才需要不是 const。
// 注意,返回它时不涉及是否是 const,这不是返回引用。
const auto print = [=]() mutable -> int {
s.append("1919810");
std::cout << std::format("{}", s) << std::endl;
return 114514;
};
return print;
}
int main() {
// 此处不能是 const,调用不是 const 的。
auto t = demo();
t();
t();
}
如果调用闭包会导致任何捕获变量失去所有权,则这个闭包只能调用一次。原因很简单:第二次调用时,捕获变量已经失去所有权了,所以无法执行关于该捕获变量的代码。
这种只能调用一次的闭包不再满足 Fn
特征或 FnMut
特征,只满足 FnOnce
特征,后者是前两者的必要条件。
fn demo() -> impl FnOnce() -> i32 {
let mut s = String::from("114514");
// print 的类型是 impl FnOnce() -> i32。
let print = move || -> i32 {
s.push_str("1919810");
println!("{}", s);
// 取走 s 的所有权。
drop(s); // 见下一节《RAII 范式》。
114514
};
print
}
fn main() {
// 尽管调用 t 会导致状态发生变化,但无需使用 mut。
let t = demo();
// 调用 t() 后,t 直接失去所有权。
t();
}
import std;
auto demo() {
auto s = std::string("114514");
// 此处的 mutable 关键字不可省略,否则捕获变量不可变。
const auto print = [=]() mutable -> int {
s.append("1919810");
std::cout << std::format("{}", s) << std::endl;
// 将 s 置于不确定的状态,不应在下次赋值前继续使用。
const auto _ = std::move(s);
return 114514;
};
return print;
}
int main() {
// 此处不能是 const,调用不是 const 的。
auto t = demo();
// 调用 t 后,理论上不可再调用,但编译器不做检查。
t();
}
综上,Rust 可以根据闭包对捕获变量的操作自动将可调用对象分为由强到弱的三个类型,进而允许借用检查器帮助我们检查调用是否合理。
Rust | Rust 特征 | C++ | |
---|---|---|---|
嵌套函数 | fn foo() {}; |
Fn |
constexpr auto foo = [] {}; |
不可变闭包 | `let foo = | {};`(闭包函数体需读取捕获变量) | |
取得所有权的不可变闭包 | `let foo = move | {};`(闭包函数体需读取捕获变量) | |
可变闭包 | `let mut foo = | {};`(闭包函数体需修改捕获变量) | |
取得所有权的可变闭包 | `let mut foo = move | {};`(闭包函数体需修改捕获变量) | |
单次闭包 | `let foo = | {};`(闭包函数体需取走捕获变量的所有权) | |
取得所有权的单次闭包 | `let foo = move | {};`(闭包函数体需取走捕获变量的所有权) |
注意:
move
关键字取得捕获变量所有权无关,只与闭包函数体中对捕获变量的操作有关。let
语句定义闭包时不一定需要可变。上表假设了定义闭包后需要立即用 foo();
调用闭包。最后,我们来看一下闭包的参数。与 C++ 不同,Rust 常常根据上下文推导类型。
fn main() {
let logic = false;
let logic_or = |another| logic || another;
// 推导出 another 的类型为 bool,因为只有 bool 类型才能作逻辑或运算。
// 只有一句返回值时,可以省略大括号。
let float_add = |another| 1.14 + (another as f64);
// 推导出 another 的类型为 f64,因为 5.14 是 f64。
println!("{}", float_add(5.14));
// println!("{}", float_add(514)); // 参数类型 i32 与此前推导出的 f64 不匹配,编译错误。
let int_add = |another: i32| 114 + another;
// 手动指明 another 的类型为 i32。
}
import std;
int main() {
const auto logic = false;
const auto logic_or = [&](bool another) { return logic || another; };
// 只能手动指定 another 的类型。
constexpr auto float_add = [](double another) { return 1.14 + another; };
// 只能手动指定 another 的类型。
std::cout << std::format("{}", float_add(5.14)) << std::endl;
// C++ 允许类型隐式转换。
std::cout << std::format("{}", float_add(514)) << std::endl;
constexpr auto int_add = [](int another) { return 114 + another; };
// 只能手动指定 another 的类型。
}
Rust 不支持泛型闭包,即闭包参数的类型无法指定为 impl ...
。
实现了 Drop 特征的结构体就是具有析构函数的结构体。
struct Foo(i32); // 复习:元组结构体。
struct Bar(i32);
impl Drop for Foo {
fn drop(&mut self) {
println!("drop Foo({})", self.0);
}
}
impl Drop for Bar {
fn drop(&mut self) {
println!("drop Bar({})", self.0);
}
}
fn main() {
let f = Foo(114);
let b = Bar(514);
}
import std;
struct Foo {
int _0;
~Foo() {
std::cout << std::format("destruct Foo({})", _0) << std::endl;
}
};
struct Bar {
int _0;
~Bar() {
std::cout << std::format("destruct Bar({})", _0) << std::endl;
}
};
int main() {
const auto f = Foo(114);
const auto b = Bar(514);
}
通常,析构函数的作用是回收资源,且析构函数只会被调用一次。如果析构函数被多次调用,则程序的行为通常是不确定的。然而,C++ 中却允许我们手动调用析构函数,调用方法形如 对象名.~类名()
。这几乎总是会导致运行时错误,因为离开当前作用域时析构函数总是还会再被调用一次。因此,Rust 不允许我们调用 对象名.drop()
。
如果真的希望提前析构对象该怎么办?除了突兀地打上一个大括号,Rust 中还允许我们使用全局的 drop
函数析构对象。drop
函数实际上取走了对象的所有权,并且让编译器知道对象已经可以被回收。
struct Foo(i32);
impl Drop for Foo {
fn drop(&mut self) {
println!("drop Foo({})", self.0);
}
}
fn main() {
let f = Foo(114);
drop(f);
println!("end of main");
}
import std;
struct Foo {
int _0;
~Foo() {
std::cout << std::format("destruct Foo({})", _0) << std::endl;
}
};
int main() {
auto f = std::make_unique<Foo>(114);
f.reset();
std::cout << std::format("end of main") << std::endl;
}
C++ 程序 68 用 unique_ptr
模拟了 Rust 中对象因 drop
函数丧失所有权而回收,从而发生析构,实际上 Rust 程序 68 并不涉及智能指针(复习:Box
)。如果在程序 68 中不使用 C++ 中的智能指针,而是直接调用析构函数,析构函数就会被调用两次,导致 "destruct Foo(114)"
被输出两次:这并不符合预期。
一旦一个类型具有 Drop
特征,即具有析构函数,就不应该允许它进行逐字节拷贝了,即 Drop
和 Copy
这两个特征是互斥的。这就好比 C++ 中你不应该对一个不满足 is_trivial
的类型调用 memcpy
函数一样。
最后,不要忘记元组和结构体的成员的所有权都是单独管理的,所以可以使用 drop
函数手动析构它们的成员。相对应的,数组的元素无法被 drop
,只能通过 drop
整个数组使得所有数组元素被回收。
并发、异步。
理论上,全局变量越少越好,以防形成一盘散沙之势。然而,全局变量通常也是无法避免的,例如在使用单例设计模式时。