关键词:Rust


References:

未完,待施工…

前言

什么是 Rust

Rust 是一门新的静态编译编程语言,其功能定位与 C++ 相似,它的 1.0 版本于 2015 年发布。

rustc 使用 LLVM 作为它的后端。

Rust 支持多种平台和架构:x86、ARM、WebAssembly、Linux、Mac、Windows……

Rust 被广泛用于各种设备中:

  • 固件和引导程序
  • 智能显示器
  • 手机
  • 桌面
  • 服务器

Rust 和 C++ 适用于类似的场景:

  • 极高的灵活性。
  • 高度的控制能力。
  • 能够在资源匮乏的设备(如手机)上运行。
  • 没有运行时和垃圾收集。
  • 关注程序可靠性和安全性,而不会牺牲任何性能。

Rust 系统由许多工具组成:

  • rustc:Rust 编译器,将 .rs 文件转换为二进制文件和其他中间格式。
  • cargo:Rust 依赖项管理器和构建工具(包管理工具)。负责下载依赖项并在构建项目时传递给编译器。其还附带内置的测试运行程序,用于执行单元测试。
  • rustup:Rust 工具链安装和更新工具。当更新版本时,其用于安装并更新 rustccargo。还可用于下载标准库的文档,同时安装多个 Rust 版本。

Rust 区分版本。这些版本支持对语言进行向后不兼容的更改。

  • 为防止破坏代码,版本是可选的: 通过 Cargo.toml 文件为 crate 选择合适的版本。
  • 为免分割生态系统,Rust 编译器可以混合使用为不同版本编写的代码。

Rust 的优势

编译时内存安全:在编译时可防止所有类内存 bug。

  • 不存在未初始化的变量。
  • 不存在“双重释放”。
  • 不存在“释放后使用”。
  • 不存在 NULL 指针。
  • 不存在被遗忘的互斥锁。
  • 不存在线程之间的数据竞争。
  • 不存在迭代器失效。

没有未定义的运行时行为:每个 Rust 语句的行为都有明确定义。

  • 数组访问有边界检查。
  • 整数溢出有明确定义(panic 或回绕)。

现代语言功能:具有与高级语言一样丰富且人性化的表达能力。

  • 枚举和模式匹配。
  • 泛型。
  • 无额外开销的外部函数接口(FFI)。
  • 零成本抽象。
  • 强大的编译器错误提示。
  • 内置依赖管理器。
  • 对测试的内置支持。
  • 优秀的语言服务协议(Language Server Protocol)支持。

Windows 下安装 Rust

可执行二进制文件下载地址:

点击运行 rustup-init.exe

安装路径提示

显示默认安装在 C 盘。

  • 此处已做修改,自定义安装路径。

自定义安装路径步骤如下:

  1. 创建自定义文件夹,我此处为 D:/rust。在内创建两个文件夹,分别为 .cargo.rustup

创建文件夹

  1. 配置环境变量:把新建的文件夹添加到环境变量中。

CargoHome

RustupHome

  1. PATH 环境变量中加入上两个变量。

添加到PATH

至此再打开 rustup-init.exe,观察路径是否修改。

接着输入 1 执行默认安装即可。

在 VSCode 上使用 Rust

步骤如下:

  1. 在扩展中查找并安装 rust-analyzerNative Debug 两个插件。
  • 另外两个推荐插件:
    • Even Better TOML,支持 .toml 文件完整特性
    • Error Lens,更好的获得错误展示
  1. 新建代码文件夹,在终端使用命令生成工程:
1
cargo new hello
  1. 编写代码后,在终端使用命令运行:
1
2
cargo build
cargo run

第一个 Rust 程序

每一个语言一开始会有它的 Hello World。

1
2
3
4
fn main()
{
println!("Hello, world!");
}
  • 函数以 fn 开头。
  • 代码块以 { 开头,以 } 结尾。
  • Rust 有卫生宏,println! 就是一个例子。
    • 这意味着它们不会意外地捕获它们所在作用域中的标识符。
  • Rust 字符串是 UTF-8 编码,可以包含 Unicode 字符。

下载依赖卡顿问题

解决方法是:覆盖默认的镜像地址

$HOME/.cargo/config.toml 添加以下内容:

1
2
3
4
5
[source.crates-io]
replace-with = 'ustc'

[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"
  • 创建一个新的镜像源 [source.ustc],然后将默认的 crates-io 替换成新的镜像源: replace-with = 'ustc'

Rust 基础入门

速览语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Rust 程序入口函数,跟其它语言一样,都是 main,该函数目前无返回值
fn main() {
// 使用let来声明变量,进行绑定,默认a是不可变的
// 此处没有指定a的类型,编译器会默认根据a的值为a推断类型:i32,有符号32位整数
let a = 10; // 语句的末尾必须以分号结尾

// 主动指定b的类型为i32
let b: i32 = 20;

// 可以在数值中带上类型:30i32表示数值是30,类型是i32
// 声明变量时添加 mut 表示变量是可变的,mut是mutable的缩写
let mut c = 30i32;

// 还能在数值和类型中间添加一个下划线,让可读性更好
let d = 30_i32;

// 跟其它语言一样,可以使用一个函数的返回值来作为另一个函数的参数
let e = add(add(a, b), add(c, d));

// println!是宏调用,看起来像是函数但是它返回的是宏定义的代码块
// 该函数将指定的格式化字符串输出到标准输出中(控制台)
// {}是占位符,在具体执行过程中,会把e的值代入进来
println!("( a + b ) + ( c + d ) = {}", e);
// println! 会自动推导出具体的类型,因此无需手动指定输出类型
}

// 定义一个函数,输入两个i32类型的32位有符号整数,返回它们的和
fn add(i: i32, j: i32) -> i32 {
// 返回相加值,这里可以省略return
// 不添加 ;,表示返回 i + j,添加则表示返回空。
i + j
}

变量绑定与解构

Rust 默认变量是不可修改的,这使得程序运行时性能上的提升。

在命名方面,和其它语言没有区别,不过当给变量命名时,需要遵循 Rust 命名规范

变量绑定

在其他语言中,如 int a = 1 称为赋值。但 Rust 中, let a = 1 称为 变量绑定

绑定一词源自 Rust 的所有权问题。

  • 任何内存对象都是有主人的(Owner),对象完全属于它的主人。
  • 绑定就是把这个对象绑定给一个变量,使变量成为它的主人。

变量的不可变与可变

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let x = 5;
println!("Hello, world! {}", x);
x = 6; // 错误:x 是不可变的
println!("Hello, world! {}", x);

let mut y = 10;
println!("Hello, world! {}", y);
y = 11; // 正确
println!("Hello, world! {}", y);
}

这种不可变的优势是:一个变量往往被多处代码所使用,其中一部分代码假定该变量的值永远不会改变,而另外一部分代码却无情的改变了这个值,在实际开发过程中,这个错误是很难被发现的,特别是在多线程编程中。

忽略未使用的变量

Rust 的高安全性会认为不使用的变量可能会是个 BUG,所以进行警告。

使用下划线 _ 开头的变量名会被忽略。

1
2
3
fn main {
let _x = 1;
}

变量解构

let 不仅可以进行变量的绑定,还可以进行复杂变量的解构。

  • 从一个相对复杂的变量中,匹配出该变量的一部分内容。
1
2
3
4
5
6
7
fn main() {
// 匹配 a 和 b
let (a, mut b): (bool, bool) = (true, false);
println!("Hello, world! {} {}", a, b);
b = true;
println!("Hello, world! {} {}", a, b);
}

不可变量与常量

Rust 默认的是不可变量。常量是经过 const 修饰的量,且在 Rust 中值类型必须标注。

1
2
3
4
5
6
7
fn main() {
let a = 123; // 可以编译,但可能有警告,因为该变量没有被使用
let a = 456;

const b: i32 = 1;
let b = 2; // 错误:常量不能被重新赋值
}

变量遮蔽

像上面写过的,Rust 允许声明相同的变量名,且后声明的变量名会遮蔽前面声明的。

1
2
3
4
5
6
7
8
9
10
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("x = : {x}"); // 12
}

println!("x = : {x}"); // 6
}
  • 这个程序首先将数值 5 绑定到 x,然后通过重复使用 let x = 来遮蔽之前的 x,并取原来的值加上 1,所以 x 的值变成了 6。第三个 let 语句同样遮蔽前面的 x,取之前的值并乘上 2,得到的 x 最终值为 12。
  • 这和 mut 变量的使用是不同的,第二个 let 生成了完全不同的新变量,两个变量只是恰好拥有同样的名称,涉及一次内存对象的再分配,而 mut 声明的变量,可以修改同一个内存地址上的值,并不会发生内存对象的再分配,性能要更好。

变量遮蔽的用处,在于在某个作用域内无需再使用之前的变量时,就可以重复的使用变量名字,而不用绞尽脑汁去想更多的名字。

基本类型

内置数据类型有:

