Rust/Tauri学习笔记

Rust/Tauri学习笔记

小叶子

封面作者:NOEYEBROW

请先阅读JavaScript学习笔记

⭐Rust

Rust 程序设计语言的本质实际在于赋能 Empowerment: 无论你现在编写的是何种代码, Rust 能让你在更为广泛的编程领域走得更远, 写出自信

举例来说, 那些系统层面的工作涉及内存管理、数据表示和并发等底层细节。从传统角度来看, 这是一个神秘的编程领域, 只为浸润多年的极少数人所触及, 也只有他们能避开那些臭名昭著的陷阱。即使谨慎的实践者, 亦唯恐代码出现漏洞、崩溃或损坏

Rust 破除了这些障碍: 它消除了旧的陷阱, 并提供了伴你一路同行的友好、精良的工具。想要深入底层控制的程序员可以使用 Rust, 无需时刻担心出现崩溃或安全漏洞, 也无需因为工具链不靠谱而被迫去了解其中的细节。更妙的是, 语言设计本身会自然而然地引导你编写出可靠的代码, 并且运行速度和内存使用上都十分高效

Rust 不止可以用于系统编程, 还可以用于制作 WebAssembly命令行程序Web前后端 等; Rust 也是如今性能最好的编程语言之一, 远比 JavaScript 高效, 但又可以通过 WebAssemblyJavaScript 无缝交互

配置开发环境

对于 Windows 系统:

  1. 下载 Visual Studio, 安装 C++ 工作负载(至少包括 MSVCWindows 11 SDK
  2. xxx\VisualStudio\VC\Tools\MSVC\14.39.33519\bin\Hostx64\x64 添加到 PATH 环境变量
  3. 官网 下载 RUSTUP-INIT 安装包
  4. 打开安装包, 根据提示安装 Rust
  5. 在命令行输入 rustup --versioncargo --version, 检查是否安装成功
  • 推荐使用 VSCode(安装 rust-analyzer 插件) 或 RustRover 进行 Rust 项目开发
  • 要升级 Rust, 可以使用 rustup update
  • 要卸载 Rust, 可以使用 rustup self uninstall
  • 要查看本地文档, 可以使用 rustup doc

Cargo

CargoRust 的构建系统和包管理器, 类似于 npmpipyarn

命令作用
cargo new xxx./xxx 目录下创建一个新的 Rust 项目
cargo build编译项目(开发模式), 输出到 ./target/debug
cargo build --release编译项目(生产模式), 输出到 ./target/release
cargo run相当于 cargo build + ./target/debug/xxx
cargo check检查项目是否能编译通过
cargo update更新项目的依赖
cargo doc --open生成项目依赖的文档并在浏览器打开
cargo publish发布项目到 crates.io
  • --release 选项会启用所有优化, 这样编译出来的程序会更快, 但编译时间会更长
  • 如果在 Cargo.toml 中添加了依赖, 会在编译前自动下载新添加的依赖

Cargo.toml

Cargo.tomlCargo.lockRust 项目的配置文件, 类似于 package.jsonpackage-lock.json; 在 Rust 中, 包通常被称为 crate

1
2
3
4
5
6
7
[package]
name = "xxx"
version = "0.1.0"
edition = "2021"

[dependencies]
xxx = "0.1.0"

0.1.0 默认被认为是 ^0.1.0

Hello World

1
2
3
4
5
6
# 创建项目
cargo new hello
# 进入项目
cd hello
# 编译并运行
cargo run
1
2
3
fn main() {
println!("Hello, world!");
}
猜大小游戏
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
44
45
46
47
48
49
50
51
52
use std::io; // 导入标准库
use std::cmp::Ordering; // 导入 Ordering 枚举
use rand::Rng; // 导入 rand crate
// 记得先在 Cargo.toml 中添加 rand = "0.8.5"

fn main() { // 程序入口函数
println!("猜大小游戏!"); // 打印字符串

let secret_number = rand::thread_rng().gen_range(1..=100); // 生成 1-100 的随机数
// 虽然 rust 是强类型语言, 但是它也有类型推断

loop { // 无限循环, 相当于 JS 中的 while(true)

println!("请输入你的猜测: "); // 打印字符串

let mut guess = String::new(); // 创建一个可变的字符串变量
// :: 表示关联函数(静态方法)
// let mut 类似于 let, 而 let 类似于 const

io::stdin() // 从标准输入读取
// 如果开头没有 use std::io, 可以使用 std::io::stdin()
.read_line(&mut guess) // 读取一行并存入 guess
// & 表示引用, 相当于 JS 中引用类型的地址, 而不是值
// &mut 表示可变引用, 而 & 表示不可变引用
// read_line 返回一个 Result 类型, 包含 Ok 和 Err 两种枚举情况
.expect("读取失败"); // 读取失败时抛出异常
// expect 是 Result 类型的方法
// 如果是 Ok 则返回 Ok 的值, 如果是 Err 则抛出异常

let guess: u32 = match guess.trim().parse() { // 将字符串转换为数字
Ok(num) => num, // 如果转换成功, 则返回数字
Err(_) => continue, // 如果转换失败, 则继续循环
};
// : u32 表示 guess 的类型是 u32
// rust 允许使用同名变量来覆盖(隐藏)之前的变量

println!("你的猜测是: {}", guess); // 打印字符串
// {} 是占位符, 类似于 C 语言的 %s、JS 的 ${}(不过变量不放在 {} 中)

match guess.cmp(&secret_number) { // 匹配 guess 和 secret_number
// cmp 是字符串的方法, 返回 Ordering 枚举类型
// Ordering 枚举类型有三个值: Less、Greater、Equal
Ordering::Less => println!("太小了!"), // 如果 guess < secret_number
Ordering::Greater => println!("太大了!"), // 如果 guess > secret_number
Ordering::Equal => {
println!("猜对了!"); // 如果 guess = secret_number
break; // 结束循环
}
}

}
}

点击下载上面的程序(Windows), 仅 171KB, 梦回大一 C 语言课

  • Rust 类似于 C, 程序的入口是 main 函数
  • Rust 中的缩进风格是 4 个空格
  • Rust 代码必须添加 ; 结尾
  • ! 表示宏调用, println! 是一个宏, 不是函数
  • Rust 中的注释同 JavaScript

变量

关键字作用
let xxx = xxx;创建一个不可变变量
let mut xxx = xxx;创建一个可变变量
const xxx: xxx = xxx;创建一个常量, 必须指定类型
  • 可以重复声明(必须加 let 关键字, 否则视为赋值)同名变量, 但是会在作用域内覆盖之前的变量
  • 变量的命名规范是蛇形命名法, 如 snake_case
  • 常量的命名规范是全大写, 如 MAX_POINTS
  • 常量的字面量必须是一个常量表达式, 不能是函数调用的结果
1
2
3
4
5
6
7
8
9
10
let var1 = 5; // 创建一个不可变变量
let mut var2 = 5; // 创建一个可变变量
var1 = 6; // 报错, 不可变变量无法重新赋值
var2 = 6; // 正确, 可变变量可以重新赋值
// 但可以利用"遮蔽"来"改变"不可变变量
{
let var1 = 7;
println!("{}", var1); // 7
}
println!("{}", var1); // 5

隐藏机制的作用

隐藏机制可以减少变量名的冲突, 但是也会增加代码的复杂性

1
2
3
4
5
// 请求用户输入空格字符来说明希望在文本之间显示多少个空格
let spaces = " ";
println!("{}", spaces); // (3 个空格)
let spaces = spaces.len();
println!("{}", spaces); // 3

标量类型

Rust 是一种静态类型语言, 有两大类数据类型:

  • 标量类型 (类似于 JavaScript 的基本数据类型)
    整型浮点型布尔型字符型
  • 复合类型 (类似于 JavaScript 的引用类型)
    元组数组字符串切片引用结构体枚举联合体函数元组结构体Never 类型

Rust 也存在类型推断, 可以省略类型声明, 如果需要显式声明类型, 语法类似于 TypeScript

整型 / Integer

长度有符号无符号取值范围
8-biti8u80-255 / -128-127
16-biti16u160-65535 / -32768-32767
32-biti32u320-4.29e9 / -2.14e9-2.14e9
64-biti64u640-1.84e19 / -9.22e18-9.22e18
128-biti128u1280-3.40e38 / -1.70e38-1.70e38
archisizeusize计算机是 32 位则是 i32 / u32
计算机是 64 位则是 i64 / u64
  • 有无符号表示是否允许负数
  • 默认的整型类型是 i32
  • n-bit 的无符号整数的 10 进制范围是 02^n - 1
  • n-bit 的有符号整数的 10 进制范围是 -2^(n-1)2^(n-1) - 1
  • 字面量:
    98_22210 进制, _ 用于增加可读性)
    0xff16 进制)
    0o778 进制)
    0b1111_00002 进制)
  • 如果数字过大, 发生 整形溢出, 会在 debug 模式下报错, 在 release 模式下会进行二进制补码操作