类型 字面量
有符号整数 i8i16i32i64i128isize -1001_000123_i64
无符号整数 u8u16u32u64u128usize 012310_u16
浮点数 f32f64 3.14-10.0e202_f32
Unicode 标量类型 char 'a''α''∞'
布尔值 bool truefalse
单元类型 () 其唯一的值也是 ()
  • 数字中的下划线均可忽略,仅方便辨识,即 1_000 可以写成 1000123_i64 等价 123i64

每种类型占用空间为:

  • iNuNfN 占用 NN 位。
  • isizeusize 占用一个指针大小的空间。
  • char 占用 4 个字节,32 位空间。
  • bool 占用 1 个字节,8 位空间。

类型推导与标注:编译器必须在编译期知道所有变量的类型,但这不意味着你需要为每个变量指定类型。

  • Rust 编译器可以根据变量的值和上下文中的使用方式来自动推导出变量的类型。
  • 在某些情况下,Rust 编译器无法推导出变量类型,需要手动去给予一个类型标注。

数值类型

整数运算溢出

关于运算时整数溢出:

1
2
3
4
5
6
7
fn mul(a: i16, b: i16, c: i16) -> i16
{
return a * b * c;
}

let _product = mul(100, 200, 300);
// 尝试与溢出相乘,有符号整数16位的最大值为65535
  • 在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 panic(崩溃,Rust 使用这个术语来表明程序因错误而退出)。

可以显式处理溢出,通过标准库针对原始数字类型提供的方法:

  • wrapping_* 在所有模式下都按照补码循环溢出规则,如 wrapping_add
  • checked_* 在发生溢出时返回 None 值。
  • overflowing_* 返回该值和一个指示是否存在溢出的布尔值。
  • saturating_* 可以限定计算后的结果不超过目标类型的最大值或低于最小值。
1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let a: u8 = 255;
let b = a.wrapping_add(20);
let c = a.checked_add(20);
let d = a.overflowing_add(20);
let e = a.saturating_add(20);
println!("{a}"); // 255
println!("{b}"); // 19
println!("{c:?}"); // None
println!("{d:?}"); // (19, true)
println!("{e}"); // 255
}
浮点数陷阱

浮点数的使用需要谨慎,原因:

  1. 浮点数往往是数字的近似表达(并不是精确的)。
  2. 浮点数在某些特性上时反直觉的(比如比较时)。

所以:

  • 避免在浮点数上判断相等;
  • 当结果在数学上存在未定义时需要小心。

一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
let abc: (f32, f32, f32) = (0.1, 0.2, 0.3);
let xyz: (f64, f64, f64) = (0.1, 0.2, 0.3);

println!("abc (f32)");
println!(" 0.1 + 0.2: {:x}", (abc.0 + abc.1).to_bits());
// 3e99999a
println!(" 0.3: {:x}", (abc.2).to_bits());
// 3e99999a
println!();

println!("xyz (f64)");
println!(" 0.1 + 0.2: {:x}", (xyz.0 + xyz.1).to_bits());
// 3fd3333333333334
println!(" 0.3: {:x}", (xyz.2).to_bits());
// 3fd3333333333333
println!();

assert!(abc.0 + abc.1 == abc.2);
assert!(xyz.0 + xyz.1 == xyz.2);
}
NaN

对于数学上未定义的结果,比如负数取平方根,会返回一个 NaN表示。

  • 所有与 NaN 交互的操作都会返回一个 NaN
  • NaN 并不能用于比较。

可以使用 is_nan() 方法判断是否为 NaN

数字运算

Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和取模运算。

更多,运算符如下:

运算符 示例 解释 是否可重载
! ident!(...), ident!{...}, ident![...] 宏展开
! !expr 按位非或逻辑非 Not
!= var != expr 不等比较 PartialEq
% expr % expr 算术取模 Rem
%= var %= expr 算术取模与赋值 RemAssign
& &expr, &mut expr 借用
& &type, &mut type, &'a type, &'a mut type 借用指针类型
& expr & expr 按位与 BitAnd
&= var &= expr 按位与及赋值 BitAndAssign
&& expr && expr 逻辑与
* expr * expr 算术乘法 Mul
*= var *= expr 算术乘法与赋值 MulAssign
* *expr 解引用
* *const type, *mut type 裸指针
+ trait + trait, 'a + trait 复合类型限制
+ expr + expr 算术加法 Add
+= var += expr 算术加法与赋值 AddAssign
, expr, expr 参数以及元素分隔符
- - expr 算术取负 Neg
- expr - expr 算术减法 Sub
-= var -= expr 算术减法与赋值 SubAssign
-> fn(...) -> type, \|...\| -> type 函数与闭包,返回类型
. expr.ident 成员访问
.. .., expr.., ..expr, expr..expr 右排除范围
.. ..expr 结构体更新语法
.. variant(x, ..), struct_type { x, .. } “与剩余部分”的模式绑定
... expr...expr 模式: 范围包含模式
/ expr / expr 算术除法 Div
/= var /= expr 算术除法与赋值 DivAssign
: pat: type, ident: type 约束
: ident: expr 结构体字段初始化
: 'a: loop {...} 循环标志
; expr; 语句和语句结束符
; [...; len] 固定大小数组语法的部分
<< expr << expr 左移 Shl
<<= var <<= expr 左移与赋值 ShlAssign
< expr < expr 小于比较 PartialOrd
<= expr <= expr 小于等于比较 PartialOrd
= var = expr, ident = type 赋值/等值
== expr == expr 等于比较 PartialEq
=> pat => expr 匹配准备语法的部分
> expr > expr 大于比较 PartialOrd
>= expr >= expr 大于等于比较 PartialOrd
>> expr >> expr 右移 Shr
>>= var >>= expr 右移与赋值 ShrAssign
@ ident @ pat 模式绑定
^ expr ^ expr 按位异或 BitXor
^= var ^= expr 按位异或与赋值 BitXorAssign
\| pat \| pat 模式选择
\| expr \| expr 按位或 BitOr
\|= var \|= expr 按位或与赋值 BitOrAssign
\|\| expr \|\| expr 逻辑或
? expr? 错误传播
序列

即基于范围,如 1..5 生成从1到4的连续数字,左闭右开,常用于循环中。

  • 1..=5 即可表示全闭区间。
1
2
3
4
5
6
fn main() {
for i in 1..5 {
print!("{} ", i);
}
// 1 2 3 4
}
使用 As 完成类型转换

使用 As 来完成一个类型到另一个类型的转换,其最常用于将原始类型转换为其他原始类型。

有理数与复数

社区开发的高质量 Rust 数值库:num

导入也十分简单:

  1. 创建工程:cargo new complex-num
  2. Cargo.toml 中的 [dependencies] 添加 num = "0.4.0"
  3. 导入 numuse num::Complex;
  4. cargo build && cargo run

实例代码:

1
2
3
4
5
6
7
8
9
10
use num::complex::Complex;

fn main() {
let a = Complex { re: 2.1, im: -1.2 };
let b = Complex::new(11.1, 22.2);
let result = a + b;

println!("{} + {}i", result.re, result.im);
// 13.2 + 21i
}

字符、布尔、单元类型

字符:

  • 所有的 Unicode 值都可以作为 Rust 字符。
  • Unicode 为 4 字节编码,故字符类型也占用 4 个字节。

布尔类型:

  • 占用 1 字节。

单元类型:

  • 就是 (),唯一的值也为 (),可以理解为 void
  • main 函数返回的就是单元类型。
  • 可以用 () 作为 map 的值,表示不关注具体的值,只关注 key

Rust中没有返回值的函数称为发散函数,即无法收敛的函数。

语句与表达式

Rust 的语句和表达式:

  • 语句会执行一些操作但是不会返回一个值
  • 表达式会在求值后返回一个值。
1
2
3
let mut x = 0; // 语句
x = x + 1; // 语句
x + 1 // 表达式

表达式总要返回值,且不能包含分号

  • 带上分号则变成一条语句。

let 也是语句,故不能将 let 语句赋值给其他值。

表达式可以成为语句的一部分,例如 let y = 6 中,6 就是一个表达式,处理后后返回一个值 6

能返回值,它就是表达式。

函数

大概的格式如:

1
fn <函数名> ( <参数> ) <函数体>
  • 规范上,函数名和变量名需要使用蛇形命名法。
  • 函数的位置随便,不在乎定义在何处。

Rust 中定义函数如果需要具备参数 必须声明参数名称和类型

1
2
3
4
5
6
7
8
9
10
fn main()
{
fun(5, 6);
}

fn fun(x: i32, y: i32)
{
println!("x 的值为 : {}", x);
println!("y 的值为 : {}", y);
}

Rust 函数声明返回值类型的方式:

  • 在参数声明之后用 -> 来声明函数返回值的类型。
1
2
3
4
fn five() -> i32
{
5
}
  • 在函数体中,随时都可以以 return 关键字结束函数运行并返回一个类型合适的值。
1
2
3
4
fn ten() -> i32
{
return 10;
}

当函数无返回值时:

  • 返回一个 ()
  • 通过 ; 结尾的语句返回一个 ()

上面的发散函数,可以用 ! 作返回类型,表示永不返回。

  • 这种语法往往用作会导致程序崩溃的函数。
1
2
3
fn eend() -> ! {
panic!();
}

所有权和借用

所有权

Rust 使用 所有权系统

对于内存管理方面,计算机语言出现了三种流派:

  • 垃圾回收机制(GC):程序运行时不断寻找不再使用的内存,代表有 Java、Go;
  • 手动管理内存:通过调用函数方式进行申请和释放内存,代表有 C++;
  • 通过所有权管理内存:编译器在编译时会根据一系列规则进行检查。
    • 检查只发生在编译器,所有在运行期并不会有性能上的损失。

C 语言上的不安全:

1
2
3
4
5
6
7
int * fun()
{
int a;
a = 100;
char *c = "xyz";
return &a;
}
  • 上述代码可以编译通过。
  • a 为局部变量,当离开作用域时,其内存会被系统回收,从而返回成了悬挂指针。
  • c 的值时常量,存储在常量区,其生命周期为整个程序运行期间,当程序结束系统才会回收这片内存。
栈与堆

栈:先进后出。栈中的所有数据都必须占用已知且固定大小的内存空间。

堆:可存放对于大小未知或可能变化的数据。

  • 当往堆上放入数据时,请求一定大小的内存空间,OS在堆的某处寻找并标记区域为已使用,返回该地址的指针。这个过程称为在堆上分配内存(分配)。
  • 指针会被推入栈中,通过栈中的指针获取在堆上的数据。

两者在性能上:在栈上分配内存比在堆上分配内存要快,处理器在栈上分配数据会比在堆上分配数据更高效。

当代码调用函数时,传递给函数的参数(可能指向堆上数据的指针和函数的局部变量)依次入栈;当调用结束时,这些值按相反顺序出栈。

堆上的数据是缺乏组织的,所以管理堆上数据的分配和释放尤为重要。

  • 当没有及时释放时,便造成了内存泄漏(数据无法被回收)。
所有权的原则

原则如下:

  1. Rust 中每一个值都被一个变量所拥有,该变量称为值的所有者(Owner)。
  2. 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者。
  3. 当变量(所有者)离开作用域时,这个值将被丢弃。
变量绑定背后的数据交互

了解深拷贝和浅拷贝。

简单来说,深拷贝在内存上独立,复制内容在新的内存空间上。浅拷贝在内存上共享。比如把A复制到B,如果是深复制,则A和B独立互不影响;如果是浅复制,在修改A,B也会改变。

拷贝(浅拷贝)

浅拷贝只发生在栈上,如:

1
2
let x = 5;
let y = x;

基本类型在编译时是已知大小,会存储在栈上,所以拷贝其值是快速的。

Rust 具有 Copy 的特征,可以用在类似整型这样在栈中存储的类型。

  • 如果拥有 Copy 特征,则一个旧变量在被赋值给其他变量后仍可用,即赋值的过程是拷贝的过程。

规则:

  • 任何基本类型的组合可以 Copy
  • 不需要分配内存或某种形式的资源的类型是可以 Copy

一些 Copy 的类型:

  • 所有整数类型;
  • 所有浮点数类型;
  • 布尔类型;
  • 字符类型;
  • 包含的类型都可 Copy 的元组;
  • 不可变引用 &T
    • &mut T 是可变引用,不可以 Copy
克隆(深拷贝)

Rust 永远不会自动创建数据的深拷贝。

  • 任何自动的复制都不是深拷贝。

当需要深度复制数据时,使用方法 clone()

  • 使用 clone() 会降低程序性能
转移所有权

有代码如下:

1
2
let x = 5;
let y = x;
  • 这是浅拷贝,且没有发生所有权的转移。
  • 整个过程的赋值都是通过浅拷贝方式完成,发生在栈中,所以不需要所有权转移。

另有代码如下:

1
2
let s1 = String::from("hello");
let s2 = s1;
  • String 为字符串类型,是一个复杂类型,由存储在栈中的堆指针、字符串长度、字符串容量共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存(跟 C++ 的 std::vector 类似)。
    • 即不会自动拷贝。
  • 此时处理方式为:当 s1 被赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop(丢弃) 任何东西。
    • 把所有权从 s1 转移给了 s2s1 在被赋予 s2 后就马上失效了。
    • 这种类似于移动语义的机制,C++ 的 std::move()

如果真的有两个所有者,那么当 s1s2 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free) 的错误。

函数传值与返回

将值传递给函数,一样会发生移动或者赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效
// println!("s value: {}", s); // 报错,s 已经移出作用域

let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,
println!("x value: {}", x); // 但 i32 是 Copy 的,所以在后面可继续使用 x
}
// 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 所以不会有特殊操作

fn takes_ownership(some_string: String) {
// some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) {
// some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

对于返回的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值移给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2);
// s2 被移动到 takes_and_gives_back 中,它也将返回值移给 s3

println!("s1: {:?}", s1);
// println!("s2: {:?}", s2); // s2 被移走
println!("s3: {:?}", s3);
}
// 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 移出作用域并被丢弃

/// gives_ownership 将返回值移动给调用它的函数
/// #### 返回值
/// 返回一个 String 的所有权
fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string
}

/// takes_and_gives_back 将传入字符串并返回该值
/// #### 参数
/// * `a_string` - 要被移动给调用者的 String
/// #### 返回值
/// 将接收到的 String 的所有权返回出去
fn takes_and_gives_back(a_string: String) -> String {
// a_string 进入作用域

a_string // 返回 a_string 并移出给调用的函数
}

由于所有权,所以总是把一个值传来传去来使用它。

引用和借用

Rust 具有 借用 这一概念。

  • 借用:获取变量的引用。
    • 如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。
引用和解引用

常规引用是一个指针类型,指向了对象存储的内存地址。

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = &x;

assert_eq!(5, x);
assert_eq!(5, *y);
}
  • 使用 * 进行解引用。
不可变引用

有如下代码:

1
2
3
4
5
6
7
8
9
10
11
fn calculate_length(s: &String) -> usize {
s.len()
}

fn main() {
let s1 = String::from("hello"); // 构造一个字符串
let len = calculate_length(&s1);

println!("{} {}", s1, len);
// hello 5
}
  • 此处函数传入字符串,但无需再用返回的方式传出所有权。
  • 函数参数为 &String
  • & 在此处表示引用,允许使用值,但不获取所有权。
  • &s1 为指向 s1 的引用,但不拥有它。

这样借用可以进行对变量一定的访问。

可变引用

当然也可以试着修改借用的变量。(得寸进尺)

1
2
3
4
5
6
7
8
9
10
11
fn change(string: &mut String) {
string.push_str(", 🌏");
}

fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s);
// hello, 🌏
// 由于 Unicode 编码,当然 🌏 也可以显示
}
  • s 是可变类型,&mut s 是可变的引用,string: &mut String 是接收可变引用的参数。

但是,可变引用同时只能存在一个

1
2
3
4
5
6
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // 报错
// print!("{} {}", r1, r2);
}
  • 可变借用 r1 持续到最后一次使用的位置(即输出);
  • r1 存活间,又尝试创建第二个可变借用 r2 是会引起出错的。

这种限制的好处就是使 Rust 在编译期就避免数据竞争。数据竞争会导致未定义行为,这种行为很可能超出我们的预期,难以在运行时追踪,并且难以诊断和修复。

数据竞争可由以下行为造成:

  • 两个或更多的指针同时访问同一数据
  • 至少有一个指针被用来写入数据
  • 没有同步数据访问的机制

可以通过加大括号限制作用域的方式解决部分问题。引用的作用域从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号

还有一件事,可变引用与不可变引用不能同时存在。(太安全了吧QAQ)

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut s = String::from("hello");

let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s;
// 大问题,不能将`s`借用为可变的,因为它也被借用为不可变的

println!("{}, {}, and {}", r1, r2, r3);
}
  • 可以理解为,正在借用不可变引用的用户,肯定不希望借用的东西莫名其妙被改了。
NLL

NLL:Not-Lexical-Lifetimes,一种 Rust 编译器优化行为。

  • 用于找到某个引用在作用域 } 结束前就不再被使用的代码位置。
悬垂引用

悬垂引用也叫做悬垂指针。

  • 指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。

如在 C++ 中,易见的悬挂:

1
2
3
4
5
6
7
8
9
10
11
12
int* fun()
{
int a = 10;
return &a;
}

int main()
{
int aa = *fun();
cout << aa;
return 0;
}
  • 这其实是可以编译通过,但函数 fun 返回的指针式悬挂的。
    • fun 中的 a 离开作用域时,a 会被释放,但 fun 返回的指针仍然指向 a

在 Rust 中编译器可以确保 引用永远也不会变成悬垂状态

  • 当获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用。(🐂牛)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let r1 = dangle();
let r2 = no_dangle();
}

fn dangle() -> &i32 {
// 报错:该函数返回了一个借用的值,但是已经找不到它所借用值的来源
let a = 10;
&a
}

fn no_dangle() -> i32 {
// 正常的,返回一个解引用,即值
let a = 10;
*&a
}
借用规则总结
  • 同一时刻,要么只有一个可变引用,要么有任意数量的不可变引用。
  • 引用必须总是有效的(非悬挂)。