浮点型 / Float

Rust 有两种浮点类型: f32f64, 默认的浮点类型是 f64

1
2
3
4
5
6
7
8
9
10
11
12
let x = 2.0; // f64
let y: f32 = 3.0; // f32
// 加法运算
let sum = x + y;
// 减法运算
let difference = x - y;
// 乘法运算
let product = x * y;
// 除法运算
let quotient = x / y;
// 取余运算
let remainder = x % y;

整数除法的结果会被截断为整数

布尔型 / Boolean

Rust 的布尔类型 bool 只有两个值: truefalse

1
2
let t = true;
let f: bool = false; // 显式指定类型

字符型 / Character

Rust 的字符类型 char4 字节的 Unicode 标量值, 用单引号表示 (而双引号表示字符串)

1
2
3
4
let c = 'z';
let z = 'ℤ'; // 支持奇怪的 Unicode 字符
let zh = '喵'; // 支持中文
let cat = '😻'; // 支持 Emoji

任何四字节的 Unicode 标量值都是一个有效的 char 类型值, 而不仅限于一般意义的 字符

复合类型

元组 / Tuple

Rust 中的元组是一个将多个类型的值组合在一起的复合类型, 长度固定

  • Rust 中的元组是栈分配的, 长度固定, 不能动态增长
  • Rust 中通过索引访问元组的值时是通过 . (与数组不同)
  • 不带任何值的元组 () 称为单元元组 unit, 相当于 null
1
2
3
4
5
6
7
// 声明一个元组
let tup: (i32, f64, u8) = (500, 6.4, 1);
// 解构赋值
let (x, y, z) = tup;
println!("The value of y is: {y}"); // 6.4
// 通过索引访问
let five_hundred = tup.0;

数组 / Array

Rust 中的数组是一个将相同类型的值组合在一起的复合类型, 长度固定

  • Rust 中的数组是栈分配的, 长度固定, 不能动态增长
  • Rust 中的数组是连续的, 不能有空缺的元素
  • 虽然数组的长度固定, 但可以修改数组中的元素值
1
2
3
4
5
6
// 声明一个数组
let a: [i32; 5] = [1, 2, 3, 4, 5];
// 填充相同的值
let a = [3; 5]; // [3, 3, 3, 3, 3]
// 通过索引访问
let first = a[0];

字符串 / String

  • 用字面量 "xxx" 可以声明一个字符串, 此时该字符串是不可变的, 直接存储在二进制文件中
  • String 类型可以声明一个可变的字符串, 存储在
  • 要使用 String 类型, 首先通过 String::from 向内存申请空间, 然后通过所有权机制来自动地释放空间 (在其他语言中是依赖垃圾回收机制, 或手动释放空间)
1
2
3
4
5
let s = "hello"; // 不可变字符串
let s = String::from("hello"); // 可变字符串
// 追加字符串
s.push_str(", world!");
println!("{}", s); // hello, world!

函数

Rust 中的函数使用 fn 关键字声明, 并使用蛇形命名法命名

  • main 函数是程序的入口函数
  • 函数声明的位置不限 (类似于 JavaScript 的函数提升)
  • 函数的参数和返回值都必须显式声明类型
  • 函数的返回值是最后一个表达式的值, 或者使用 return 关键字返回值
  • 语句: 一段执行一些操作但不返回值的代码, 如 let x = 5;, 无返回值的函数定义
  • 表达式: 计算并返回一个值的代码, 如 5 + 6, println!("xxx") (返回 ())
  • Rust 是一门基于表达式的语言, 几乎所有的东西都是表达式, 甚至是 if 语句和循环
1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
say_hello(); // 调用函数
println!("{}", plus(5, 6)); // 11
}

fn say_hello() { // 声明函数
println!("Hello, world!");
}

fn plus(x: i32, y: i32) -> i32 { // 声明函数
x + y // 返回值
}

函数, 包括后面的结构体, 在 main 函数的前, 后, 外, 内都可以声明; 但不可以嵌套声明

常见问题

1
2
3
4
5
6
7
8
9
10
11
12
fn plus_one(x: i32) -> i32 {
x + 1; // 报错
// expected `i32`, found `()`
}

fn plus_one(x: i32) -> i32 {
return x + 1; // 正确
}

fn plus_one(x: i32) -> i32 {
x + 1 // 正确
}

Rust 中的 ;语句的结束符, 如果在表达式后面加上 ; 会变成语句, 从而导致返回值类型不匹配; 例如在猜大小游戏中, 我们不断通过 . 获取上一个表达式的返回值, 最后用 ; 结束这一段代码 (此时不需要返回值)

控制流

if 表达式

Rust 中的 if 语句的条件必须是一个 bool 类型的值, 且不会进行隐式转换, 如 if number { ... } 会报错, 应改为 if number != 0 { ... }

1
2
3
4
5
6
7
8
let number = 3;
if number < 3 {
println!("小于 3");
} else if number > 3 {
println!("大于 3");
} else {
println!("等于 3");
}

用作三元运算符

1
2
3
let condition = true;
let number = if condition { 5 } else { 6 };
println!("{}", number); // 5

注意: if 语句的每个分支的返回值必须是相同的类型

loop 循环

Rust 中的 loop 语句是一个无限循环

  • 可以通过 break 关键字退出循环
  • 也可以通过 continue 关键字跳过本次循环
1
2
3
4
5
6
7
8
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("{}", result); // 20