复合类型

复合类型是由其它类型组合而成的,最典型的就是结构体 struct 和枚举 enum

字符串与切片

Rust 的字符串并没有想象中的简单。

切片

切片:允许引用集合中部分连续的元素序列,而不是引用整个集合。

  • 创建切片的语法:[开始索引..终止索引],其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,左闭右开。
1
2
3
4
5
6
7
8
let s = String::from("hello world");

let hello = &s[0..5]; // 等价于 &s[..5]
let world = &s[6..11];

let len = s.len();
let all1 = &s[0..len]; // 完整切片
let all2 = &s[..]; // 完整切片

注意,中文在 UTF-8 中占用三个字节。

字符串切片的类型标识是 &str

一个对初学者难受的代码:

1
2
3
4
5
6
7
8
9
10
fn main() {
let s = String::from("hello world");

let word = first_word(&s);

println!("the first word is: {}", word);
}
fn first_word(s: &String) -> &str {
&s[..1]
}
字符串

Rust 中的字符是 Unicode 类型。

  • 每个字符占据 4 个字节内存空间。

但是在字符串中是 UTF-8 编码。

  • 也就是字符串中的字符所占的字节数是变化的(1 - 4)。

Rust 语言级别上的字符串类型:str

  • 通常以引用类型出现 &str,即字符串切片。
  • str 是硬编码进可执行文件,无法修改。

String 是一个可增长、可改变且具有所有权的 UTF-8 编码字符串。

当提及字符串,往往指的是 String 类型和 &str 字符串切片类型。

  • 除了 String,还有 OsStringOsStrCsStringCsStr 等。
  • 都以 StringStr 结尾,分别对应具有所有权和被借用的变量。
String 与 &str 的转换

&str 类型生成 String

1
2
let s1 = String::from("hello")
let s2 = "hello".to_string();

String 类型转为 &str:取引用

1
2
3
4
5
6
7
8
9
10
fn say_hello(s: &str) {
println!("Hello, {}!", s);
}

fn main() {
let s = String::from("Fingsinz");
say_hello(&s);
say_hello(&s[..2]);
say_hello(s.as_str());
}
字符串索引

字符串底层数据存储是 [u8]

  • 但不能使用索引进行访问字符串的子串。
    • 因为字符串中的字符所占的字节数是变化的。
    • 比如汉字占 3 个字节,而英文占 1 个字节。

以此类推,使用索引进行字符串切片(如 &str[0..2])时也需要格外小心。

字符串的操作

追加:在原有的字符串上追加,并不会返回新的字符串。

  • push(@pos, @char) 追加字符 char
  • push_str(@pos, @str) 追加字符串字面量。
  • 字符串需要是可变的。
1
2
3
4
5
6
7
fn main() {
let mut s = String::from("Hello");
s.insert(5, '!');
println!("{}", s); // Hello!
s.insert_str(6, "World");
println!("{}", s); // Hello!World
}

替换:将字符串中的某个字符串替换成其他字符串。

  • replace(@target, @replacement):将所有的目标字符串替换成新字符串。适用于 String&str
    • 该方法是返回一个新的字符串,而不是操作原来的字符串。
  • replacen(@target, @replacement, @count):将 count 个的目标字符串替换成新字符串。适用于 String&str
    • 该方法是返回一个新的字符串,而不是操作原来的字符串。
  • replace_range(@range, @replacement):将范围内的字符串替换成新字符串。仅适用于 String 类型。
    • 该方法是直接操作原来的字符串,不会返回新的字符串。
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let s = "rust rust rust";
let news1 = s.replace("rust", "Rust");
let news2 = s.replacen("rust", "Rust", 2);
println!("{}", news1); // Rust Rust Rust
println!("{}", news2); // Rust Rust rust

let mut ms = "rust rust rust".to_string();
ms.replace_range(5..6, "R");
println!("{}", ms); // rust Rust rust
}

删除:都仅适用于 String 类型。

  • pop():删除并返回字符串的最后一个字符。
    • 该方法直接操作原来的字符串。
    • 返回值是一个 Option 类型,如果字符串为空,则返回 None
  • remove(@pos):删除并返回字符串指定位置(按字节处理)的字符。
    • 该方法直接操作原来的字符串。
  • truncate(@pos):删除字符串从指定位置(按字节处理)开始到结尾的全部字符。
    • 该方法直接操作原来的字符串。
  • clear():清空字符串。
    • 该方法直接操作原来的字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let mut s1 = "Rrust!".to_string();
let popstr = s1.pop();
println!("{}", popstr.unwrap()); // !

s1.remove(1);
println!("{}", s1); // Rust

s1.truncate(2);
println!("{}", s1); // Ru

s1.clear();
println!("{}", s1); // s1 为空
}

连接:连接字符串。

  • 使用 + 或者 += 连接字符串:要求右边参数必须为字符串的切片引用类型,不能直接传递 String 类型。
    • + 返回一个新的字符串,调用了 add() 方法:fn add(self, s: &str) -> String
  • format! :与 print! 类似。适用于 String&str 类型。
1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let s1 = "Hello ".to_string();
let s2 = "World";
let s3 = s1 + &s2;
// println!("{}", s1); // s1 所有权由函数已经转移给 s3
println!("{}", s2); // World
println!("{}", s3); // Hello World

let mut s4 = s3.clone();
s4 += "!";
println!("{}", s4); // Hello World!
}
字符串转义

通过转移的方式 \ 输出 ASCII 和 Unicode 字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
// 通过 \ + 字符的十六进制表示,转移输出一个字符
let s1 = "\x52\x75\x73\x74";
println!("{}", s1); // Rust

// \u 可以输出一个 unicode 字符
let s2 = "\u{211D}";
println!("{}", s2); // ℝ

// 加 \ 转义不换行
let s3 = "a\
b\
c\
d
e";
println!("{}", s3);
// abcd
// e
}
操作 UTF-8 字符串

逐字符遍历:

  • 避免索引尴尬情况。

逐字节遍历:

  • 遍历字符串的底层字节数组表现形式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fn main() {
let s = "我在用 Rust 编程";

for c in s.chars() {
print!("{}", c);
}
// 我在用 Rust 编程

println!("");

for b in s.bytes() {
print!("{} ", b);
}
// 230 136 145 229 156 168 231 148 168 32 82 117 115 116 32 231 188 150 231 168 139

println!("");

// 如果字符串包含双引号,可以在开头和结尾加 #
let quotes = r#"a"bbb"a"#;
println!("{}", quotes);

// 如果还是有歧义,可以继续增加,没有限制
let longer_delimiter = r###"aaaa"##!"###;
println!("{}", longer_delimiter);
}

如果需要准确从 UTF-8 字符串中获取子串是较为复杂的事情。

  • 比如,想要从 holla中国人नमस्ते 这种变长的字符串中取出某一个子串,使用标准库是做不到的。
    • 需要在 crates.io 上搜索第三方库来寻找想要的功能。
剖析字符串

关于 String 可变,而字符串字面值 str 却不可以:

  • 字面值文本在编译时就知道内容,直接硬编码进可执行文件中。
    • 字面值是不可变的,而字符串是有在程序运行中动态变化的需求。

String 类型支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来管理内容:

  • 在使用 String::from 是就会构造 String类型;
  • } 处会自动调用释放内存函数 drop,用于释放离开作用域的变量。

元组

元组是由多种类型组合到一起形成的。

创建元组的语法如下:

1
let tup: (i32, f64, u8) = (500, 6.4, 1);
解构元组

用同样的形式把一个复杂对象中的值匹配出来或者使用 . 都可以访问元组的元素。

1
2
3
4
5
6
7
8
9
fn main() {
let tup = (500, 6.4, 1, "aaa".to_string());
let (x, y, z, w) = tup;

println!("{}, {}, {}, {}", x, y, z, w); // 500, 6.4, 1

//println!("{}, {}, {}, {}", tup.0, tup.1, tup.2, tup.3);
// 上语句会报错,因为字符串的所有权已经转移到 w,所以 tup.3 不再有效
}
  • 使用 let (x, y, z, w) = tup 进行模式匹配,用相似的模式进行匹配,元组对应的值就会绑定到变量 xyzw 上。

元组常用于函数的返回值上,返回多个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let s1 = "Hello ".to_string();
let s2 = "World";

let (s1, len) = string_splicing(s1, s2);

println!("{}:{}", s1, len);
}

fn string_splicing(s1: String, s2: &str) -> (String, usize) {
let ret = s1 + s2;
let len = ret.len();
(ret, len)
}

结构体

结构体与元组:都是由多种类型组合而成。

  • 但是与元组不同的是,结构体可以为内部的每个字段起一个富有含义的名称。
结构体的语法

一个结构体由几部分组成:

  • 关键字 struct 定义;
  • 清晰的结构体名称;
  • 几个结构体字段。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Student {
id: i32,
name: String,
age: i8,
}

fn main() {
let student = Student {
id: 1,
age: 20,
name: String::from("John"),
};
println!("{}, {}, {}", student.id, student.name, student.age);
}
  • 初始化实例时,每个字段都需要初始化;
  • 初始化时的顺序不必与定义时的顺序一致。
  • 访问结构体字段时使用 .