此时 loop 中的 break 相当于 fn 中的 return

循环嵌套

嵌套的循环中, breakcontinue 默认只作用于最内层的循环, 如果想要作用于外层的循环, 可以使用循环标签 'label: loop { ... }

1
2
3
4
5
6
7
8
'outer: loop {
println!("Entered the outer loop");
'inner: loop {
println!("Entered the inner loop");
break 'outer;
}
println!("This point will never be reached");
}

while 循环

语法类似 if 语句, 可以使用 breakcontinue

1
2
3
4
5
6
let mut number = 3;
while number != 0 {
println!("{}", number);
number -= 1;
}
println!("LIFTOFF!!!");

for 循环

Rust 中的 for in 循环是一个迭代器的语法糖, 可以使用 breakcontinue

1
2
3
4
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}

Range & Rev

Rust 中的 ....=范围运算符, 用于生成一个迭代器

1
2
3
4
5
6
7
for number in (1..4).rev() {
println!("{}", number);
} // 3 2 1

for number in 1..=4 {
println!("{}", number);
} // 1 2 3 4

0 开始时可以省略 0, 如 ..4 等价于 0..4

所有权

Rust 的核心功能之一是所有权, 它使 Rust 不需要垃圾回收, 也不需要手动释放内存, 从而避免了内存泄漏和二次释放的问题

栈内存和堆内存

Stack 与 堆 Heap

在很多语言中,你并不需要经常考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的抉择

栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同

栈以放入值的顺序存储值并以相反顺序取出值。这也被称作后进先出 Last in, first out。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做进栈 Pushing onto the stack,而移出数据叫做出栈 Popping off the stack。栈中的所有数据都必须占用已知且固定的大小

在编译时大小未知或大小可能变化的数据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。内存分配器 Memory allocator 在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针 Pointer。这个过程称作在堆上分配内存 Allocating on the heap,有时简称为分配 Allocating(将数据推入栈中并不被认为是分配)。因为指向放入堆中数据的指针是已知的并且大小是固定的,你可以将该指针存储在栈上,不过当需要实际数据时,必须访问指针。想象一下去餐馆就座吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪

入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备

访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。继续类比,假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜,接着桌子 B 听一个菜,然后再桌子 A,然后再桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作

当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈

跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过明白了所有权的主要目的就是管理堆数据,能够帮助解释为什么所有权要以这种方式工作

  • Rust 中的每一个值都有一个被称为所有者 Owner 的变量
  • 值在任一时刻只能有一个所有者
  • 当所有者离开作用域 Out of scope 时, 值将被丢弃 Drop

作用域 Scope 是一个项 Item 在程序中的有效范围, 其中声明的变量在这个范围结束前都是有效的; 通常 {} 内的代码块就是一个作用域

移动

当一个复合类型数据的变量被赋值给另一个变量时, 原来的变量将失效, 这被称为移动 Move (而不是像 JavaScript 中, 两个变量都指向同一个地址)

复合类型数据的变量只会将指针, 长度, 容量这些元数据存储在栈上, 而数据存储在堆上; 移动是针对元数据而非数据本身 (数据本身没有变化)

1
2
3
4
let str1 = String::from("hello");
println!("{}", str1); // hello
let str2 = str1;
println!("{}", str1); // 报错

Rust 之所以会在变量离开作用域时自动调用 drop 方法, 释放内存, 是因为如果两个变量同时指向同一个地址, 会导致二次释放 (一个内存安全性 Bug)

克隆

移动操作既不是浅拷贝也不是深拷贝, 实际上 Rust 永远不会隐式地创建数据的深拷贝

但如果确实需要复制推内存上的数据, 可以使用 clone 方法

1
2
3
4
let str1 = String::from("hello");
println!("{}", str1); // hello
let str2 = str1.clone();
println!("{} {}", str1, str2); // hello hello

标量类型的变量是存储在栈上的, 所以赋值时其实就是复制 (因为栈内存的复制是快速的); 除此之外, 如果一个元组中的所有元素都是标量类型, 那将该元组赋值给另一个变量时, 也是复制

函数传参

Rust 中的函数, 如果传递复合类型的实参, 会发生移动操作, 除非使用引用 & (后面会讲到) 或克隆 clone 方法

1
2
3
4
5
6
7
8
fn take_ownership(s: String) {
println!("{}", s);
}

let str = String::from("hello");
println!("{}", str); // hello
take_ownership(str); // hello
println!("{}", str); // 报错

请再次注意, 移动是针对复合类型数据; 标量类型数据会隐式发生复制

引用

Rust 中复合类型的引用 Reference (指向某个数据的指针) 允许你借用一个值, 但不获得其所有权 (借用: 创建一个引用的行为)

&xxx 来创建一个指向 xxx 的引用, 该引用不是数据的所有者, 所以引用被丢弃时, 数据不会被丢弃

1
2
3
4
5
6
7
8
9
10
// 注意定义函数时的参数类型
fn borrow(s: &String) {
println!("{}", s);
}

let str = String::from("hello");
println!("{}", str); // hello
// 传入引用
borrow(&str); // hello
println!("{}", str); // hello

可变引用

正如变量默认是不可变的, 引用也是默认不可变的, 如果想要引用的数据是可变的, 需要使用可变引用 &mut

注意: 在特定作用域中, 只能有一个可变引用, 这是为了避免数据竞争 Data Race; 同时, 可变引用也不能和不可变引用同时存在 (可以有多个不可变引用)

1
2
3
4
5
6
fn change(s: &String) {
s.push_str(", world!");
}

let mut str = String::from("hello");
change(&str); // 报错
1
2
3
4
5
6
7
fn change(s: &mut String) {
s.push_str(", world!");
}

let mut str = String::from("hello");
change(&mut str);
println!("{}", str); // hello, world!
1
2
3
4
5
6
7
let mut str = String::from("hello");
let r1 = &str; // 没问题
let r2 = &str; // 没问题
// let r3 = &mut str; // 报错
// 把 r1 和 r2 移动并丢弃后, 就可以创建一个可变引用了
println!("{}, {}", r1, r2);
let r3 = &mut str; // 没问题

悬垂引用 / Dangling Reference

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针 Dangling Pointer,所谓悬垂指针是其指向的内存可能已经被分配给其它持有者

相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会比它的引用先离开作用域

1
2
3
4
5
6
7
8
9
fn dangle() -> &String {
// 在函数作用域内创建一个字符串
let s = String::from("hello");
// 返回字符串的引用
&s
// 由于 s 在函数作用域结束时被丢弃, 所以返回的引用是无效的
}

let reference_to_nothing = dangle(); // 报错

Slice

Rust 中的切片 Slice 允许你引用 (意味着没有所有权) 一个集合中的一部分数据, 而不用引用整个集合

1
2
3
4
5
6
7
8
let stri = String::from("hello, world");
let arr = [1, 2, 3, 4, 5];

let res = &stri[..5]; // hello
let res = &stri[7..]; // world
let res = &stri[..]; // hello, world
let res = &arr[1..3]; // [2, 3]

注意:字符串 Slice Range 的索引必须位于有效的 UTF-8 字符边界内,如果尝试从一个多字节字符的中间位置创建字符串 Slice,则程序将会因错误而退出; 出于介绍字符串 Slice 的目的,本部分假设只使用 ASCII 字符集

相关说明

调用字符串的 clear() 方法会清空字符串; clear() 方法会尝试创建一个字符串的可变引用, 所以如果之前已经创建了一个不可变引用, 则会报错

1
2
3
let mut s = String::from("hello");
let r = &s;
s.clear(); // 报错

之前提到过, 字符串字面量是存储在二进制文件中的, 所以用字符串字面量创建的变量实际上是指向二进制特定位置不可变引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取第一个单词
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}

let str_literal = "hello, world";
let str_cons = String::from("hello, world");

let word = first_word(&str_literal); // OK
let word = first_word(&str_literal[..6]); // OK
// 由于 str_literal 是字符串字面量, 所以可以直接传入
let word = first_word(str_literal); // OK
let word = first_word(&str_cons); // OK
let word = first_word(&str_cons[..]); // OK
let word = first_word(str_cons); // 报错!

参数类型为 &str&String 更通用, 因为 &String 可以隐式转换为 &str, 但反之不行

结构体

Rust 中的结构体 Struct 是一种自定义数据类型, 可以将不同类型的数据组合在一起 (类似于 JavaScript 的对象和类)

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
// 定义结构体
struct Person {
name: String,
age: u8,
active: bool,
}


fn main() {
// 创建结构体实例
let active = true;
let mut me = Person {
name: String::from("小叶子"),
age: 18,
active, // 简写, 等价于 active: active
};

// 访问结构体字段
println!("{} {}", me.name, me.age); // 小叶子 18
me.active = false;

say_hello(&me); // Hello, I'm 小叶子
}

// 作为类型
fn say_hello(p: &Person) {
println!("Hello, I'm {}", p.name);
}
结构体数据的所有权

在上面的 Person 结构体的定义中,我们使用了自身拥有所有权的 String 类型而不是 &str 字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的

也可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上生命周期 Lifetimes,这是一个后面会讨论的 Rust 功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的,比如这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这段代码无法通过编译!
struct Person {
name: &str,
age: u8,
active: bool,
}

fn main() {
let me = Person {
name: "小叶子",
age: 18,
active: true,
};
}
// 编译器会抱怨它需要生命周期标识符

之后会讲到如何修复这个问题以便在结构体中存储引用,不过现在,我们会使用像 String 这类拥有所有权的类型来替代 &str 这样的引用以修正这个错误

一如其他变量, 结构体的字段默认是不可变的; 而且结构体不能只有一部分字段是可变的, 要么全部可变, 要么全部不可变

结构体更新语法

Rust 提供了结构体更新语法 Struct Update Syntax, 可以使用现有实例的字段值来初始化一个新实例

1
2
3
4
5
6
7
8
9
10
11
let me = Person {
name: String::from("小叶子"),
age: 18,
active: true,
};

let older_me = Person {
age: 19,
..me // 使用 me 的其他字段值
// 必须放在最后
};

结构体更新语法等同于赋值, 也会发生移动操作, 被移动的复合类型数据的所有权会被转移

元组结构体

Rust 中的元组结构体 Tuple Struct 是一种特殊的结构体, 可以用元组的形式来定义结构体

1
2
3
4
5
6
7
8
9
// 定义元组结构体
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

// 创建元组结构体实例
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}

注意: 元组结构体和元组是不同的类型, 即使它们的字段类型和数量相同; 上面的例子中, ColorPoint 也是不同的类型 (即使它们的字段类型和数量相同)

类单元结构体

Rust 中的类单元结构体 Unit-Like Struct 是一种特殊的结构体, 用于创建一个没有字段的结构体

1
2
3
4
5
6
7
// 定义类单元结构体
struct Empty;

// 创建类单元结构体实例
fn main() {
let empty = Empty;
}

类单元结构体通常用于实现 trait 和作为泛型类型的占位符 (后面会讲到)

打印和 dbg!

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
// 第一步: 加入外部属性 #[derive(Debug)]
#[derive(Debug)]
struct Person {
name: String,
age: u8,
active: bool,
}

// 第二步: 使用 {:?} 或 {:#?} 打印
fn main() {
let me = Person {
name: String::from("小叶子"),
age: 18,
active: true,
};
println!("{:?}", me);
// Person { name: "小叶子", age: 18, active: true }
println!("{:#?}", me);
// Person {
// name: "小叶子",
// age: 18,
// active: true
// }
}

dbg! 宏

Rust 中的 dbg! 宏可以用于打印调试信息, 与 println! 不同的是:

  • dbg! 接收一个表达式的所有权 (除非显式传入引用; 而 println! 接收引用), 并返回该表达式的值和所有权
  • dbg! 的输出目标是 stderr (而 println! 的输出目标是 stdout, 后面会讲到)
  • dbg! 的输出格式是 "{:?}""{:#?}", 并且会定位到调用 dbg! 的代码行
1
2
3
4
5
6
let me = Person {
name: dbg!(String::from("小叶子")),
age: 18,
active: true,
};
dbg!(&me); // 显式传入引用, 不让 dbg! 获取所有权
1
2
3
4
5
6
[src/main.rs:2] String::from("小叶子") = "小叶子"
[src/main.rs:9] me = Person {
name: "小叶子",
age: 18,
active: true,
}

方法

Rust 中的方法 Method关联函数 Associated Function 的一种, 用于定义在结构体上的函数; 其第一个参数是 self / &self / &mut self (实际上是 self: self/&self/&mut self 的缩写)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Person {
name: String,
age: u8,
active: bool,
}

impl Person { // implementation 的缩写
// 定义方法
fn say_hello(&self, position: &str) {
println!("Hello, I'm {} at {}", self.name, position);
}
}

fn main() {
// 调用方法
let me = Person {
name: String::from("小叶子"),
age: 18,
active: true,
};
// 方法语法: 实例.方法(参数)
me.say_hello("home"); // Hello, I'm 小叶子 at home
}

方法和属性可以同名, 调用时不加括号就是属性, 加括号就是方法; 通常用于实现 gettersetter (后面会讲到)

关联函数

Rustimpl 块中定义的函数叫做关联函数 Associated Function, 而其中没有 self 参数的函数需要使用 :: 语法调用 (而不是 .), 例如 String::from

:: 的实质是将函数与模块创建的命名空间 Namespace 进行关联, 从而实现模块化 Modularity (后面会讲到)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
impl Person {
// 关联函数
fn new(name: &str, age: u8) -> Person {
Person {
name: String::from(name),
age,
active: true,
}
}
}

fn main() {
// 调用关联函数
let me = Person::new("小叶子", 18);
}

这种函数通常用于实现构造函数 Constructor, 并一般以 new 命名 (但 new 不是关键字); impl 块可以有多个, 用意后面会讲到

枚举

Rust 中的枚举 Enum 是一种自定义数据类型, 用于定义一个类型可以是多个值中的一个的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义枚举
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}

// 定义枚举方法
impl IpAddr {
fn show(&self) {
match self {
IpAddr::V4(a, b, c, d) => println!("{}.{}.{}.{}", a, b, c, d),
IpAddr::V6(s) => println!("{}", s),
}
}
}