另外注意,Rust 不支持将结构体的某个字段标记为可变,需要整个结构体声明为可变。

支持简化结构体构建:

1
2
3
fn make_student(id: i32, name: String, age: i8) -> Student {
Student { id, name, age }
}

进行结构体更新时,也是挺方便的:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let student1 = Student {
id: 1,
age: 20,
name: String::from("John"),
};

let student2 = Student {
age: 21,
..student1
};
println!("{} {} {}", student2.id, student2.name, student2.age);
}
  • .. 语法表明凡是没有显式声明的字段,全部从 student1 中自动获取。
  • 需要注意的是 ..student1 必须在结构体的尾部使用。
  • 此处 student1.name 的所有权已经移到 student2,所以 student1.name 不再有效,不能输出,但是其他字段(基本类型)依旧有效。
结构体的内存排序
1
2
3
4
struct File {
name: String,
data: Vec<u8>,
}

File 的内存排序如下:

File struct
name data
String Vec
ptr size capacity ptr size capacity
  • nameptr 指向一块 [u8; name.size] 内存的开头。
  • dataptr 指向另一块 [u8; data.size] 内存的开头。
元组结构体

结构体必须要有名称,但是结构体的字段可以没有名称。

  • 元组结构体:这种结构体长得很像元组,字段没有名称。
1
2
3
4
5
6
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
let p1 = Point(0, 0, 0);
}
单元结构体

单元结构体跟单元类型很像,没有任何字段和属性。

  • 当定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用单元结构体。
结构体数据的所有权

结构体当中它所拥有的数据应当是拥有所有权的,而不是其他地方借用的。

  • 借用数据需要考虑生命周期:生命周期确保结构体的作用范围比它所借用的数据的作用范围要小。
[derive(Debug)]

Rust 默认没有给结构体实现 Display 特征,而把输出格式的选择权利交给程序员。

  • 顾名思义,Display 特征能够使得结构体实现自动格式输出。

使用 #[derive(Debug)] 对结构体进行了标记,这样才能使用 println!("{:?}", s); 的方式对其进行打印输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

println!("{:?}", rect1);
// Rectangle { width: 30, height: 50 }
}
  • 使用 println!("{:?}", s); 输出时,需要结构体实现 Debug 特征。
  • #[derive(Debug)]derive 派生实现了 Debug 特征。

当结构体较大时,此时可以使用 {:#?} 来替代 {:?} 会有更美观的格式。

还有一个简单的输出 debug 信息的方法:使用 dbg! 宏。

  • 该宏会拿走表达式的所有权,然后打印出相应的文件名、行号等 debug 信息,还有表达式的求值结果。
  • 它最终还会把表达式值的所有权返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale), // 30 * scale = 60
height: 50,
};

dbg!(&rect1);
// [src\main.rs:14:5] &rect1 = Rectangle {
// width: 60,
// height: 50,
// }
}

枚举

通过列举可能的成员来定义一个枚举类型,如熟悉的枚举一周:

1
2
3
4
5
6
7
8
9
enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}

枚举类型是一个类型,其会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[derive(Debug)]
enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
fn main() {
let (oh_no, no_no, ok, come_on, happy) = (
Weekday::Monday,
Weekday::Tuesday,
Weekday::Wednesday,
Weekday::Thursday,
Weekday::Friday,
);
let (happier, sad) = (Weekday::Saturday, Weekday::Sunday);
println!("{:?} {:?} {:?} {:?} {:?}", oh_no, no_no, ok, come_on, happy);
println!("{:?} {:?}", happier, sad);
}

在 Rust 中,任何数据类型都可以放入到枚举中,这给枚举增加了更多功能。

1
2
3
4
5
6
7
8
9
10
11
12
enum Operation {
Quit, // 不包含变量
ChangeColor(u8, u8, u8), // 包含三个 u8 变量
Write(String), // 包含一个 String 变量
Move { x: i32, y: i32 }, // 包含一个匿名结构体变量
}

fn main() {
let o1 = Operation::Quit;
let o2 = Operation::ChangeColor(0, 0, 0);
let o3 = Operation::Write("hello".to_string());
}

数组

数组:array,长度固定,是基本类型。

动态数组:Vector,长度可动态增长。

  • 数组的长度是编译时确定的,而动态数组的长度是运行时确定的。
  • 数组 array 是存储在栈上,而动态数组 Vector 是存储在堆上。
  • VectorString 一样都是高级类型,即集合类型。
创建数组
1
2
3
4
5
fn main() {
let a = [1, 2, 3, 4, 5];
let b: [i32; 5] = [1, 2, 3, 4, 5];
let c = [3; 5]; // [3, 3, 3, 3, 3]
}
  • 数组类型通过方括号声明;
  • 可以显式指定数组元素类型和长度。
  • 数组的长度也是类型的一部分。
访问数组元素

数组是连续的,可以通过索引访问。

  • 下标从 0 开始。

另外,如果使用索引访问元素时,编译器会在编译期预防越界情况。但如果索引是外部输入的,那么编译器并不能预防。

如果数组元素非基础类型,且出现:

1
2
3
4
5
fn main() {
let array = [String::from("rust is good!"); 8];

println!("{:#?}", array);
}
  • 那必然是报错的。String 是具有所有权的高级类型,其并不能在数组中使用这种形式进行复制(没有深拷贝)。

而解决方法应该是调用 std::array::from_fn

1
2
3
4
fn main() {
let array: [String; 8] = std::array::from_fn(|_i| String::from("rust"));
println!("{:#?}", array);
}
数组切片

数组也允许引用集合中的部分连续片段:

1
2
3
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
println!("{:#?}", slice);
  • 省略主函数部分。

切片的特点:

  • 切片的长度可以与数组不同,并不是固定的,取决于指定的起始和结束位置。
  • 创建切片的代价非常小,因为切片只是针对底层数组的一个引用。
  • 切片类型 [T] 拥有不固定的大小,而切片引用类型 &[T] 则具有固定的大小,&[T] 更有用,&str 字符串切片也同理。

流程控制

if-else if

if else 表达式根据条件执行不同的代码分支:

1
2
3
4
5
if condition == true {
// A...
} else {
// B...
}
  • condition 的值为 true,则执行 A 代码,否则执行 B 代码。

if 语句(块)是表达式,可以返回值,但是需要保证每个分支的返回类型一样:

1
2
3
4
5
6
7
let a = 1;
let b = if a == 1 {
5
} else {
6 // 正确
//'6' // 错误,类型不一致
};

与其它语言一样,通过 if-else if 可以处理多重条件判断。

循环控制

Rust 有三种循环控制方式:

  • for 循环;
  • while 循环;
  • loop 循环。
for 循环

简单举个例子:

1
2
3
4
5
fn main() {
for i in 1..=5 {
print!("{} ", i);
}
}
  • 1..=5 是一个范围,表示 从 1 到 5 的 序列。

除了数字的循环外,for-in 用法更靓眼:

1
2
3
for 元素 in 集合 {
// do something...
}
for-in的用法
使用方法 等价使用方式 所有权
for item in collection for item in IntoIterator::into_iter(collection) 转移所有权
for item in &collection for item in collection.iter() 不可变借用
for item in &mut collection for item in collection.iter_mut() 可变借用

for-in 中也可以获取元素的索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let a = [4, 3, 2, 1];

// 一般的循环,数组实现了深拷贝所以所有权还在
for i in a {
print!("{} ", i);
// 4 3 2 1
}

// 可获取索引的循环
// `.iter()` 方法把 `a` 数组变成一个迭代器
for (i, v) in a.iter().enumerate() {
print!("({}:{}) ", i + 1, v);
// (1:4) (2:3) (3:2) (4:1)
}
}

如果想单纯的循环十次,(用于)循环变量不使用,可以这样写:

1
2
3
4
5
6
fn main() {
for _ in 1..=10 {
println!("Hello, world!");
// 打印10次
}
}
  • _ 的含义是忽略该值或类型的意思。

比较两种循环:

1
2
3
4
5
6
7
8
9
10
11
// 第一种
let collection = [1, 2, 3, 4, 5];
for i in 0..collection.len() {
let item = collection[i];
// ...
}

// 第二种
for item in collection {

}
  • 性能比较:
    • 第一种方式使用索引,会触发边界检查,导致性能损耗。
    • 第二种方式在编译时就完成分析并证明访问时合法的,性能不会有损失。
  • 安全性比较:
    • 第一张对 collection 的索引访问是非连续的,存在一定可能性在两次访问之间 collection 发生了变化,导致脏数据产生。
    • 第二种直接迭代是连续访问。(由于所有权限制,访问过程中,数据不会发生变化)
continue 和 break
  • 使用 continue 可以跳过当次循环,开始下次循环。(在其它语言也这样吧)
  • 使用 break 可以直接跳出当前整个循环。
while 循环

跟 C++ 类似。

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut n = 0;

while n <= 5 {
print!("{} ", n); // 0 1 2 3 4 5

n = n + 1;
}
println!("{}", n); // 6
}
loop 循环

loop 就是一个简单的 无限循环,不会自动结束。

  • 需要额外的 break 关键字控制循环结束。
  • loop 是一个表达式,可以返回值。
  • loop 循环中,break 结束时可以带出一个返回值。
1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let mut n = 0;

let ret = loop {
n += 1;
if n == 5 {
break n;
}
};
println!("{}", ret);
// 5
}

模式匹配

match 的魅力。

match 匹配

match 的通用形式:

1
2
3
4
5
6
7
8
9
match target {
模式1 => 表达式1,
模式2 => {
语句1;
语句2;
表达式2
},
_ => 表达式3
}
  • match 允许将一个值与一系列的模式相比较,并根据相匹配的模式执行对应的代码。
  • match 的分支有两个部分:一个模式和针对该模式的处理代码
  • switch 很像,_ 类似于 default
    • 除了 _,还可以随便用一个变量名承接即可。
  • match 语句会从上往下匹配,遇到第一个匹配的就执行对应的表达式,然后结束。
  • 可能会出现 |,类似于逻辑或,比如 X|Y 可以匹配 X 也可以匹配 Y

举一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum Direction {
Up,
Down,
Left,
Right,
}

fn main() {
let direction = Direction::Up;
match direction {
Direction::Up => println!("Up"),
Direction::Down => println!("Down"),
Direction::Left => println!("Left"),
Direction::Right => println!("Right"),
}
}
match 表达式赋值

match 本身也是一个表达式,因此可以用来赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Direction {
Up,
Down,
}

fn main() {
let direction = Direction::Up;
let direction_num = match direction {
Direction::Up => 0,
Direction::Down => 1,
};

println!("{}", direction_num);
}
模式绑定

模式匹配还可以从模式中取出绑定的值,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum Action {
Say(String),
MoveTo(i32, i32),
}

fn main() {
let actions = [Action::Say("Hello Rust".to_string()), Action::MoveTo(1, 2)];

for action in actions {
match action {
Action::Say(s) => {
println!("{}", s);
}
Action::MoveTo(x, y) => {
println!("Point ({}, {})", x, y);
}
}
}
}
  • enum 中可以放入数据类型,再加上模式匹配可以从模式中取出绑定的值,所以可以实现上述代码。
穷尽匹配

match 的匹配必须穷尽所有情况,比如下述代码因为没有穷尽所有情况而报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Direction {
Up,
Down,
Left,
Right,
}

fn main() {
let direction = Direction::Up;
match direction {
Direction::Up => println!("UpUp"),
// 缺少 Down 的匹配
Direction::Left | Direction::Right => {
println!("ohh");
}
};
}

if let 匹配

当只要匹配一个条件,且忽略其他条件时就用 if let

如下面两个代码是等价的:

实现1:

1
2
3
4
5
6
7
8
fn main() {
let v = Some(3u8);

match v {
Some(3) => println!("three"),
_ => (),
}
}

实现2:

1
2
3
4
5
6
7
fn main() {
let v = Some(3u8);

if let Some(3) = v {
println!("three");
}
}

变量遮蔽

无论是 match 还是 if let,这里都是一个新的代码块,而且这里的绑定相当于新变量,如果你使用同名变量,会发生变量遮蔽:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Action {
Say(String),
}

fn main() {
let action = Action::Say("Hello Rust".to_string());
let s = "abc".to_string();
match action {
Action::Say(s) => {
println!("{}", s); // Hello Rust
}
}
println!("{}", s); // abc
}

matches! 宏

matches! 宏可以将一个表达式跟模式进行匹配,然后返回匹配的结果 true 或者 false

如使用:

1
2
3
4
5
6
7
fn main() {
let ch = 'a';
let number = 5;

println!("{}", matches!(ch, 'a'..='z')); // true
println!("{}", matches!(number, 10..=20)); // false
}

解构 Option

Option 是一种枚举,用于解决 Rust 中变量是否有值的问题:

1
2
3
4
enum Option<T> {
None,
Some(T),
}
  • 一个变量要么有值:Some(T),要么为空:None。
  • 由于封装,可以直接使用 Some(T)None,而不需要使用 Option::Some(T)Option::None

使用 Option<T>,是为了从 Some 中取出其内部的 T 值以及处理没有值的情况。

编写一个函数,它获取一个 Option<i32>,如果其中含有一个值,将其加一;如果其中没有值,则函数返回 None 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn add_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(x) => Some(x + 1),
}
}

fn main() {
let five = Some(5);
let none: Option<i32> = None;

let (ans1, ans2) = (add_one(five), add_one(none));
println!("{:?}, {:?}", ans1, ans2); // Some(6), None
}

模式使用场景

用到模式的地方:

  • match 分支。
  • if let 语句。
  • while let 循环。
1
2
3
4
5
6
7
8
fn main() {
let mut v = ['a', 'b', 'c'].to_vec();

while let Some(top) = v.pop() {
print!("{} ", top);
}
// c b a
}
  • for 循环:使用特定模式匹配可迭代容器。
  • let 语句:使用变量绑定数据也是一种模式匹配。
  • 函数参数也是模式。

还有关于 letif let

1
let Some(x) = some_option_value;
  • 因为右边的值可能不为 Some,而是 None,这种时候就不能进行匹配。
  • 对于 letformatch 都要求完全覆盖匹配。

if let

1
2
3
if let Some(x) = some_option_value {
println!("{}", x);
}
  • if let 允许匹配一种模式,忽略其余模式。

全模式列表

模式的相关语法

匹配字面值
1
2
3
4
5
6
7
8
fn main() {
let x = 1;

match x {
1 => println!("one"),
_ => println!("anything"),
}
}
  • 代码获得特定的具体值。
匹配命名变量
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let x = Some(5);
let y = 10;

match x {
Some(y) => println!("match y = {:?}", y),
_ => println!("match: x = {:?}", x),
}

println!("main: x = {:?}, y = {:?}", x, y);
}
  • 变量遮蔽。
单分支多模式
1
2
3
4
5
6
7
8
9
fn main() {
let x = 1;

match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}
  • 使用 | 表示或。
通过序列 …= 匹配值范围
1
2
3
4
5
6
7
8
fn main() {
let x = 5;

match x {
1..=5 => println!("1~5"),
_ => println!("something else"),
}
}
  • 序列只允许用于数字或字符类型,原因是它们可以连续。
解构并分解值

使用模式来解构结构体、枚举、元组、数组和引用。

解构结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Point {
x: i32,
y: i32,
}

fn main() {
let p = Point { x: 0, y: 7 };

let Point { x, y } = p;
let Point { x: a, y: b } = p;
println!("({}, {})", x, y); // (0, 7)
println!("({}, {})", a, b); // (0, 7)
}
  • 模式中的变量名不必与结构体中的字段名一致。

还可以匹配结构体中的某个字段:

1
2
3
4
5
6
7
8
9
10
fn main() {
let p = Point { x: 0, y: 7 };

match p {
Point { x, y: 0 } => println!("在x轴上{}", x),
Point { x: 0, y } => println!("在y轴上{}", y),
Point { x, y } => println!("不在轴上({}, {})", x, y),
}
// 在y轴上7
}
解构枚举
1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Op {
Quit,
Move1 { x: i32, y: i32 }, // 绑定结构体
Move2(i32, i32), // 绑定元组
}

fn main() {
let op = Op::Move2(1, 2);
match op {
Op::Quit => println!("Quit"),
Op::Move1 { x, y } => println!("Move1: ({}, {})", x, y),
Op::Move2(x, y) => println!("Move2: ({}, {})", x, y),
};
}
  • 模式匹配需要类型相同。
解构嵌套的结构体和枚举
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}

enum Op {
Quit,
ChangeColor(Color),
}

fn main() {
let msg = Op::ChangeColor(Color::Hsv(0, 160, 255));

match msg {
Op::ChangeColor(Color::Rgb(r, g, b)) => {
println!("R:{}, G:{}, B:{}", r, g, b)
}
Op::ChangeColor(Color::Hsv(h, s, v)) => {
println!("H:{}, S:{}, V:{}", h, s, v)
}
_ => (),
}
}
  • match 可以匹配嵌套的项。
解构结构体和元组
1
2
3
4
5
6
7
8
9
fn main() {
struct Point {
x: i32,
y: i32,
}

let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
println!("feet: {}, inches: {}, x: {}, y: {}", feet, inches, x, y);
}
  • 用复杂的方式来混合、匹配和嵌套解构模式。
  • 上述代码为结构体和元组嵌套在元组中,把原始类型解构出来。
解构数组
  • 定长数组解构:
1
2
3
4
5
fn main() {
let arr = [1, 2, 3];
let [x, y, z] = arr;
println!("{} {} {}", x, y, z);
}
  • 不定长数组解构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let _arr = [1, 2, 3];
let arr = &_arr[..];

if let [x, ..] = arr {
println!("{}", x); // 1
}

if let &[.., y] = arr {
println!("{}", y); // 3
}

let arr: &[i32] = &[];