// 创建枚举实例
fn main() {
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
// home 和 loopback 的类型是 IpAddr
home.show(); // 127.0.0.1
loopback.show(); // ::1
}

定义 enum 时, 可以不用指定 (xxx), 即这个枚举成员不带参数, 例如 Option 枚举的 None

Option

Rust 标准库中的 Option 枚举是一个通用的枚举, 用于表示一个值可能存在也可能不存在的情况

相比于其他语言中的 nullundefined, Option 是一个类型安全的解决方案, 通过 SomeNone 两个枚举成员来表示有值和无值

Option<T> 枚举是如此有用以至于它甚至被包含在了 prelude 之中,你不需要将其显式引入作用域; 另外,它的成员也是如此,可以不需要 Option:: 前缀来直接使用 SomeNone; 即便如此, Option<T> 也仍是常规的枚举,Some(T)None 仍是 Option<T> 的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
// Option 的定义
enum Option<T> {
Some(T),
None,
}

// 使用 Option 枚举
fn main() {
let some_number = Some(5); // 类型是 Option<i32>
let some_string = Some("a string"); // 类型是 Option<&str>
let absent_number: Option<i32> = None; // 类型是 Option<i32>
// 赋值为 None 时, 需要显式指定类型 (因为编译器无法推断)
}

<T> 是一个泛型参数, 用于表示 Option 枚举的值的类型, 后面会讲到

取出 Option 的值

Rust 的设计中, 除了 Option<T> 之外的所有类型都是肯定有值的, 也就是说, 所有可能为空的值都会被包装在 Option<T>

在使用时, Option<i32> 不能直接与 i32 进行运算, 需要先取出 Option 的值, 通常使用 match 或者 unwrap 方法

1
2
3
4
5
6
7
8
9
10
let some_number = Some(5);

// 使用 match
let i = match some_number {
Some(i) => i,
None => 0,
};

// 使用 unwrap
let i = some_number.unwrap();

unwrap 方法会直接取出 Option 的值, 如果是 None 则会导致程序崩溃

match 控制流

Rust 中的模式匹配 Pattern Matching 是一种强大的运算符, 可以根据模式 Pattern (可由字面值, 变量, 通配符等等内容构成) 匹配不同的值

match 的语法类似于其他语言中的 switch 语句, 但更加强大, 可以匹配更多的模式

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
enum Coin {
OneMao,
FiveMao,
OneYuan,
}
enum Dumpling {
Normal,
Luck(Coin),
}

fn eat_dumpling(d: Dumpling) {
match d {
Dumpling::Normal => println!("😋"),
Dumpling::Luck(coin) => {
println!("🎉");
match coin {
Coin::OneMao => println!("哇, 一毛钱"),
Coin::FiveMao => println!("哇, 五毛钱"),
Coin::OneYuan => println!("哇, 一块钱"),
}
}
}
}

fn main() {
let d = Dumpling::Luck(Coin::OneYuan);
eat_dumpling(d); // 🎉 哇, 一块钱
}

通配模式和 _ 占位符

match 语句必须覆盖所有可能的情况, 否则会报错; 但有时候我们并不关心某些情况, 这时可以使用通配模式 Wildcard Pattern占位符 _

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 以上面吃饺子的例子为例
fn eat_dumpling(d: Dumpling) {
match d {
Dumpling::Normal => println!("😋"),
other => do_something(other),
}
}

fn eat_dumpling(d: Dumpling) {
match d {
Dumpling::Normal => println!("😋"),
Dumpling::Luck(coin) => {
match coin {
Coin::OneYuan => println!("🎉"),
_ => println!("😭"),
}
}
}
}

注意: other_ 必须放在最后, 它们的区别在于: other 是一个变量, 可以在后面使用, 会导致所有权转移; 而 _ 是一个占位符, 不能在后面使用, 不会导致所有权转移

if let 语法糖

Rust 中的 if let 语法糖可以简化 match 语句, 用于匹配单个模式的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 不用 if let
fn eat_luck_dumpling(d: Dumpling::Luck) {
match d {
Dumpling::Luck(Coin::OneYuan) => println!("😊"),
_ => (),
}
}

// 使用 if let
fn eat_luck_dumpling(d: Dumpling::Luck) {
// 如果 d 是 Dumpling::Luck(Coin::OneYuan)
if let Dumpling::Luck(Coin::OneYuan) = d {
println!("🎉");
}
}

if let 后面也可以像一般的 if 语句一样加上 else 语句

模块化

Rust 有许多功能可以让你管理代码的组织,包括哪些内容可以被公开,哪些内容作为私有部分,以及程序每个作用域中的名字; 这些功能,有时被统称为模块系统 The Module System,包括:

  • PackagesCargo 的一个功能,它允许你构建、测试和分享 crate
  • Crates:一个模块的树形结构,它形成了库或二进制项目
  • 模块 Modulesuse:允许你控制作用域和路径的私有性
  • 路径 Path:一个命名例如结构体、函数或模块等项的方式

package & crate

Rust 中的 Package 是一个或多个二进制项 Binary Crate Library Crate 的集合, 它包含一个 Cargo.toml 文件, 用于描述如何构建这些 crate

没有 main 函数, 也不会被编译成可执行文件, 而是被其他程序引用; 二进制项main 函数, 会被编译成可执行文件, 我们上面的所有例子都是二进制项

Cargo 其实就是一个包含用于构建代码的二进制项和其所依赖的库的包; 一个包中可以包含多个二进制项, 但只能有一个库, 且至少含有一个 crate (可以是库也可以是二进制项)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建一个新的项目 (包)
cargo new my_project
# my_project
# ├── Cargo.toml
# └── src
# └── main.rs
cargo new my_project --lib
# my_project
# ├── Cargo.toml
# └── src
# └── lib.rs

# src/main.rs 是默认的二进制项
# src/lib.rs 是默认的库
# src/bin/xxx.rs 是额外的二进制项

总述

这里我们提供一个简单的参考,用来解释模块、路径、use 关键词和 pub 关键词如何在编译器中工作,以及大部分开发者如何组织他们的代码

  1. crate 根节点开始: 当编译一个 crate, 编译器首先在 crate 根文件(通常,对于一个库而言是 src/lib.rs,对于一个二进制项而言是 src/main.rs)中寻找需要被编译的代码
  2. 声明模块: 在 crate 根文件中,你可以声明一个新模块;比如,你用mod garden; 声明了一个叫做 garden 的模块; 编译器会在下列路径中寻找模块代码:
    1. 内联,在大括号中,当 mod garden 后方不是一个分号而是一个大括号
    2. 在文件 src/garden.rs
    3. 在文件 src/garden/mod.rs
  3. 声明子模块: 在除了 crate 根节点以外的其他文件中,你可以定义子模块; 比如,你可能在 src/garden.rs 中定义了 mod vegetables;; 编译器会在以父模块命名的目录中寻找子模块代码:
    1. 内联,在大括号中,当 mod vegetables 后方不是一个分号而是一个大括号
    2. 在文件 src/garden/vegetables.rs
    3. 在文件 src/garden/vegetables/mod.rs
  4. 模块中的代码路径: 一旦一个模块是你 crate 的一部分,你可以在隐私规则允许的前提下,从同一个 crate 内的任意地方,通过代码路径引用该模块的代码; 举例而言,一个 garden vegetables 模块下的 Asparagus 类型可以在 crate::garden::vegetables::Asparagus 被找到
    也可以使用相对路径, 如与 vegetables 同级的 fruits, 在 vegetables 中可以使用 super::fruits::Apple; 在 garden 中可以使用 self::fruits::Apple; 其中 self 可以省略
  5. 私有与公有: 一个模块里的代码默认对其父模块私有; 为了使一个模块公用,应当在声明时使用 pub mod 替代 mod; 为了使一个公用模块内部的成员公用,应当在声明前使用 pub (除 enum 外, struct 等成员的成员默认也是私有的, 需要显式声明为 pub)
  6. use 关键字: 在一个作用域内,use 关键字创建了一个成员的快捷方式,用来减少长路径的重复。在任何可以引用 crate::garden::vegetables::Asparagus 的作用域,你可以通过 use crate::garden::vegetables::Asparagus; 创建一个快捷方式,然后你就可以在作用域中只写 Asparagus 来使用该类型
    通常只创建到要引用函数的父模块的快捷方式,如 use crate::garden::vegetables;,然后在作用域中使用 vegetables::Asparagus, 这样可以明显地区分项目的父项, 避免命名冲突; 还可以用 use xxx as xxx 来重命名, 用 use xxx::* 来导入所有公有项 (不推荐)
1
2
3
4
5
6
7
8
// my_project
// ├── Cargo.toml
// └── src
// ├── main.rs
// └── garden
// ├── mod.rs
// ├── vegetables.rs
// └── fruits.rs
1
2
3
4
5
6
7
8
9
10
11
12
// src/garden/mod.rs
pub mod vegetables;
// src/garden/vegetables.rs
#[derive(Debug)]
pub struct Asparagus {}
// src/main.rs
use my_project::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I have {:?}", plant);
}

二进制和库的最佳实践

包可以同时包含一个 src/main.rs 二进制 crate 根和一个 src/lib.rscrate 根,并且这两个 crate 默认以包名来命名; 通常,这种包在二进制 crate 中只有足够的代码来启动一个可执行文件,可执行文件调用库 crate 的代码; 因为库 crate 可以共享,这使得其它项目从包提供的大部分功能中受益

模块树应该定义在 src/lib.rs 中; 这样通过以包名开头的路径,公有项就可以在二进制 crate 中使用; 二进制 crate 就完全变成了同其它外部 crate 一样的库 crate 的用户:它只能使用公有 API; 这有助于你设计一个好的 API;你不仅仅是作者,也是用户!

私有成员和公有成员示例

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
mod back_of_house {
// 早餐中, 面包是公有的, 季节水果是私有的
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
// 定义公有方法
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}

pub fn eat_at_restaurant() {
// 在夏天订购一个黑麦土司作为早餐
let mut meal = back_of_house::Breakfast::summer("Rye");
// 改变主意更换想要面包的类型
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);

// 如果取消下一行的注释代码不能编译;
// 不允许 查看或修改 早餐附带的季节水果
// meal.seasonal_fruit = String::from("blueberries");
}

注意: 由于公有结构体 Breakfast 具有私有字段 seasonal_fruit,因此不能直接 back_of_house::Breakfast { xxx } 创建实例,必须提供一个公有的构造函数 summer 来创建实例

重导出 / Re-exporting

使用 use 关键字,将某个名称导入当前作用域后,这个名称在此作用域中就可以使用了,但它对此作用域之外还是私有的; 如果想让其他人调用我们的代码时,也能够正常使用这个名称,就好像它本来就在当前作用域一样,那我们可以将 pubuse 合起来使用。这种技术被称为重导出 re-exporting:我们不仅将一个名称导入了当前作用域,还允许别人把它导入他们自己的作用域

使用外部包

Rust 的包管理工具 Cargo 允许你在你的项目中引入外部包, 通过 Cargo.toml 文件中的 [dependencies] 部分来指定; 在编译前, Cargo 会自动从crates.io 下载这些包及其依赖

另外, 标准库 std 其实也是一个外部包, 只不过我们不需要在 Cargo.toml 中引入, 但使用时仍需要 use std::xxx 来导入

1
2
[dependencies]
rand = "0.8.5"
1
2
3
4
5
6
use rand::Rng;

fn main() {
let secret_number = rand::thread_rng().gen_range(1..=10);
println!("The secret number is: {}", secret_number);
}

嵌套路径

如果需要从一个模块中引入多个项, 有时会写很多行 use 语句, 这时可以使用嵌套路径 Nested Path 来简化

1
2
3
4
5
6
// 不用嵌套路径
use std::cmp::Ordering;
use std::io;

// 使用嵌套路径
use std::{cmp::Ordering, io};
1
2
3
4
5
6
// 不用嵌套路径
use std::io;
use std::io::Write;

// 使用嵌套路径
use std::io::{self, Write};

常见集合

Rust 标准库提供了许多常见的集合 Collection 类型, 用于存储多个值, 并且长度可变 (存储在堆上)

Vector

Rust 中的向量 Vector 是一个动态数组 Dynamic Array, 可以存储多个相同类型的值, 并且长度可变

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
// 创建一个新的空向量
// 如果有初始值, Rust 可以根据初始值推断类型
let v: Vec<i32> = Vec::new();

// 从初始值创建
let mut v = vec![1, 2, 3];

// 添加元素
v.push(4);
// 删除元素
v.pop();

// 读取值
let third: &i32 = &v[2]; // 如果索引不存在, 会导致程序崩溃
let third: Option<&i32> = v.get(2); // 返回一个 Option, 不会导致程序崩溃
match third {
Some(i) => println!("The third element is {}", i),
None => println!("There is no third element."),
}

// 遍历元素
for i in &mut v {
*i += 50;
println!("{}", i);
}

借用机制

vector 的结尾增加新元素时,在没有足够空间将所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中; 这时,下面的代码中第一个元素的引用就指向了被释放的内存; 借用规则阻止程序陷入这种状况

1
2
3
4
5
// 以下代码不能通过编译
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);

使用枚举来存储多种类型

1
2
3
4
5
6
7
8
9
10
11
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}

let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];

String

Rust 的核心语言中只有一种字符串类型 str, 它是不可变的, 并且是固定长度的, 通常使用 &str 类型来引用字符串字面量

Rust 标准库中的字符串 String 类型是一个可变的, 可增长的, 堆分配的字符串类型, 它是 str所有权类型

&strString 都是 UTF-8 编码的, 因此可以包含任何 Unicode 字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 创建一个新的空字符串
let mut s = String::new();

// 从字符串字面量创建
let data = "initial contents";
let mut s = data.to_string();
// 相当于
let mut s = String::from("initial contents");

// 添加字符串
let new_s = "bar";
s.push_str(new_s);
// push_str() 不获取所有权, 可以继续使用 new_s

// 添加字符
s.push('l');

// 拼接字符串
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
// 使用 format! 宏
let s3 = format!("{}{}", s1, s2); // format! 宏不获取所有权
// 使用 + 运算符
let s3 = s1 + &s2; // s1 被移动, 不能继续使用

字符串索引和遍历