assert!(matches!(arr, [..])); // 断言成功
assert!(!matches!(arr, [x, ..])); // 断言成功
}
忽略模式中的值
  • 使用 _ 忽略整个值:当不再需要特定函数参数时,最好修改签名不再包含无用的参数。
1
2
3
4
5
6
7
fn fun(_: i32, y: i32) -> () {
println!("只使用y:{}", y);
}

fn main() {
fun(1, 2);
}
  • 使用嵌套的 _ 忽略部分值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
let mut setting_value = Some(5);
let new_setting_value = Some(10);

match (setting_value, new_setting_value) {
(Some(_), Some(_)) => {} // 不关心值,只关心类型
_ => {
setting_value = new_setting_value;
}
}

println!("setting is {:?}", setting_value);

let numbers = (2, 3, 6, 7, 8);
match numbers {
(first, _, third, _, fifth) => {
println!("{}, {}, {}", first, third, fifth);
// 2, 6, 8
}
}
}
  • 使用下划线开头忽略未使用的变量。

    • _ 的变量仍会将值绑定到变量,而 _ 则完全不会绑定。
  • .. 忽略剩余值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let numbers = (2, 3, 6, 7, 8);
match numbers {
(first, .., end) => {
println!("{}, {}", first, end);
// 2, 8
}
}
match numbers {
(first, ..) => {
println!("{}", first); // 2
}
}
}
匹配守卫提供的额外条件

匹配守卫(match guard)是一个位于 match 分支模式之后的额外 if 条件,它能为分支模式提供更进一步的匹配条件。

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let x = Some(6);
let y = false;

match x {
Some(n) if n > 5 => println!("> 5"),
Some(n) if y => println!("{}", n),
Some(_) | None if y => println!("aa"),
_ => println!("bb"),
}
}
  • 匹配守卫直接取得解构后的值作比较(如匹配分支1);
  • 匹配守卫可以直接用外部的 y(如匹配分支2);
  • 使用 | 加上匹配守卫,需要先满足前面 或 的条件再判断匹配守卫的条件(如匹配分支3),即 (Some(_) | None) if y
@ 绑定

@ 运算符允许为一个字段绑定另一个变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Op {
Operation { id: i32 },
}

fn main() {
let op = Op::Operation { id: 5 };
match op {
Op::Operation { id: mid @ 3..=7 } => {
println!("{} >= 3 and {} <= 7", mid, mid);
}
Op::Operation { id: 0..=2 } => {
println!(">= 0 and <= 2");
}
Op::Operation { id } => {
println!("{}", id);
}
};
}
  • 第一个匹配分支中,测试 Op::Operationid 字段是否位于 3..=7 范围内,同时也希望能将其值绑定到 mid 变量中以便此分支中相关的代码可以使用它。
    • 其实也可以把 mid 命名为 id,不影响、
  • 第二个匹配分支中,没有使用 @ 绑定,所以不能再使用结构体中的 id

在 Rust 1.56 时新增,使用 @ 还可以在绑定新变量的同时对目标进行解构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Point {
x: i32,
y: i32,
}

fn main() {
// 绑定新变量 `p`,同时对 `Point` 进行解构
let p @ Point { x: px, y: py } = Point { x: 10, y: 23 };
println!("x: {}, y: {}", px, py); // x: 10, y: 23
println!("{:?}", p); // Point { x: 10, y: 23 }

let point = Point { x: 10, y: 5 };
if let p @ Point { x: 10, y } = point {
println!("({},{})", p.x, p.y); // (10,5)
} else {
println!(":(");
}
}

在 Rust 1.53 新增特性:

  • 在 Rust 1.53 之前,需要这么写:
1
2
3
4
5
6
7
fn main() {
let number = 5;
match number {
num @ 1 | num @ 2 | num @ 3 => println!("{}", num),
_ => println!("{}", number),
}
}
  • 但是在 Rust 1.53 之后,可以这么写:
1
2
3
4
5
6
7
fn main() {
let number = 5;
match number {
num @ (1 | 2 | 3) => println!("{}", num),
_ => println!("{}", number),
}
}

方法

在面向对象编程中,方法指的是对象可执行的函数。

1
object.method();

定义方法

使用 impl 来定义方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Rect {
width: i32,
height: i32,
}

impl Rect {
fn new(w: i32, h: i32) -> Rect {
Rect {
width: w,
height: h,
}
}

fn area(&self) -> i32 {
self.width * self.height
}
}

fn main() {
let shape = Rect::new(4, 5);
println!("{}", shape.area());
}
  • impl Rect 表示为 Rect 实现方法,即 impl 语句块中一切都是跟 Rect 相关联的。
  • newRect 的关联函数,因为第一个参数不是 self,且 new 不是关键字。
  • area 中的参数 &self 表示借用当前的 Rect 结构体,

Rust 的对象定义和方法定义是分离的。

方法代替函数的好处有:

  • 不用再在函数签名中书写 self 对应的类型;
  • 代码的组织性、内聚性更强,对于代码维护和阅读有好处。
self

self 指代类型的实例(跟Python中挺像)。

  • 为哪个结构体实现方法,那么 self 就是指代哪个结构体的实例。

self 依然具有所有权的概念:

  • self 表示 Rect 的所有权转移到该方法中,这种形式用的较少。
  • &self 表示该方法对 Rect 的不可变借用。
  • &mut self 表示可变引用。
方法名

在 Rust 中,允许方法名跟结构体的字段名相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Rect {
width: i32,
height: i32,
}

impl Rect {
fn width(&self) -> i32 {
self.width
}

fn height(&self) -> i32 {
self.height
}

fn area(&self) -> i32 {
self.width * self.height
}
}
  • 此时,rect.width() 表示调用方法,rect.width 表示访问字段。

方法跟字段同名,适用于 getter 访问器的实现。

->运算符?

C/C++ 中,如果对象指针调用方法时,会使用到 ->object->fun()

但在 Rust 中,会有自动引用和解引用的功能。

  • 当使用 object.fun() 调用方法时,会自动为 object 添加 &&mut* 以便使得与方法签名匹配。
  • 因为方法中明确接收 self 的类型。

带有多个参数的方法

和普通函数一样:

1
2
3
4
5
impl Rect {
fn can_hold(&self, other: &Rect) -> bool {
self.width >= other.width && self.height >= other.height
}
}

关联函数

关联函数:定义在 impl 中且参数没有 self 的函数。

构造函数的写法:不包含 self 即可。

Rust 中有一个约定俗成的规则,使用 new 来作为构造器的名称,出于设计上的考虑,Rust 特地没有用 new 作为关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Rect {
width: i32,
height: i32,
}

impl Rect {
fn new(width: i32, height: i32) -> Rect {
Rect { width, height }
}
}

fn main() {
let rect = Rect::new(30, 50);
}

多个 impl 定义

Rust 允许为一个结构体定义多个 impl 块,目的是提供更多的灵活性和代码组织性。

  • 例如当方法多了后,可以把相关的方法组织在同一个 impl 块中。

为枚举实现方法

枚举可以像结构体一样,实现方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#![allow(dead_code)]
enum WeekDays {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
}

impl WeekDays {
fn get_day_name(&self) -> String {
match self {
WeekDays::Monday => "Monday".to_string(),
WeekDays::Tuesday => "Tuesday".to_string(),
WeekDays::Wednesday => "Wednesday".to_string(),
WeekDays::Thursday => "Thursday".to_string(),
WeekDays::Friday => "Friday".to_string(),
}
}
}

fn main() {
let day = WeekDays::Friday;
println!("今天是 {}", day.get_day_name());
}

泛型和特征

泛型 Generics

当出现需求:用同一功能的函数处理不同类型的数据,例如两个数的加法,无论是整数还是浮点数,甚至是自定义类型,都能进行支持。

  • C++ 中的模板函数就是一种解决方法。

泛型怎么不是一种多态呢。

Rust 给出的解决方案是:

1
2
3
4
5
6
7
8
9
10
11
12
fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
a + b
}

fn main() {
println!(
"{}, {}, {}",
add(1i8, 2i8), // 3
add(1.2f32, 2.3f32), // 3.5
add(20, 30) // 50
);
}
  • T 就是泛型参数。
  • std::ops::Add<Output = T 为对 T 进行限制,因为不是所有的 T 类型都能进行相加。
结构体中使用泛型
1
2
3
4
5
6
7
8
9
struct Point<T> {
x: T,
y: T,
}

fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3.0, y: 4.0 };
}
  • 需要提前声明泛型参数 Point<T>
  • xy 字段时相同的类型。

当然可以不止一个泛型参数:

1
2
3
4
struct Point<T, U> {
x: T,
y: U,
}
枚举中使用泛型

很明显,Option 中过就有一个泛型参数 T

1
2
3
4
enum Option<T> {
Some(T),
None,
}

还有一个:

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}
  • 这个枚举主要用于函数返回值,Result 关注的主要是值的正确性。
方法中使用泛型
1
2
3
4
5
6
7
8
9
10
struct Rect<T> {
width: T,
height: T,
}

impl<T> Rect<T> {
fn width(&self) -> &T {
&self.width
}
}
  • 使用泛型参数前,需要提前声明,如 impl<T>
  • impl 处的 Rect<T> 不再是泛型声明,而是一个完整的结构体类型。
为具体的泛型类型实现方法

T 换成特定的具体类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Rect<T, U> {
width: T,
height: U,
}

impl Rect<i32, i32> {
fn width(&self) -> i32 {
self.width
}
}

fn main() {
let rect1 = Rect {
width: 10,
height: 20,
};
let rect2 = Rect {
width: 10,
height: 22.0,
};
println!("{}", rect1.width());
println!("{}", rect2.width()); // 报错,无该方法
}
const 泛型

Rust 1.51 版本引入。

const 泛型是针对值的泛型。

正好可以用于处理数组长度的问题。

  • 数组而言,长度也是类型的一部分。
1
2
3
4
5
6
7
8
9
10
fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(arr);

let arr: [i32; 2] = [1, 2];
display_array(arr);
}
  • 定义一个类型为 [T; N] 的数组,T 是一个基于类型的泛型参数;而 N 是一个基于值的泛型参数,用来代替数组的长度。
泛型的性能

在 Rust 中泛型是零成本的抽象,意味着在使用泛型时,完全不用担心性能上的问题。

  • 实际上是损失了编译速度和增大了最终生成文件的大小。

Rust 通过在编译时进行泛型代码的单态化来保证效率。

  • 单态化:将通用代码转换为特定代码的过程。
  • 编译器的工作与创建泛型函数的步骤相反。

对于程序员而言,使用泛型可以编写不重复的代码,而 Rust 将会为每一个实例编译其特定类型的代码。

特征 Trait

特征定义了一组可以被共享的行为:只要实现了特征,就能使用这组行为

定义特征

定义特征:把一些方法组合在一起。

  • 目的是定义一个实现某些目标所必需的行为的集合。

举个例子,在数据中有小说和日记等内容载体,希望对相应的内容进行总结。那么总结这个行为就是共享的,可以都用一个特征:

1
2
3
pub trait Summary {
fn summarize(&self) -> String;
}
  • 使用 trait 关键字声明一个特征,Summary 是特征名。
  • 大括号中定义了该特征的所有方法。
    • 特征不定义行为具体是怎么样的,因此使用函数签名。
    • 每一个实现这个特征的类型都需要具体实现该特征的相应方法,编译器也会确保任何实现 Summary 特征的类型都拥有与这个签名的定义完全一致的 summarize 方法。
为类型实现特征
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
pub trait Summary {
fn summarize(&self) -> String;
}
#[derive(Debug)]
pub struct Novel {
pub title: String,
pub author: String,
pub content: String,
}

#[derive(Debug)]
pub struct Diary {
pub date: String,
pub content: String,
}

impl Summary for Novel {
fn summarize(&self) -> String {
format!("{} by {}", self.title, self.author)
}
}

impl Summary for Diary {
fn summarize(&self) -> String {
format!("Diary for {}", self.date)
}
}

fn main() {
let novel = Novel {
title: String::from("Dracula"),
author: String::from("Bram Stoker"),
content: String::from(
"Bram Stoker's classic novel about a bloodthirsty master of the night.",
),
};
let diary = Diary {
date: String::from("2014-01-02"),
content: String::from("A record of my daily life."),
};
println!("Novel: {:#?}", novel);
println!("Diary: {:#?}", diary);
}
特征定义与实现的位置

孤儿规则

上述代码中,Summary 被定义为公开的 pub,所以只需要引入到包中,就可使用该特征。

关于特征实现与定义的位置:如果想要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的

这样确保其他人编写的代码不会破坏自己的代码。

默认实现

在特征中定义具有默认实现的方法,这样其它类型无需再实现该方法,或者也可以选择重载该方法:

1
2
3
4
5
pub trait Summary {
fn summarize(&self) -> String {
"Reading...".to_string()
}
}

还有,默认实现允许调用相同特征中的其他方法,哪怕这些方法没有默认实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pub trait Summary {
fn simple_summary(&self) -> String;

fn summarize(&self) -> String {
println!("{}", self.simple_summary());
"Reading...".to_string()
}
}
// ...
impl Summary for Novel {
fn simple_summary(&self) -> String {
format!("{}", self.content)
}
}
  • 那么通过上述代码,Novel 的实例可以通过调用 summarize 方法间接调用了 simple_summary 方法。
使用特征作为函数参数

先定义一个函数,使用特征作为函数参数:

1
2
3
pub fn notify(item: &impl Summary) {
println!("{}", item.summarize());
}
  • impl Summary 表示实现了 Summary 特征的 item 参数。
  • 可以使用任何实现了 Summary 特征的类型作为该函数的参数,同时在函数体内,还可以调用该特征的方法。
特征约束

通过特征约束一些变量类型。

1
2
3
pub fn notify<T: Summary>(item: &T) {
println!("{}", item.summarize());
}
  • T: Summary 被称为特征约束。

特征约束的表达很奇妙,比如;

1
pub fn notify(item1: &impl Summary, item2: &impl Summary) {}
  • 函数的两个参数可以是实现了 Summary 特征的不同的类型。
1
pub fn notify<T: SUmmary>(item1: &T, item2: &T) {}
  • 函数的两个参数都必须是实现了 Summary 特征的相同的类型。
多重约束

可以指定多个约束条件:

1
pub fn notify(item: &(impl Summary + Send)) {}

1
pub fn notify<T: Summary + Send>(item: &T) {}
  • T: Summary + Send 表示 T 必须同时实现 SummarySend 特征。
Where 约束

当特征约束变得很多时,使用 where 进行一些形式上的改进:

1
2
3
4
5
fn fun<T, U>(t: &T, u: &U) -> i32
where
T: Send + Clone,
U: Clone + Summary,
{...}
例-找最大值

方式一:使用特征约束,且使用引用方式。

1
2
3
4
5
6
7
8
9
10
fn largest_1<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];

for item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
  • PartialOrd 特征可以用于比较两个值。

方法二:使用特征约束,使得值具有 Copy 特征。

1
2
3
4
5
6
7
8
9
10
11
fn largest_2<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];

for &item in list.iter() {
if item > largest {
largest = item;
}
}

largest
}
特征约束有条件地实现方法或特征

特征约束,可以在指定类型 + 指定特征的条件下去实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
use std::fmt::Display;

struct Pair<T> {
x: T,
y: T,
}

impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}

impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
}
  • 只有同时实现了 DisplayPartialOrd 特征的类型 T,才可以调用 cmp_display 方法。
函数返回中的 impl Trait

可以通过 impl Trait 来说明一个函数返回了一个类型,该类型实现了某个特征:

1
2
3
4
5
6
7
fn ret_summary() -> impl Summary {
Novel {
title: String::from("a"),
author: String::from("b"),
content: String::from("c"),
}
}
  • Novel 实现了 Summary 特征,所以可以用它作为返回值。
  • ret_summary 返回一个实现了 Summary 特征的类型,但不知道具体什么类型。

可能在数据类型十分复杂,不知道怎么声明,就可以使用这种返回类型。如闭包和迭代器的类型就是很复杂。

但是这种返回值只能有一种具体的类型,不能模棱两可。

  • 即一个分支下返回实现了某特征的 A 类型,而另一个分支又返回实现了某特征的 B 类型。这种情况是拒绝的。
通过 derive 派生特征

形如 #[derive(Debug)] 的代码,是一种特征派生语法。

derive 派生出来的是 Rust 默认提供的特征。

更多见派生特征

调用方法需要引入特征

如果要使用一个特征的方法,那么需要将该特征引入当前的作用域中。

Rust 把最常用的标准库中的特征通过 std::prelude 模块提前引入到当前作用域中。

综合例子
  • 自定义类型实现加法操作。
  • 自定义类型实现打印输出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
use std::{fmt::Display, ops::Add};

//限制类型 T 必须实现了 Add 特征,否则无法进行 + 操作
struct Complex<T: Add<T, Output = T>> {
real: T,
imag: T,
}

// 为 Complex<T: Add<T, Output = T>> 实现 Add 特征
impl<T: Add<T, Output = T>> Add for Complex<T> {
type Output = Complex<T>;
// 定义了一个类型别名 Output

fn add(self, p: Complex<T>) -> Complex<T> {
Complex {
real: self.real + p.real,
imag: self.imag + p.imag,
}
}
}

// 为 Complex<T: Add<T, Output = T>> 实现 Display 特征
impl<T: Add<T, Output = T> + Display> Complex<T> {
fn print(&self) {
println!("{} + {}i", self.real, self.imag);
}
}

fn add<T: Add<T, Output = T>>(a: T, b: T) -> T {
a + b
}

fn main() {
let a = Complex { real: 1, imag: 2 };
let b = Complex { real: 3, imag: 4 };

let c = add(a, b);
c.print();
}

特征对象 Todo

深入特征 Todo

集合类型 Todo

动态数组 Vector

KV 存储 HashMap

认识生命周期 Todo

返回值和错误处理 Todo

包和模块 Todo

包 Crate

模块 Module

使用 use 引入模块及受限可见性

注释和稳定 Todo

格式化输出 Todo