String 类型支持使用 [index] 语法来获取单个字符, 因为 String 类型是一个 Vec<u8> 的封装, 一个字符并不总是占用一个字节

如果需要遍历字符串中的字符, 需要显式指定如何遍历, 通常使用 chars 方法来获取字符, bytes 方法来获取字节

1
2
3
4
5
6
7
8
9
10
11
let s = String::from("你好");

// 遍历字符
for c in s.chars() {
println!("{}", c);
} // 你 好

// 遍历字节
for b in s.bytes() {
println!("{}", b);
} // 228 189 160 229 165 189

HashMap

Rust 中的哈希映射 HashMap 是一个键值对 Key-Value 的集合 (相当于 JavaScirpt 中的对象或 Map); HashMap 的所有键和所有值必须分别是相同类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 注意: HashMap 不会被自动引入作用域
use std::collections::HashMap;

// 创建一个新的空哈希映射
let mut scores = HashMap::new();

// 插入键值对
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

// 获取值
let team = String::from("Blue");
// get 方法返回一个 Option<&V>
// copied 使 Option<&V> 变为 Option<V>
// unwrap_or 使 Option<V> 变为 V (如果是 None, 则返回 0)
let score = scores.get(&team).copied().unwrap_or(0);

// 遍历键值对
for (key, value) in &scores {
println!("{}: {}", key, value);
}

HashMap 的键和值都是所有权类型, 如果将某个引用插入 HashMap, 那么引用指向的数据必须在 HashMap 有效时也是有效的

更新哈希映射

HashMapinsert 方法会覆盖已有的值, 如果想要在键没有值时才插入, 可以使用 entry 方法

entry 方法返回一个 Entry 枚举, 它代表了可能存在或不存在的值, 并提供了一个 or_insert 方法, 在键没有值时插入, 在键有值时返回这个值的可变引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::collections::HashMap;

let text = "hello world wonderful world";

let mut map = HashMap::new();

// split_whitespace() 方法返回一个迭代器
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}

println!("{:?}", map);
// {"world": 2, "hello": 1, "wonderful": 1}

错误处理

Rust 将错误分为两大类:可恢复的 Recoverable 和不可恢复的 Unrecoverable 错误; 对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作; 不可恢复的错误总是 bug 出现的征兆,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序

大多数语言并不区分这两种错误,并采用类似异常这样方式统一处理它们; Rust 没有异常; 相反,它有 Result<T, E> 类型,用于处理可恢复的错误,还有 panic! 宏,在程序遇到不可恢复的错误时停止执行

panic!

panic! 宏表示程序遇到了一个无法处理的错误,它会导致程序立即停止执行,并打印错误信息

panic! 会在程序错误时自动触发, 但也可以手动调用

1
2
3
fn main() {
panic!("crash and burn");
}
1
2
3
4
5
6
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

使用自定义类型进行有效性检查

如在开头的猜大小游戏中, 如果用户输入的数字不在 1-100 之间, 我们可以使用 panic! 来终止程序 (但更好的方式是使用 Result 类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pub struct Guess {
value: i32,
}

impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}

Guess { value }
}

pub fn value(&self) -> i32 {
self.value
}
}

Result

Result<T, E> 是一个通用的枚举, 有两个成员:OkErr, 分别表示操作成功和操作失败

1
2
3
4
5
// T, E 是泛型参数, 后面会讲到
enum Result<T, E> {
Ok(T), // T 是操作成功时的返回值的类型
Err(E), // E 是操作失败时的错误类型
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::fs::File;
use std::io::ErrorKind;

fn main() {
let file_result = File::open("hello.txt");
let file = match file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(file) => file,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}

unwrap & expect

Result 类型有一个 unwrap 方法, 如果 ResultOk 则返回 Ok 中的值, 如果是 Err 则调用 panic!

expect 方法与 unwrap 类似, 但可以指定 panic! 的错误信息

1
2
let file = File::open("hello.txt").unwrap();
let file = File::open("hello.txt").expect("Failed to open hello.txt");

传播错误

Result 类型有一个 ? 运算符, 如果 ResultOk 则返回 Ok 中的值, 如果是 Err 则将 Err 的值传播给函数的调用者

使用 ? 时, 可能的返回值必须是 Result (或 Option), 且函数的返回值也必须是 Result (或 Option), 且两者必须相同

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
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}

// 也可以使用链式调用
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}

// 相当于
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}

std::fs::read_to_string 函数可以简化读取文件的操作, 它会打开文件、新建一个 String、读取文件的内容,并将内容放入 String,接着返回它

泛型

Rust 中的泛型 Generics 是一种在编译时将类型参数化的功能, 使得我们可以不必为了不同的类型编写不同的代码, 而是可以编写一次代码, 并在需要时使用不同的类型

编译器会根据泛型参数的具体类型生成不同的代码 (单态化 Monomorphization), 使得泛型代码的性能和特化代码一样高效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::cmp::PartialOrd;
// 通常约定使用大写字母来表示泛型参数
// 在 <> 中声明泛型参数, 才能在函数签名和函数体中使用
// T: PartialOrd 表示 T 必须实现 PartialOrd trait, 即可以比较大小
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}

fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);

let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}

结构体定义中的泛型

Rust 中的结构体 Struct枚举 Enum 也可以使用泛型 (如 Option<T>Result<T, E>)

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

struct MixedPoint<T, U> {
x: T,
y: U,
}

fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
let mix = Point { x: 5, y: 4.0 }; // 编译错误!
let mix = MixedPoint { x: 5, y: 4.0 }; // 正确
}

方法定义中的泛型

Rust 中的方法 Method 也可以使用泛型, 但需要在 impl 块中声明泛型参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
impl<X> Point<X> {
fn x(&self) -> &X {
&self.x
}
}

// 表示只有 x 是 i32 类型的 Point 才有 x_squared 方法
impl Point<i32> {
fn x_squared(&self) -> i32 {
self.x * self.x
}
}

fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}

Trait

Rust 中的特性 Trait 类似于其他语言中的接口 Interface, 它定义了一些行为, 并允许类型实现这些行为; 可以使用 trait bounds 来指定泛型参数必须实现的特性

要让一个类型实现一个特性, 需要先定义 trait, 然后在 impl Trait for Type 块中实现这个 trait

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
// src/lib.rs
// 定义一个 trait
pub trait Summary {
// 任何实现了 Summary 的类型都必须实现 summarize 方法
fn summarize(&self) -> String;
}

// 实现一个类型
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}

// 实现另一个类型
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// src/main.rs
use my_project::{NewsArticle, Tweet};

fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from("The Pittsburgh Penguins once again are the best hockey team in the NHL."),
};
println!("New article available! {}", article.summarize());
}

默认实现

Rust 中的 trait 可以有默认实现 Default Implementation, 这样实现 trait 的类型就不需要实现这个方法, 但可以选择重写

1
2
3
4
5
6
7
8
9
pub trait Summary {
fn summarize(&self) -> String {
// 默认实现
format!("Read more from {}", self.summarize_author())
}

// 这样只需实现 summarize_author 方法即可使用 summarize 方法
fn summarize_author(&self) -> String;
}

trait bounds

Rust 中的 trait bounds 是一种约束泛型参数的方法, 用于指定泛型参数必须实现的 trait

指定函数参数
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
// T 必须实现 Summary trait
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// 也可以用简写语法 impl Trait
fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}

// 要求实现多个 trait
fn notify(item: &(impl Summary + Display)) {
println!("Breaking news! {}", item.summarize());
}
fn notify<T: Summary + Display>(item: &T) {
println!("Breaking news! {}", item.summarize());
}

// 通过 where 语法简化
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// ...
}
指定返回类型
1
2
3
4
5
6
7
8
9
10
// 返回实现了 Summary 的类型
// 只适用于返回单个类型的情况, 多个类型后面会讲到
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
有条件地实现方法
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
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 }
}
}

// 只有实现了 Display + PartialOrd 的类型才有 cmp_display 方法
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);
}
}
}

// 只为实现了 Display trait 的类型实现 ToString trait
impl<T: Display> ToString for T {
// ...
}

生命周期

Rust 中的生命周期 Lifetime 是一种约束引用的有效时间, 用于避免悬垂引用 Dangling Reference 和内存泄漏

类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明它们的关系,这样就能确保运行时实际使用的引用绝对是有效的

1
2
3
4
&i32        // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
// 生命周期注解不会改变引用的生命周期, 只是表明引用之间的关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 函数签名中的生命周期注解
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
// 此时返回的引用的生命周期与函数参数所引用的值的生命周期的较小者一致

fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
// 正常, 因为 string2 的生命周期比 result 的生命周期短
println!("The longest string is {}", result);
}
// 错误, string2 已经被销毁
println!("The longest string is {}", result);
}
详细解释

现在函数签名表明对于某些生命周期 'a,函数会获取两个参数,它们都是与生命周期 'a 存在的一样长的字符串 slice; 函数会返回一个同样也与生命周期 'a 存在的一样长的字符串 slice; 它的实际含义是 longest 函数返回的引用的生命周期与函数参数所引用的值的生命周期的较小者一致; 这些关系就是我们希望 Rust 分析代码时所使用的

记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝; 注意 longest 函数并不需要知道 xy 具体会存在多久,而只需要知道有某个可以被 'a 替代的作用域将会满足这个签名

当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中; 生命周期注解成为了函数约定的一部分,非常像签名中的类型; 让函数签名包含生命周期约定意味着 Rust 编译器的工作变得更简单了; 如果函数注解有误或者调用方法不对,编译器错误可以更准确地指出代码和限制的部分; 如果不这么做的话,Rust 编译会对我们期望的生命周期关系做更多的推断,这样编译器可能只能指出离出问题地方很多步之外的代码

当具体的引用被传递给 longest 时,被 'a 所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分; 换一种说法就是泛型生命周期 'a 的具体生命周期等同于 xy 的生命周期中较小的那一个; 因为我们用相同的生命周期参数 'a 标注了返回的引用值,所以返回的引用值就能保证在 xy 中较短的那个生命周期结束之前保持有效

详见原文

结构体定义中的生命周期注解

目前为止,我们定义的结构体全都包含拥有所有权的类型。也可以定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期注解

1
2
3
4
struct ImportantExcerpt<'a> {
// 表示 ImportantExcerpt 的实例不能比其引用的文本片段存在的更久
part: &'a str,
}

生命周期省略

Rust 有一套生命周期省略规则 Lifetime Elision Rules, 它允许我们在某些情况下省略生命周期注解 (早期版本的 Rust 没有这个功能, 需要为每个引用添加生命周期注解)

  • 输入生命周期 Input Lifetimes: 函数或方法参数的生命周期
  • 输出生命周期 Output Lifetimes: 返回值的生命周期
  • 规则一: 每个是引用的参数都有它自己的生命周期参数
  • 规则二: 如果只有一个输入生命周期参数, 那么它被赋予所有输出生命周期参数
  • 规则三: 如果方法有多个输入生命周期参数, 但其中一个是 &self&mut self, 那么 self 的生命周期被赋予所有输出生命周期参数
  • 编译器会依此检查这些规则, 如果代码符合规则, 则不需要显式指定生命周期

方法定义中的生命周期注解

1
2
3
4
5
6
7
8
9
10
11
12
impl<'a> ImportantExcerpt<'a> {
// 根据规则一, 无需显式指定生命周期
fn level(&self) -> i32 {
3
}

// 根据规则三, 无需显式指定生命周期
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}

静态生命周期

'static 是一个特殊的生命周期, 它代表整个程序的运行时间内都有效, 所有的字符串字面量都拥有 'static 生命周期

1
let s: &'static str = "I have a static lifetime.";

综合示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}

🚧测试

Rust 有一个内建的测试框架, 可以编写单元测试、集成测试和文档测试

🚧闭包

🚧迭代器

🚧智能指针

🚧并发

🚧面向对象

🚧模式匹配

🚧高级特性

⭐Rust Std Lib

⭐Tauri

Tauri 是一个用于构建现代 Web 应用程序的 Rust 框架, 它使用 Web 技术来构建用户界面, 并使用 Rust 来构建应用程序的后端

1
2
3
4
5
6
7
8
9
10
# 创建一个新的 Tauri 项目
pnpm create tauri-app
# 前端使用 Vite, 可以选择 React 等框架

# 启动开发服务器, 两种命令都可以
pnpm tauri dev
cargo tauri dev
# 构建应用程序, 两种命令都可以
pnpm tauri build
cargo tauri build
  • ./src 目录下是前端代码 (Vite & React)
  • ./src-tauri 目录下是后端代码 (Rust)
  • ./src-tauri/targe 目录下是构建后的应用程序

调用 Rust 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src-tauri/src/main.rs

// 添加宏来导出函数
#[tauri::command]
fn hello(user_name: &str) -> String {
format!("Hello, {}!", name)
}

fn main() {
tauri::Builder::default()
// 注册函数
.invoke_handler(tauri::generate_handler![hello])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
1
2
3
4
5
6
7
// src/App.js

import { invoke } from '@tauri-apps/api'

const res = await invoke('hello', { userNmae: 'world' })

console.log(res) // Hello, world!
  • 函数的参数名分别遵守 RustJavaScript 的命名规范, 即 snake_casecamelCase
  • Rust 函数的返回值可以是一个 Result 类型, 以便 JavaScript 可以处理错误

JavaScript API

Tauri 提供了一些 JavaScriptAPI, 就算不编写 Rust 代码也可以进行文件操作等

要使用 TauriJavaScript API, 需要在 tauri.conf.json 中显式允许某个 API

这些 API 都是由 Rust 实现的 (类似地, Node.js 中的各种模块是由 C++ 实现的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// tauri.conf.json
{
"tauri": {
"allowlist": {
"fs": {
"all": true // 允许所有 fs API
},
"dialog": {
"ask": true, // 允许 dialog.ask API
"confirm": true // 允许 dialog.confirm API
}
}
}
}

fs

  • 标题: Rust/Tauri学习笔记
  • 作者: 小叶子
  • 创建于 : 2024-03-09 12:15:50
  • 更新于 : 2024-05-20 15:13:42
  • 链接: https://blog.leafyee.xyz/2024/03/09/Rust学习笔记/
  • 版权声明: 版权所有 © 小叶子,禁止转载。
评论