Rust 学习笔记
Hello, World!
fn main() {
println!("Hello, World!");
}
编译与运行Rust程序
- 编译:
rustc main.rs
- 运行:
- Windows:
.\main.exe
- Linux/Mac:
./main
- Windows:
Rust程序解剖
- 定义函数:
fn main() {}
- 没有参数,没有返回
- main 函数很特别,它是每个Rust可执行程序最先运行的代码
- 打印文本:
println!("Hello, World!")
- Rust的缩进是4个空格,而不是tab
- println! 是一个Rust macro(宏)
- 如果是函数的话,就没有!
- "Hello, World!"是字符串,它是println!的参数
- 这行代码以
;
结尾
编译和运行是单独的两步
- 运行Rust程序之前必须编译,命令为
rustc 源文件名
rustc main.rs
- 编译成功后,会生成一个二进制文件
- 在Windows上还会生成一个.pdb文件,里面包含调试信息
- Rust是ahead-of-time编译的语言
- 可以先编译程序,然后把可执行文件交给别人运行(无需安装Rust)
- rustc 只适合编译简单的Rust程序
Hello Cargo
Cargo
- Caogo是Rust的构建系统和包管理工具
- 构建代码、下载依赖的库、构建这些库…
- 安装Rust的时候会安装Cargo
cargo --version
使用Cargo创建项目
- 创建项目:
cargo new hello_cargo
- 项目名称也是hello_cargo
- 会创建一个新的目录hello_cargo
- Cargo.toml
- src目录
- main.rs
- 初始化了一个新的GIt仓库,.gitignore
- 可以使用其他VCS或不使用VCS:cargo new 的时候使用
--vcs
这个flag
- 可以使用其他VCS或不使用VCS:cargo new 的时候使用
Cargo.toml
- TOML(Tom’s Obvious, Minimal Language)格式,是Cargo的配置格式
- [package],是一个区域标题,表示下方的内容是用来配置包(package)的
- name,项目名
- version,项目版本
- authors,项目作者
- edition,使用的Rust版本
- [dependencies],另一个区域的开始,它会列出项目的依赖项
- 在Rust里面,代码的包称做crate
src/main.rs
- cargo生成的main.rs在是src目录下
- 而Cargo.toml在项目顶层下
- 源代码都应该在src目录下
- 顶层目录可以放置:README、许可信息、配置文件和其他与程序源码无关的文件
- 如果创建项目时没有使用cargo,也可以把项目转换为使用cargo:
- 把源代码文件移动到src下
- 创建Cargo.toml并填写相应配置
构建项目 cargo build
cargo build
- 创建可执行文件:target/debug/hello_cargo 或 target\debug\hello_cargo.exe (Windows)
- 运行可执行文件:./target/debug/hello_cargo 或 .\target\debug\hello_cargo.exe (Windows)
- 第一次运行cargo build会在顶层目录生成cargo.lock文件
- 该文件负责追踪项目依赖的精确版本
- 不需要手动修改该文件
运行项目 cargo run
cargo run
,编译代码+执行结果- 如果之前编译成功过,并且源码没有改变,那么就会直接运行二进制文件
cargo check
cargo check
,检查代码,确保能通过编译,但是不产生任何可执行文件- cargo check 要比 cargo build 快得多
- 编写代码的时候可以连续反复使用cargo check检查代码,提高效率
为发布构建
cargo build --release
- 编译时会进行优化
- 代码会运行得更快,但是编译时间更长
- 会在target/release而不是target/debug生成可执行文件
- 编译时会进行优化
- 两种配置:
- 一个开发
- 一个正式发布
建议尽量使用Cargo
猜数游戏——一次猜测
游戏目标
- 生成一个1-100之间的随机数
- 提示玩家输入一个猜测
- 猜完之后,程序会提示猜测太小了还是太大了
- 如果猜测正确,那么打印一个庆祝信息,程序退出
代码实现
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("猜数!");
let secret_number = rand::thread_rng().gen_range(1, 101); // [1, 101)
// println!("神秘数字是:{}", secret_number);
loop {
println!("猜测一个数(1~100)");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("无法读取行");
println!("你猜测的数是:{}", guess);
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
match guess.cmp(&secret_number) {
Ordering::Less => println!("你猜的数小了"),
Ordering::Greater => println!("你猜的数太大了"),
Ordering::Equal => {
println!("恭喜你,猜中了!!!");
break;
}
}
}
}
通用的编程概念
变量与可变性
- 声明变量使用
let
关键字 - 默认情况下,变量是不可变的(Immutable)
- 声明变量时,在变量前面加上
mut
关键字,就可以使变量可变
变量与常量
- 常量(constant),常量在绑定值以后也是不可变的,但是它与不可变的变量有很多区别:
- 不可以使用
mut
,常量永远都是不可变的 - 声明常量使用
const
关键字,它的类型必须被标注 - 常量可以在任何作用域内进行声明,包括全局作用域
- 常量只可以绑定到常量表达式,无法绑定到函数的调用结果或只有在运行时才能计算出的值
- 不可以使用
- 在程序运行期间,常量在其声明的作用域内一直有效
- 命名规范:Rust里常量使用全大写字母,每个单词之间用下划线分开,例如:
MAX_POINT
- 例子:
const MAX_POINT: u32 = 100_000;
Shadowing(隐藏)
- 可以使用相同的名字声明新的变量,新的变量就会shadow(隐藏)之前声明的同名变量
- 在后续的代码中这个变量名代表的就是新的变量
- shadow和把变量标记为mut是不一样的:
- 如果不使用let关键字,那么重新给非mut的变量赋值会导致编译时错误
- 而使用let声明的同名新变量,也是不可变的
- 使用let声明的同名新变量,它的类型可以与之前不同
数据类型
- 标量和复合类型
- Rust是静态编译语言,在编译时必须知道所有变量的类型
- 基于使用的值,编译器通常能够推断出它的具体类型
- 但如果可能的类型比较多,就必须添加类型的标注,否则编译会报错
标量类型
- 一个标量类型代表一个单个的值
- Rust有4个主要的标量类型:
- 整数类型
- 浮点类型
- 布尔类型
- 字符类型
整数类型
- 整数类型没有小数部分
- 例如
u32
就是一个无符号的整数类型,占据32位的空间 - 无符号整数类型以 u 开头
- 有符号整数类型以 i 开头
- Rust的整数类型:
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
- 有符号的范围: 2 n − 1 到 2 n − 1 − 1 2^n - 1 到 2^{n -1} - 1 2n−1到2n−1−1
- 无符号范围: 0 到 2 n − 1 0 到 2^n - 1 0到2n−1
isize 和 usize 类型
- isize 和 usize 类型的位数由程序运行的计算机的架构所决定:
- 如果是64位计算机,那就是64位的
- …
- 使用 isize 和 usize 的主要场景是对某种集合进行索引操作
整数字面值
Number Literals | Exanple |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte(u8 only) | b’A’ |
- 除了 byte 类型外,所有的数值字面值都允许使用类型后缀
- 例如:57u8
- 如果你不太清楚应该使用哪种类型,可以使用Rust相应的默认类型
- 整数的默认类型就是
i32
- 总体上来说速度很快,即使在64位系统中
整数溢出
- 例如:u8 的范围是0 ~ 255,如果你把一个u8变量的值设为256,那么:
- 调试模式下编译:Rust会检查整数溢出,如果发生溢出,程序在运行时就会panic
- 在发布模式下(–release)编译:Rust不会检查可能导致panic的整数溢出
- 如果溢出发生:Rust会执行”环绕“操作:
- 256变成0,257变成1…
- 但程序不会panic
- 如果溢出发生:Rust会执行”环绕“操作:
浮点类型
- Rust有2种基础的浮点类型,也就是含有小数部分的类型
f32
,32位,单精度f64
,64位,双精度
- Rust 的浮点类型采用了 IEEE-754 标准来表述
- f64 是默认类型,因为在现代CPU上 f64 和 f32 的速度差不多,而且精度更高
布尔类型
- Rust的布尔类型也有2个值:
true
和false
- 一个字节大小
- 符号是
bool
字符类型
- Rust语言中
char
类型被用来描述语言中最基础的单个字符 - 字符类型的字面值使用单引号
- 占用4字节大小
- 是Unicode标量值,可以表示比ASCII多得多的字符内容:拼音、中日韩文、零长度空白符、emoji表情等
- U+0000 到 U+D7FF
- U+E000 到 U+10FFF
- 但是Unicode中并没有”字符“的概念,所以直觉上认识的字符也许与Rust中的概念并不相符
let x := 'z';
let y : char = 'Z';
let z = '😂';
复合类型
- 复合类型可以将多个值放在一个类型里
- Rust提供了2种基础的复合类型:元组(Tuple)、数组
Tuple
- Tuple 可以将多个类型的多个值放在一个类型里
- Tuple 的长度是固定的:一旦声明就无法改变
创建Tuple
- 在小括号里,将值用逗号分开
- Tuple 中的每个位置都对应一个类型,Tuple 中各元素的类型不必相同
let tup: (i32, f64, u8) = (500, 6.4, 1);
获取Tuple的元素值
- 可以使用模式匹配来解构(destructure)一个Tuple来获取元素的值
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;
println!("{}, {}, {}", x, y, z);
访问Tuple的元素
- 在tuple变量使用点标记法,后接元素的索引号
let tup: (i32, f64, u8) = (500, 6.4, 1);
println!("{}, {}, {}", tup.0, tup.1, tup.2);
数组
- 数组也可以将多个值放在一个类型里
- 数组中每个元素的类型必须相同
- 数组的长度也是固定的
声明一个数组
- 在括号里,各值用逗号分开
数组的用处
- 如果想让你的数据存放在stack(栈)上而不是heap(堆)上,或者想保证有固定数量的元素,这时使用数组更有好处
- 数组没有Vector灵活
- Vector和数组类似,它由标准库提供
- Vector的长度可以改变
- 如果你不确定应该用数组还是Vector,那么估计你应该用Vector
数组的类型
- 数组的类型以这种形式表示:
[类型; 长度]
- 例如:
let a: [i32; 5] = [1, 2, 3, 4, 5];
- 例如:
另一种声明数组的方法
- 如果数组的每个元素值都相同,那么可以:
- 在中括号里指定初始值
- 然后是一个“;”
- 最后是数组的长度
- 例如:
let a = [3; 5];
它就相当于:let a = [3, 3, 3, 3, 3];
访问数组的元素
- 数组是Stack上分配的单个块的内存
- 可以使用索引来访问数组的元素
let first = months[0];
let second = months[1];
- 如果访问的索引超出了数组的范围,那么:
- 编译会通过
- 运行会报错(runtime 时会 panic)
- Rust不会允许其继续访问相应地址的内存
函数
- 声明函数使用
fn
关键字 - 依照惯例,针对函数和变量名,Rust使用snake case命名规范:
- 所有的字母都是小写的,单词之间使用下划线分开
函数的参数
- parameter,arguments
- 在函数的签名里,必须声明每个参数的类型
fn main() {
another_function(5); // arguments
}
fn another_function(x: i32) { // parameter
println!("the value of x is: {}", x);
}
函数体中的语句和表达式
- 函数体由一系列语句组成,可选的由一个表达式结束
- Rust是一个基于表达式的语言
- 语句是执行一些动作的指令
- 表达式会计算并产生一个值
- 函数的定义也是语句
- 语句不返回值,所以不可以使用let将一个语句赋给一个变量
函数的返回值
- 在
->
符号后边声明函数返回值的类型,但是不可以为返回值命名 - 在Rust里面,返回值就是函数体里面最后一个表达式的值
- 若想提前返回,需使用
return
关键字,并指定一个值- 大多数函数都是默认使用最后一个表达式作为返回值
fn plus_five(x: i32) -> i32 {
x + 5
}
注释
- 单行注释:
//
- 多行注释:
/**/
控制流
if 表达式
- if 表达式允许您根据条件来执行不同的代码分支
- 这个条件必须是bool类型
- if表达式中,与条件相关联的代码就叫做分支(arm)
- 可选的,在后边加上一个else表达式
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
使用else if 处理多重条件
- 如果使用了多于1个else if,那么最好使用match来重构代码
在let语句中使用if
- 因为if是一个表达式,所以可以将它放在let语句中等号的右边
let condition = true;
let number = if condition { 5 } else { 6 };
Rust的循环
- Rust提供了3种循环:loop,while 和 for
loop循环
loop
关键字告诉Rust反复执行一块代码,直到你喊停- 可以在loop循环中使用
break
关键字来告诉程序何时停止循环
fn main() {
let mut number = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is: {}", result);
}
while条件循环
- 另外一种常见的循环模式就是每次执行循环体之前都判断一次条件
- while条件循环为这种模式而生
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number = number - 1;
}
println!("LIFTOFF!!!");
}
使用for循环来遍历集合
- 可以使用while或loop来遍历集合,但是易错且低效
- 使用for循环更简洁紧凑,它可以针对集合中的每个元素来执行一些代码
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
- 由于for循环的安全、简洁性,所以它在Rust里用的最多
Range
- 标准库提供
- 指定一个开始数字和一个结束数字,range可以生成它们之间的数字(不含结束)
- rev 方法可以反转Range
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!!!");
所有权
- 所有权是Rust最独特的特性,它让Rust无需GC就可以保证内存安全
什么是所有权
- Rust的核心特性就是所有权
- 所有程序在运行时都必须管理它们使用计算机内存的方式
- 有些语言由垃圾收集机制,在程序运行时,它们会不断地寻找不再使用的内存
- 在其他语言中,程序员必须显式地分配和释放内存
- Rust采用了第三种方式:
- 内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则
- 当程序运行时,所有权特性不会减慢程序的运行速度
栈内存(stack) vs 堆内存(heap)
- 在像Rust这样的系统级编程语言里,你为什么要做某些决定是有很大的影响的
- 在代码运行的时候,stack和heap都是可用的内存,但它们的结构很不相同
存储数据
- stack按值的接收顺序来存储,按相反的顺序将它们移除(后进先出,LIFO)
- 添加数据叫做压入栈
- 移除数据叫做弹出栈
- 所有存储在stack上的数据必须拥有已知的固定的大小
- 编译时大小未知的数据或运行时大小可能发生变化的数据必须存放在heap上
- heap内存组织性差一些
- 当你把数据放入heap时,你会请求一定数量的空间
- 操作系统会在heap里面找到一块足够大的空间,把它标记为在用,并返回一个指针,也就是这个空间的地址
- 这个过程叫做在heap上进行内存分配,有时仅仅称为“分配”
- 把值压到stack上不叫分配
- 因为指针是已知固定大小的,可以把指针存放在stack上
- 但如果想要实际数据,你必须使用指针来定位
- 把数据压到stack上要比在heap上分配快得多
- 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在stack的顶端
- 在heap上分配空间需要做更多的工作
- 操作系统首先需要找到一个足够大的空间来存放数据,然后要做好记录方便下次分配
访问数据
- 访问heap中的数据要比访问stack中的数据慢,因为需要通过指针才能找到heap中的数据
- 对于现代的处理器来说,由于缓存的缘故,如果指令在内存中跳转的次数越少,那么速度就越快
- 如果数据存放的距离比较近,那么处理器的处理速度就会更快一些(stack上)
- 如果数据之间的距离比较远,那么处理速度就会慢一些(heap上)
- 在heap上分配大量的空间也是需要时间的
函数调用
- 当你的代码调用函数时,值被传入到函数(也包括指向heap的指针)。函数本地的变量被压倒stack上,当函数结束后,这些值会从stack上弹出
所有权存在的原因
- 所有权能解决的问题:
- 跟踪代码的哪些部分正在使用heap的哪些数据
- 最小化heap上的重复数据量
- 清理heap上未使用的数据避免空间不足
- 管理heap数据就是所有权存在的原因
所有权规则
- 每个值都有一个变量,这个变量就是该值的所有者
- 每个值同时只能有一个所有者
- 当所有者超出作用域(scope)时,该值将被删除
变量作用域
- Scope就是程序中一个项目的有效范围
String类型
- String类型比那些基础标量数据类型更复杂
- 字符串字面值:程序里手写的那些字符串值。它们时不可变的
- Rust还有第二种字符串类型:String
- 在heap上分配,能够存储在编译时未知数量的文本
创建String类型的值
- 可以使用
from
函数从字符串字面值创建出String类型 let s = String::from("hello");
- "::"表示from时String类型下的函数
- 这类字符串是可以被修改的
内存和分配
- 字符串字面值,在编译时就知道它的内容了,其文本内容直接被硬编码到最终的可执行文件里
- 速度快、高效。是因为其不可变性
- String类型,为了支持可变性,需要在heap上分配内存来保存编译时未知的文本内容:
- 操作系统必须在运行时来请求内存
- 这步通过调用
String::from()
来实现
- 这步通过调用
- 当用完String之后,需要使用某种方式将内存返回给操作系统
- 这步,在拥有GC的语言中,GC会跟踪并清理不再使用的内存
- 没有GC,就需要我们去识别内存何时不再使用,并调用代码将它返回
- 如果忘了,就会浪费内存
- 如果提前做了,变量就会非法
- 如果做了两次,就是bug,必须一次分配对应一次释放
- 操作系统必须在运行时来请求内存
- Rust采用了不同的方式,对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动的交还给操作系统
drop()
函数
变量和数据交互的方式
移动(Move)
- 多个变量可以与同一个数据使用一种独特的方式来交互
let x = 5;
let y = x;
- 整数时已知且固定大小的简单的值,这两个5被压到了stack中
let s1 = String::from("hello");
let s2 = s1;
- 一个String类型由3部分组成:
- 一个指向存放字符串内容的指针
- 一个长度
- 一个容量
- 上面这些东西放在stack上
- 存放字符串内容的部分在heap上
- 长度len,就是存放字符串内容所需的字节数
- 容量capacity是指String从操作系统总共获得内存的总字节数
- 把s1赋给s2.String的数据就被复制了一份
- 在stack上复制了一份指针、长度、容量
- 并没有复制指针所指向的heap上的数据
- 当变量离开作用域时,Rust会自动调用
drop
函数,并将变量使用的heap内存释放 - 当s1、s2离开作用域时,它们都会尝试释放相同的内存:
- 二次释放(double free)bug
- 为了保证内存的安全:
- Rust没有尝试复制被分配的内存
- Rust让s1失效
- 当s1离开作用域时,Rust不需要释放任何东西
- 浅拷贝(shallow copy)
- 深拷贝(deep copy)
- 你也许会将复制指针、长度、容量视为浅拷贝,但由于Rust让s1失效了,所以我们用一个新的术语:移动(Move)
- 隐含的一个设计原则:Rust不会自动创建数据的深拷贝
- 就运行时的性能而言,任何自动赋值的操作都是廉价的
克隆(Clone)
- 如果真相对heap上面的String数据进行深度拷贝,而不仅仅是stack上的数据,可以使用
clone
方法
复制(Copy)
- 针对stack上的数据
- Copy trait,可以用于像整数这样完全存放在stack上面的类型
- 如果一个类型实现了Copy这个trait,那么旧的变量在赋值后仍然可用
- 如果一个类型或者该类型的一部分实现了Drop trait,那么Rust不允许让它再去实现Copy trait 了
一些拥有Copy trait的类型
- 任何简单标量的组合类型都可以是Copy的
- 任何需要分配内存或某种资源的都不是Copy的
- 一些拥有Copy trait的类型:
- 所有的整数类型,例如:u32
- bool
- char
- 所有的浮点类型,例如:f64
- Tuple(元组),如果其所有的字段都是Copy的
- (i32, i32) ✔
- (i32, String) ❌
所有权与函数
- 在语义上,将值传递给函数和把值赋给变量是类似的:
- 将值传递给函数将发生移动或复制
返回值与作用域
- 函数在返回值的过程中同样也会发生所有权的转移
- 一个变量的所有权总是遵循同样的模式:
- 把一个值赋给其他变量时就会发生移动
- 当一个包含heap数据的变量离开作用域时,它的值就会被drop函数清理,除非数据的所有权移动到另一个变量上了
如何让函数使用某个值,但不获得其所有权?
- Rust有一个特性叫做“引用(Reference)”
引用和借用
- 参数类型是 &String 而不是 String
&
符号就表示引用:允许你引用某些值而不取得其所有权
借用
- 我们把引用作为函数参数这个行为就叫做借用
- 不可以修改借用的东西
- 和变量一样,引用默认也是不可变的
可变引用
fn main() {
let mut s1 = String::from("hello");
let len = caculate_length(&mut s1);
println!("The length of '{}' is {}.", s1, len);
}
fn caculate_length(s: &mut String) -> usize {
s.push_str(", world");
s.len()
}
- 可变引用有一个重要的限制:在特定作用域内,对某一数据,只能有一个可变的引用
- 这样做的好处是可在编译时防止数据竞争
- 以下三种行为下会发生数据竞争:
- 两个或多个指针同时访问一个数据
- 至少有一个指针用于写入数据
- 没有使用任何机制来同步对数据的访问
- 可以通过创建新的作用域,来允许非同时的创建多个可变引用
fn main() {
let mut s = String::from("Hello");
{
let s1 = &mut s;
}
let s2 = &mut s;
}
另外一个限制
- 不可以同时拥有一个可变引用和一个不变的引用
- 多个不变的引用是可以的
悬空引用 Dangling Reference
- 悬空指针(Dangling Pointer):一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其他人使用了
- 在Rust里,编译器可以保证引用永远都不是悬空引用:
- 如果你引用了某些数据,编译器将保证在引用离开作用域之前数据不会离开作用域
引用的规则
- 在任何给定的时刻,只能满足下列条件之一:
- 一个可变的引用
- 任意数量不可变的引用
- 引用必须一直有效
切片
- Rust的另外一种不持有所有权的数据类型:切片(slice)
- 编写一个函数:
- 它接收字符串作为参数
- 返回它在这个字符串里找到的第一个单词
- 如果函数没有找到任何空格,那么整个字符串就被返回
fn first_word(s: &String) -> usize {
let butes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
字符串切片
- 字符串切片是指向字符串中一部分内容的引用
let s = String::from("Hello world");
let hello = &s[0..5];
let world = &s[6..11];
- 形式:
[开始索引..结束索引]
- 开始索引就是切片其实位置的索引值
- 结束索引是切片终止位置的下一个索引值
注意:
- 字符串切片的索引范围必须发生在有效的UTF-8字符边界内
- 如果尝试从一个多字节的字符中创建字符串切片,程序会报错并退出
使用切片重写案例
fn first_word(s: &String) -> usize {
let butes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
字符串字面值是切片
- 字符串字面值被直接存储在二进制程序中
let s = "Hello world!";
- 变量s的类型是&str,它是一个指向二进制程序特定位置的切片
- &str 是不可变引用,所以字符串字面值也是不可变的
将字符串切片作为参数传递
fn first_word(s: &String) -> &str {}
- 有经验的Rust开发者会采用 &str 作为参数类型,因为这样就可以同时接收String和&str类型的参数了
fn first_word(s: &str) -> &str {}
- 使用字符串切片,直接调用该函数
- 使用String,可以创建一个完整的String切片来调用该函数
- 定义函数时使用字符串切片来替代字符串引用会使API更加通用,且不会损失任何功能
Struct
什么时struct
- struct,结构体
- 自定义的数据类型
- 为相关联的值命名,打包 => 有意义的组合
定义struct
- 使用
struct
关键字,并为整个struct命名 - 在花括号内,为所有字段(Field)定义名称和类型
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
实例化struct
- 想要使用struct,需要创建struct的实例:
- 为每个字段指定具体的值
- 无需按声明的顺序进行指定
let user1 = User {
email: String::from("acb@126.com"),
username: String::from("Nikky"),
active: true,
sign_in_count: 556,
};
user1.email = String::from("anotheremail@example.com")
;```
>注意:
>- 一旦struct的实例是可变的,那么实例中所有的字段都是可变的
### struct作为函数的返回值
```rust
fn build_user(email: String, username: String) -> User {
User {
username: username,
email: email,
sign_in_count: 1,
active: true,
}
}
字段初始化简写
- 当字段名与字段值对应变量名相同时,就可以使用字串初始化的简写方式:
fn build_user(email: String, username: String) -> User {
User {
username,
email,
sign_in_count: 1,
active: true,
}
}
struct更新语法
- 当你想基于某个struct实例来创建一个新实例的时候,可以使用struct更新语法:
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername"),
active: user1.active,
sign_in_count: user1.sign_in_count,
};
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername"),
..user1,
};
Tuple Struct
- 可以定义类似Tuple的struct,叫做tuple struct
- Tuple struct 整体有个名,但里面的元素没有名
- 适用:想给整个tuple起名,并让它不同于其他tuple,而且又不需要给每个元素起名
- 定义tuple struct:使用
struct
关键字,后边是名字,以及里面元素的类型
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
- black和origin是不同的类型,是不同tuple struct的实例
Unit-Like Struct (没有任何字段)
- 可以定义没有任何字段的struct,叫做Unit-Like struct(与(),单元类型类似)
- 适用于需要在某个类型上实现某个trait,但是在里面有没有想要存储的数据
struct 数据的所有权
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
- 这里的字段使用了String而不是&str:
- 该struct实例拥有自身所有的数据
- 只要struct实例是有效的,那么里面的字段数据也是有效的
- struct里也可以存放引用,但这需要使用生命周期
- 生命周期保证只要struct实例是有效的,那么里面的引用也是有效的
struct的方法
- 方法和函数类似:fn 关键字、名称、参数、返回值
- 方法与函数不同之处:
- 方法是在struct(或enum、trait对象)的上下文中定义
- 第一个参数是self,表示方法被调用的struct实例
struct Rectangle {
width: u32,
length: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.length
}
}
fn main() {
let tect = Rectangle {
width: 30,
length: 50,
}
println!("{}", rect.area())
}
定义方法
- 在impl块里定义方法
- 方法的第一个参数可以是&self,也可以获得其所有权或可变借用,和其他参数一样
- 更良好的代码组织
方法调用的运算符
- C/C++:
object->something()
和(*object*).something()
一样 - Rust 没有 -> 运算符
- Rust会自动引用或解引用
- 在调用方法时就会发生这种行为
- 在调用方法时,Rust会根据情况自动添加&、&mut或*,以便object可以匹配方法的签名
- 下面两行代码的效果相同:
p1.distance(&p2);
(&p1).distance(&p2);
方法参数
- 方法可以有多个参数
关联函数
- 可以在impl块里定义不把self作为第一个参数的函数,它们叫关联函数(不是方法)
- 例如:
String::form()
- 例如:
- 关联函数通常用于构造器
::
符号- 关联函数
- 模块创建的命名空间
多个impl块
- 每个struct允许拥有多个impl块
枚举与模式匹配
定义枚举
- 枚举允许我们列举所有可能的值来定义一个类型
- IP地址:IPv4、IPv6
enum IpAddrKind {
V4,
V6,
}
枚举值
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
- 枚举的变体都位于标识符的命名空间下,使用两个冒号
::
进行分隔
将数据附加到枚举的变体中
enum IpAddr {
V4(String),
V6(String),
}
- 优点:
- 不需要额外使用struct
- 每个变体可以拥有不同的类型以及关联的数据量
- 例如:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
标准库中的IpAddr
struct Ipv4Addr {
}
struct Ipv6Addr {
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
为枚举定义方法
- 也是用
impl
关键字
Option枚举
- 定义于标准库中
- 在Prelude(预导入模块)中
- 描述了:某个值可能存在(某种类型)或不存在的情况
Rust没有Null
- 其他语言中:
- Null是一个值,它表示“没有值”
- 一个变量可以处于两种状态:空值(null)、非空
- Null引用:BIllion Dollar Mistake
- Null的问题在于:当你尝试像使用非Null值那样使用Null值的时候,就会引起某种错误
- Null的概念还是有用的:因某种原因而变为无效或缺失的值
Rust中类似Null概念的枚举——Option
- 标准库中的定义:
enum Option<T> {
Some(T),
None,
}
- 它包含在预导入模块中,可以直接使用:
- Option
- Some(T)
- None
let some_number = Some(5);
let some_string = Some("A String");
let absent_number: Option<i32> = None;
Option 比 Null好在哪?
- Option 和 T 是不同的类型,不可以把Option直接当成T
- 若想使用Option中的T,必须将它转换为T
控制流运算符——match
- match允许一个值与一系列模式进行匹配,并执行匹配的模式对应的代码
- 模式可以是字面值、变量名、通配符…
绑定值的模式
- 匹配的分支可以绑定到被匹配对象的部分值
- 因此,可以从enum变体中提取值
match匹配的时候必须穷举所有的可能
_
通配符:替代其余没列出的值
if let
- 处理只关心一种匹配而忽略其他匹配的情况
if let Some(3) = v {
println!("three");
}
- 更少的代码,更少的缩进,更少的模板代码
- 放弃了穷举的可能
- 可以把
if let
看作是match的语法糖 - 搭配
else
if let Some(3) = v {
println!("three");
} else {
println!("others");
}
Package, Crate, Module
Package、Crate、定义Module
Rust的代码组织
- 代码组织主要包括:
- 哪些细节可以暴露,哪些细节是私有的
- 作用域内哪些名称有效
- …
- 模块系统:
- Package(包):Cargo的特性,让你构建、测试、共享crate
- Crate(单元包):一个模块树,它可以产生一个library或可执行文件
- Module(模块):让你控制代码的组织、作用域、私有路径
- Path(路径):为struct、function或module等命名的方式
Package 和 Crate
- Crate的类型:
- library
- binary
- Crate Root:
- 是源代码文件
- Rust编译器从这里开始,组成你的Crate的根Module
- 一个Package:
- 包含1个Cargo.toml,它描述了如何构建这些Crates
- 只能包含0-1个library crate
- 可以包含任意数量的binary crate
- 但必须至少包含一个crate(library或binary)
Cargo的惯例
- src/main.rs:
- binary crate 的 crate root
- crate名与package名相同
- src/lib.rs:
- package包含一个library crate
- library crate 的 crate root
- crate名与package名相同
- Cargo把crate root文件交给rustc来构建library或binary
- 一个Package可以同时包含src/main.rs和src/lib.rs
- 一个binary crate,一个library crate
- 名称与package名相同
- 一个Package可以有多个binary crate:
- 文件放在src/bin
- 每个文件是单独的binary crate
Crate的作用
- 将相关功能组合到一个作用域内,便于在项目间进行共享
- 防止冲突
- 例如 rand crate,访问它的功能需要通过它的名字:rand
定义module来控制作用域和私有性
- Module:
- 在一个crate内,将代码进行分组
- 增加可读性,易于复用
- 控制项目(item)的私有性(public、private)
- 建立module:
mod
关键字- 可嵌套
- 可包含其他项(struct、enum、常量、trait、函数等)的定义
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
Module
- src/main.rs和src/lib.rs叫做crate root:
- 这两个文件(任意一个)的内容形成了名为crate的模块,位于整个模块树的根部
- 整个模块树在隐式的crate模块下
路径 Path
- 为了在Rust的模块中找到某个条目,需要使用路径
- 路径的两种形式:
- 绝对路径:从crate root开始,使用crate名或字面值crate
- 相对路径:从当前模块开始,使用
self
,super
关键字或当前模块的标识符
- 路径至少由一个标识符组成,标识符之间使用
::
// lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
crate::front_of_house::hosting::add_to_waitlist();
front_of_house::hosting::add_to_waitlist();
}
私有边界(privacy boundary)
- 模块不仅可以组织代码,还可以定义私有边界
- 如果想把函数或struct等设为私有,可以将它们放到某个模块中
- Rust中所有的条目(函数、方法、struct、enum、模块、常量)默认是私有的
- 父级模块无法访问子模块中的私有条目
- 子模块里可以使用所有祖先模块中的条目
pub 关键字
- 使用
pub
关键字来将某些条目标记为公共的
super 关键字
super
:用来访问父级模块路径中的内容,类似文件系统中的..
fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_rider();
super::serve_order();
}
fn serve_order() {}
}
pub struct
- pub 放在 struct 前:
- struct是公共的
- struct的字段默认是私有的
- struct的字段需要单独设置pub来变成公有的
pub enum
- pub放在enum前:
- enum是公共的
- enum的变体也都是公共的
use 关键字
- 可以使用
use
关键字将路径导入到作用域内- 仍遵循私有性原则
use crate::front_of_house::hosting;
- 使用use来指定相对路径
use fornt_of_house::hosting;
use 的习惯用法
- 函数:将函数的父级模块引入作用域(指定到父级)
- struct、enum、其他:指定完整路径(指定到本身)
- 同名条目:指定到父级
as 关键字
as
关键字可以为引入的路径在本地指定本地的别名
use std::fmt::Result;
use std::io::Result as IoRusult;
使用 pub use 重新导出名称
- 使用
use
将路径(名称)导入到作用域内后,该名称在此作用域内是私有的 pub use
:重导出- 将条目引入作用域
- 该条目可以被外部代码引入到它们的作用域
使用外部包(package)
- Cargo.toml添加依赖的包(package)
- https://crates.io/
use
将特定条目引入作用域
- 标准库(std)也被当作外部包
- 不需要修改Cargo.toml来包含std
- 需要使用
use
将std中的特定条目引入当前作用域
使用嵌套路径清理大量的use语句
- 如果使用同一个包或模块下的多个条目
- 可以使用嵌套路径在同一行内将上述条目进行引入:
路径相同的部分::{路径差异的部分}
use std::cmp::Ordering;
use std::io;
use std::{cmp::Ordering, io};
- 如果两个
use
路径之一是另一个的子路径- 使用
self
- 使用
use std::io;
use std::io::Write;
use std::io::{self, Write};
通配符 *
- 使用
*
可以把路径中所有的公共条目都引入到作用域
use std::collections::*;
- 注意:谨慎使用
- 应用场景:
- 测试:将所有被测试的代码引入到tests模块
- 有时被用于预导入(prelude)模块
将模块拆分为不同文件
将模块的内容移动到其他文件
- 模块定义时,如果模块名后面是
;
,而不是代码块:- Rust会从模块同名的文件中加载内容
- 模块树的结构不会变化
- 随着模块逐渐变大,该技术让你可以把模块的内容移动到其他文件中
常用的集合
Vector
使用Vector存储多个值
Vec<T>
,叫做vector- 由标准库提供
- 可存储多个值
- 只能存储相同类型的数据
- 值在内存中连续存放
创建Vector
Vec::new()
函数
let v: Vec<i32> = Vec::new();
- 使用初始值创建Vec,使用
vec!
宏
let v = vec![1, 2, 3];
更新Vector
- 向Vector添加元素,使用
push
方法
let mut v = Vec::new();
v.push(1);
删除Vector
- 与其他任何struct一样,当Vector离开作用域后
- 它就被清理掉了
- 它所有的元素也被清理掉了
读取Vector的元素
- 两种方式可以引用Vector里的值:
- 索引
get
方法
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
let second = v.get(1);
索引和get处理访问越界
- 索引:panic
- get:返回None
所有权和借用规则
- 不能在同一作用域内同时拥有可变和不可变引用
遍历Vector中的值
- for循环
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
使用enum来存储多种数据类型
- enum的变体可以附加不同类型的数据
- enum的变体定义在同一个enum类型下
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
fn main() {
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Float(10.12),
SpreadsheetCell::Text(String::from("blue")),
];
}
String
Rust开发者将常会被字符串困扰的原因
- Rust倾向于暴露可能的错误
- 字符串数据结构复杂
- UTF-8
字符串是什么
- Byte的集合
- 一些方法
- 能将byte解析为文本
- Rust的核心语言层面,只有一个字符串类型:字符串切片
str
(或&str
) - 字符串切片:对存储在其他地方、UTF-8编码的字符串的引用
- String类型:
- 来自标准库而不是核心语言
- 可增长、可修改、可拥有
通常说的字符串是指?
- String 和 &str
- 标准库里用的多
- UTF-8编码
其他类型的字符串
- Rust的标准库还包含了很多其他的字符串类型,例如:OsString、OsStr、CString、Cstr
- String和Str后缀:拥有或者借用的变体
- 可存储不同编码的文本或在内存中以不同的形式展现
- Library crate针对存储字符串提供了更多的选项
创建一个新的字符串(String)
- 很多Vec的操作都可用于String
String::new()
函数- 使用初始值来创建String:
to_string()
方法,可用于实现了Display trait的类型,包括字符串字面值String::from()
函数,从字面值创建String
let data = "initial contents";
let s = data.to_string();
let s1 = "initial contents".to_string();
let s2 = String::from("initial contents");
更新String
push_str()
方法:把一个字符串切片附加到String
let mut s = String::from("foo");
s.push_str("bar");
push()
方法:把单个字符附加到String
let mut s = String::from("lo");
s.push('l');
+
:连接字符串- 使用了类似这个签名的方法
fn add(self, s: &str) -> String { ... }
- 只能把&str添加到String
- 解引用强制转换(deref coercion)
- 使用了类似这个签名的方法
let s1 = String::from("Hello, ");
let s2 = String::from("World!");
let s3 = s1 + &s2;
format!
:连接多个字符串- 和
println!()
类似,但返回字符串 - 不会获得参数的所有权
- 和
lwt s1 = String::from("tic");
lwt s2 = String::from("tac");
lwt s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
println!("{}", s);
对String按索引的形式进行访问
- 按索引语法访问String的某部分,会报错
- Rust的字符串不支持索引语法访问
内部表示
- String是对Vec的包装
len()
方法
字节(Bytes)、标量值(Scalar Values)、字形簇(Grapheme Clusters)
- Rust有三种看待字符串的方式:
- 字节
- 标量值
- 字形簇(最接近所谓的“字母”)
- Rust不允许对String进行索引的最后一个原因:
- 索引操作应消耗一个常量时间(O(1))
- 而String无法保证:需要遍历所有内容,来确定有多少个合法字符
切割String
- 可以使用
[]
和一个范围来创建字符串的切片- 必须谨慎使用
- 如果切割时跨越了字符边界,程序就会panic
let s = &hello[0..4];
遍历String的方法
- 对于标量值:
chars()
方法 - 对于字节:
bytes()
方法 - 对于字形簇:很复杂,标准库未提供
String不简单
- Rust选择将正确处理String数据作为所有Rust程序的默认行为
- 程序员必须在处理UTF-8数据之前投入更多的精力
- 可防止在开发后期处理涉及非ASCII字符的错误
HashMap
HashMap<K, V>
- 键值对的形式存储数据,一个键(Key)对应一个值(Value)
- Hash函数:决定如何在内存中存放K和V
创建HashMap
- 创建空HashMap:
new()
函数 - 添加数据:
insert()
方法
use std::collections::HashMap;
fn main() {
let mut scores: HashMap<String, i32> = HashMap::new();
}
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
}
HashMap
- HashMap用的较少,不在Prelude中
- 标准库对其支持较少,没有内置的宏来创建HashMap
- 数据存储在heap上
- 同构的,一个HashMap中:
- 所有的K必须是同一种类型
- 所有的V必须是同一种类型
另一种创建HashMap的方式:collect方法
- 在元素类型为Turple的Vector上使用
collect
方法,可以组建一个HashMap:- 要求Tuple有两个值:一个作为K,一个作为V
collect
方法可以把数据整合成很多种集合类型,包括HashMap- 返回值需要显式指明类型
use std::collections::HashMap;
fn main() {
let teams = vec![String::from("Blue"), String::from("Yellow")];
let intial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.zip(intial_scores.iter()).collect();
}
HashMap和所有权
- 对于实现了Copy trait的类型(例如 i32),值会被复制到HashMap中
- 对于拥有所有权的值(例如 String),值会被移动,所有权会转移给HashMap
- 如果将值的引用插入到HashMap,值本身不会移动
- 在HashMap有效的期间,被引用的值必须保持有效
访问HashMap中的值
get()
方法- 参数:K
- 返回:Option<&V>
遍历HashMap
- for循环
for (k, v) in &scores {
println!("{}: {}", k, v);
}
更新HashMap<K, V>
- HashMap大小可变
- 每个K同时只能对应一个V
- 更新HashMap中的数据:
- K已经存在,对应一个V
- 替换现有的V
- 保留现有的V,忽略新的V
- 合并现有的V和新的V
- K不存在
- 添加一对K,V
- K已经存在,对应一个V
替换现有的V
- 如果项HashMap插入一对KV,然后在插入同样的K,但是不同的V,那么原来的V会被替换掉
只在K不对应任何值的情况下,才插入V
entry()
方法:检查指定的K是否对应一个V- 参数为K
- 返回enum Entry:代表值是否存在
- Entry的
or_insert()
方法:- 返回:
- 如果K存在,返回对应的V的一个可变引用
- 如果K不存在,将方法参数作为K的新值插进去,返回到这个值的可变引用
- 返回:
let e = scores.entry(String::from("Yellow"));
println!("{:?}", e);
e.or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
基于现有V来更新V
use std::collection::HashMap;
fn main() {
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
+count += 1;
}
println!("{:#?}", map);
}
Hash函数
- 默认情况下,HashMap使用加密功能强大的Hash函数,可以抵抗拒绝服务(Dos)攻击
- 不是可用的最快的Hash算法
- 但具有更好的安全性
- 可以指定不同的hasher来切换另一个函数
- hasher是实现BuildHasher trait 的类型
错误处理
panic! 和不可恢复的错误
Rust错误处理概述
- Rust的可靠性:错误处理
- 大部分情况下:在编译时提示错误,并处理
- 错误的分类:
- 可恢复
- 例如文件未找到,可再次尝试
- 不可恢复
- bug:例如访问的索引超出范围
- 可恢复
- Rust没有类似异常的机制
- 可恢复错误:
Result<T, E>
- 不可恢复:
panic!
宏
- 可恢复错误:
不可恢复的错误与panic!
- 当
panic!
宏执行:- 程序会打印一个错误信息
- 展开(unwind)、清理调用栈(Stack)
- 退出程序
为应对panic,展开或中止(abort)调用栈
- 默认情况下,当panic发生:
- 程序展开调用栈(工作最大)
- Rust沿着调用栈往回走
- 清理每个遇到的函数中的数据
- 或立即中止调用栈:
- 不进行清理,直接终止程序
- 内存需要OS进行清理
- 程序展开调用栈(工作最大)
- 想让二进制文件更小,把设置从“展开”改为“中止”:
- 在Cargo.toml中适当的profile部分配置:
panic = 'abort'
- 在Cargo.toml中适当的profile部分配置:
使用panic!产生的回溯信息
- panic! 可能出现在:
- 我们写的代码中
- 我们所依赖的代码中
- 可通过调用panic!的函数的回溯信息来定位引起问题的代码
- 通过设置环境变量
RUST_BACKTRACE
可得到回溯信息 - 为了获取带有调试信息的回溯,必须启用调试符号(不带
--release
)
Result与可恢复的错误
Result枚举
enum Result<T, E> {
Ok(T),
Err(E),
}
T
:操作成功的情况下,Ok变体里返回的数据的类型E
:操作失败的情况下,Err变体里返回的错误的类型
处理Result的一种方式:match表达式
- 和Option枚举一样,Result枚举及其变体也是有prelude带入作用域
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("Error opening file: {:?}", error);
}
};
}
匹配不同的错误
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Error creating file: {:?}", e);
},
other_error => panic!("Error opening the file: {:?}", other_error),
},
};
}
- 上例中使用了很多的match
- match很有用,但很原始
- 闭包(closure),Result<T, E>有很多方法:
- 它们接收闭包来作为参数
- 使用match实现
- 使用这些方法会让代码更简洁
let f = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Error creating file: {:?}", error);
})
} else {
panic!("Error opening file: {:?}", error);
}
});
unwrap
unwrap()
:match 表达式的一个快捷方法:- 如果Result结果是Ok,返回Ok里的值
- 如果Result结果的Err,返回
panic!
宏
expect
expect()
:和unwrap类似,但可以指定错误信息
let f = File::open("hello.txt").expect("无法打开文件");
传播错误
- 将错误返回给调用者
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Resutl<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
fn main() {
let result = read_username_form_file();
}
?运算符
?
运算符:传播错误的一种快捷方式
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Resutl<String, io::Error> {
let f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
fn main() {
let result = read_username_form_file();
}
- 如果Result是Ok:Ok中的值就是表达式的结果,然后继续执行程序
- 如果Result是Err:Err就是整个函数的返回值,就想使用了
return
注意:?运算符只能用于返回Result类型的函数
? 与 from 函数
- Trait std::convert::From 上的
from()
函数:- 用于错误之间的转换
- 被
?
所应用的错误,会隐式的被from()
函数处理 - 当?调用from函数时:
- 它所接收的错误类型会被转化为当前函数返回类型所定义的错误类型
- 用于:针对不同错误原因,返回同一种错误类型
- 只要每个错误类型实现了转换为多返回的错误类型的from函数
链式调用
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Resutl<String, io::Error> {
let mut s = String::new();
let f = File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
fn main() {
let result = read_username_form_file();
}
? 运算符与 main 函数
- main函数的返回类型是:()
- main函数的返回类型也可以是:Result<T, E>
use std::fs::File;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
Box<dyn Error>
是 trait 对象:- 简单理解:“任何可能的错误类型”
什么时候应该使用panic!
总体原则
- 在定义一个可能失败的函数时,优先考虑返回Result
- 否则就panic!
编写示例、原型代码、测试
- 可以使用panic!
- 演示某些概念:unwrap
- 原型代码:unwrap、expect
- 测试:unwrap、expect
有时候你比编译器掌握更多的信息
- 你可以确定Result就是Ok:unwrap
错误处理的指导性建议
- 当代码最终可能处于损坏状态时,最好使用 panic!
- 损坏状态(Bad state):某些假设、保证、约定或不可变性被打破
- 例如:非法的值、矛盾的值或空缺的值被传入代码
- 以及下列中的一条:
- 这种损坏状态并不是预期能够偶尔发生的事情
- 在此之后,代码如果处于这种状态就无法运行
- 在使用的类型中没有一个好的方法来将这些信息(处于损坏状态)进行编码
场景建议
- 调用你的代码,传入无意义的参数值:panic!
- 调用外部不可控的代码,返回非法状态,并且你无法修复:panic!
- 如果失败是可预期的:Result
- 当你的代码对某些值进行操作,首先应该验证这些值:panic!
为验证创建自定义类型
fn main() {
loop {
let guess = "32";
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
}
}
- 创建新的类型,把验证逻辑放在构造实例的函数里
pulic 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
}
}
fn main() {
let guess = "32";
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
let guess = Guess::new(guess);
}
- getter:返回字段的数据
- 字段是私有的:外部无法直接对字段赋值
泛型、Trait、生命周期
提取函数消除重复
重复代码
- 重复代码的危害:
- 容易出错
- 需求变更时需要在多处进行修改
- 消除重复:提取函数
消除重复的步骤
- 识别重复代码
- 提取重复代码到函数体中,并在函数签名中指定函数的输入和返回值
- 将重复的代码使用函数调用进行替代
泛型
- 泛型:提高代码复用能力
- 处理重复代码的问题
- 泛型是具体类型或其他属性的抽象代替:
- 你编写的代码不是最终的代码,而是一种模板,里面有一些“占位符”
- 编译器在编译时将“占位符”替换为具体的类型
- 例如:
fn largest<T>(list: &[T]) -> T { ... }
- 类型参数:
- 很短,通常一个字母
- Camelcase
- T : type 的缩写
函数定义中的泛型
- 泛型函数:
- 参数类型
- 返回类型
struct定义中的泛型
struct Point<T> {
x: T,
y: T,
}
- 可以使用多个类型的泛型参数
- 太多的类型参数:你的代码需要重组为多个更小的单元
struct Point<T, U> {
x: T,
y: U,
}
Enum定义中的泛型
- 可以让枚举的变体持有泛型数据类型
- 例如:Option、Result<T, E>
方法定义中的泛型
- 为struct或enum实现方法的时候,可以在定义中使用泛型
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
- 注意:
- 把 T 放在 impl 关键字后,表示在类型 T 上实现方法
- 例如:
impl<T> Point<T>
- 例如:
- 只针对具体类型实现方法(其余类型没实现方法)
- 例如:
impl Point<f32>
- 例如:
- 把 T 放在 impl 关键字后,表示在类型 T 上实现方法
- struct里的泛型类型参数可以和方法的泛型类型参数不同
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
泛型代码的性能
- 使用泛型的代码和使用具体类型的代码运行速度是一样的
- 单态化(monomorphization):
- 在编译时将泛型替换为具体类型的过程
Trait
- Trait告诉Rust编译器:
- 某种类型具有哪些并且可以与其他类型共享的功能
- Trait:抽象的定义共享行为
- Trait bounds(约束):将泛型类型参数指定为实现了特定行为的类型
- Trait与其他语言中的接口(interface)类似,但有些区别
定义一个Trait
- Trait的定义:把方法签名放在一起,来定义实现某种目的所必需的一组行为
- 关键字:
trait
- 只有方法签名,没有具体实现
- trait可以有多个方法:每个方法签名占一行,以
;
结尾 - 实现该trait的类型必须提供具体的方法实现
- 关键字:
pub trait Summary {
fn summarize(&self) -> String;
}
在类型上实现trait
- 与为类型实现方法类似
- 不同之处:
impl Xxxx for Tweet {...}
- 在impl的块里,需要对Trait里的方法进行具体的实现
实现Trait的约束
- 可以在某个类型上实现某个trait的前提条件是:
- 这个类型或这个trait是在本地crate里定义的
- 无法为外部类型来实现外部的trait:
- 这个限制是程序属性的一部分(也就i是一致性)
- 更具体地说就是孤儿规则:之所以这样命名是因为父类型不存在
- 此规则可以确保其他人的代码不能破坏你的代码,反之亦然
- 如果没有这个规则,两个crate可以为同一类型实现同一个trait,Rust就不知道应该使用哪个实现了
默认实现
pub trait Summary {
fn summarize(&self) -> String {
String::from("Read more ...")
}
}
- 默认实现的方法可以调用trait中其他的方法,即时这些方法没有默认实现
pub trait Summary {
fn summarize_author(&self) -> Stromg;
fn summarize(&self) -> String {
format!("Read more from {} ...", self.summarize_author())
}
}
- 注意:无法从方法的重写实现里面调用默认的实现
Trait 作为参数
- impl Trait 语法:适用于简单情况
pub fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}
- Trait bound 语法:可用于复杂情况
- impl trait 语法是 Trait bound 的语法糖
pub fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}
- 使用
+
指定多个 Trait bound
pub fn notify(item: impl Summary + Display) {
println!("Breaking news! {}", item.summarize());
}
pub fn notify<T: Summary + Display>(item: T) {
println!("Breaking news! {}", item.summarize());
}
- Trait bound 使用 where 子句
pub fn notify<T: Summary + Display, U: Clone + Debug>(a: T, b: U) -> String {
format!("Breaking news! {}", a.summarize());
}
pub fn notify<T, U>(a: T, b: U) -> String
where T: Summary + Display,
U: Clone + Debug,
{
format!("Breaking news! {}", a.summarize());
}
使用Trait作为返回类型
- impl trait 语法
pub fn notify(s: &str) -> impl Summary { }
- 注意:使用impl trait只能返回确定的同一种类型,返回可能不同类型的代码会报错
- trait bound 语法
fn larget<T: PartialOrd + Copy>(list: &[T]) -> T {}
使用trait bound 有条件的实现方法
- 在使用泛型类型参数的impl块上使用Trait bound,我们可以有条件的为实现了特定Trait的类型来实现方法
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self {s, 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);
}
}
}
- 也可以为实现了其他Trait的任意类型有条件的实现某个Trait
- 为满足Trait bound的所有类型上实现Trait叫做覆盖实现(blanket implementations)
生命周期
生命周期
- Rust的每个引用都有自己的生命周期
- 生命周期:引用保持有效的作用域
- 大多数情况:生命周期是隐式的、可被推断的
- 当引用的生命周期可能以不同的方式相互关联时:手动标注生命周期
生命周期——避免悬垂引用(dangling reference)
- 生命周期的主要目标:避免悬垂引用(dangling reference)
借用检查器
- Rust编译器的借用检查器:比较作用域来判断所有的借用是否合法
函数中泛型的生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if s.len() > y.len() {
x
} else {
y
}
}
生命周期标注语法
- 生命周期的标注并不会改变引用的生命周期长度
- 当指定了泛型生命周期参数,函数可以接收带有任何生命周期的引用
- 生命周期的标注:描述了多个引用的生命周期的关系,但不影响生命周期
生命周期标注——语法
- 生命周期参数名:
- 以
'
开头 - 通常全小写且非常短
- 很多人使用
'a
- 以
- 生命周期标注的位置:
- 在引用的
&
符号后 - 使用空格将标注和引用类型分开
- 在引用的
生命周期标注——例子
&i32
一个引用&'a i32
带有显式生命周期的引用&'a mut i32
带有显式生命周期的可变引用
单个生命周期标注本身没有意义
函数签名中的生命周期标注
- 泛型生命周期参数声明在:函数名和参数列表之间的
<>
里
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if s.len() > y.len() {
x
} else {
y
}
}
- 生命周期
'a
的实际生命周期是:x和y两个生命周期中较小的那个
深入理解生命周期
- 指定生命周期参数的方式依赖于函数所做的事情
- 从函数返回引用时,返回类型的生命周期需要与其中一个参数的生命周期匹配
- 如果返回的引用没有指向任何参数,那么它只能引用函数内创建的值
- 这就是悬垂引用:改值在函数结束时就走出了作用域
struct定义中的生命周期标注
- Struct里可包括:
- 自持有的类型
- 引用:需要在每个引用上添加生命周期标注
struct ImportantExcerpt<'a> {
part: &'a str,
}
生命周期的省略
- 我们知道:
- 每个引用都有生命周期
- 需要为使用生命周期的函数或struct指定生命周期参数
生命周期省略规则
- 在Rust引用分析中所编入的模式称为生命周期省略规则
- 这些规则无需开发者来遵守
- 它们是一些特殊情况,由编译器来考虑
- 如果你的代码符合这些情况,那么就无需显式标注生命周期
- 生命周期省略规则不会提供完整的推断:
- 如果应用规则后,引用的生命周期仍然模糊不清 -> 编译错误
- 解决办法:添加生命周期标注,表明引用间的相互关系
输入、输出生命周期
- 生命周期在:
- 函数/方法的参数:输入生命周期
- 函数/方法的返回值:输出生命周期
生命周期省略的三个规则
- 编译器使用3个规则在没有显式标注生命周期的情况下,来确定引用的生命周期
- 规则1应用于输入生命周期
- 规则2、3应用于输出生命周期
- 如果编译器应用完3个规则之后,仍然有无法确定生命周期的引用 -> 报错
- 这些规则适用于fn定义和impl块
- 规则1:每个引用类型的参数都有自己的生命周期
- 规则2:如果只有1个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期
- 规则3:如果有多个输入生命周期参数,但其中一个是 ==
&self
或 &mut self
(是方法),那么 self 的生命周期会被赋给所有的输出生命周期参数==
方法定义中的生命周期标注
- 在struct上使用生命周期实现方法,语句和泛型参数的语法一样
- 在哪声明和使用生命周期参数,依赖于:
- 生命周期参数是否和字段、方法的参数或返回值有关
- struct字段的生命周期名:
- 在
impl
后声明 - 在struct名后使用
- 这些生命周期是struct类型的一部分
- 在
- impl块内的方法签名中:
- 引用必须绑定与struct字段引用的生命周期,或者引用时独立的也可以
- 生命周期省略规则经常使得方法中的生命周期标注不是必须的
struct ImportantExcerpt<'a> {
}
impl<'a> ImportantExcerpt<'a> {
}
静态生命周期
'static
是一个特殊的生命周期:整个程序的持续时间- 例如:所有的字符串字面值都拥有
'atatic
生命周期let s: &'static str = "I have a static lifetime.";
- 例如:所有的字符串字面值都拥有
- 为引用指定
'static
生命周期前要三思:- 是否需要引用在程序整个生命周期内都存活
泛型参数类型、trait bound、生命周期
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
}
}
fn main() {}
编写自动化测试
编写和运行测试
测试(函数)
- 测试:
- 函数
- 验证非测试代码的功能是否和预期一致
- 测试函数体(通常)执行的3个操作:
- 准备数据/状态
- 运行被测试的代码
- 断言(Assert)结果
解剖测试函数
- 测试函数需要使用
test
属性(attribute)进行标注- Attribute就是一段Rust代码的元数据
- 在函数上加
#[test]
,可以把函数变成测试函数
运行测试
- 使用
cargo test
命令运行所有测试函数- Rust会构建一个Test Runner可执行文件
- 它会运行标注了test的函数,并报告其运行是否成功
- Rust会构建一个Test Runner可执行文件
- 当使用cargo创建library项目的时候,会生成一个test module,里面有一个test函数
- 你可以添加任意数量的test module或函数
测试失败
- 测试函数panic就表示失败
- 每个测试运行在一个新线程
- 当主线程看见某个测试线程挂掉了,那个测试就标记为失败了
断言(Assert)
使用assert!宏检查测试结果
assert!
宏,来自标准库,用来确定某个状态是否为true- true:测试通过
- false:调用
panic!
,测试失败
使用 assert_eq! 和 assert_ne! 测试相等性
- 都来自标准库
- 判断两个参数是否相等或不等
- 实际上,他们使用的就是
==
和!=
运算符 - 断言失败:自动打印出两个参数的值
- 使用debug格式打印参数
- 要求蚕食实现了 PartialEq 和 Debug Trait(所有的基本类型和标准库里大部分类型都实现了)
- 使用debug格式打印参数
自定义错误消息
添加自定义错误消息
- 可以向
assert!
、assert_eq!
、assert_ne!
添加可选的自定义消息- 这些自定义消息和失败消息都会打印出来
assert!
:第1个参数必填,自定义消息作为第2个参数assert_eq!
和assert_ne!
:前2个参数时必填的,自定义消息作为第3个参数- 自定义消息参数会被传递给
format!
宏,可以使用{}
占位符
使用 should_panic 检查恐慌
验证错误处理的情况
- 测试除了验证代码的返回值是否正确,还需验证代码是否如预期的处理了发生错误的情况
- 可验证代码在特定情况下是否发生了 panic
should_panic
属性(attribute):- 函数 panic :测试通过
- 函数没有 panic :测试失败
让 should_panic 更精确
- 为
should_panic
属性添加一个可选的expected
参数:- 将检查失败消息中是否包含所指定的文字
#[should_panic(expected = "Guess value must be less than or equal to 100")]
在测试中使用Result<T, E>
在测试中使用Result<T, E>
- 无需 panic,可使用 Result<T, E> 作为返回类型写测试:
- 返回Ok:测试通过
- 返回Err:测试失败
- 注意:不要在使用 Result<T, E> 编写的测试上标注
#[should_panic]
控制测试运行
控制测试如何运行
- 改变
cargo test
的行为:添加命令行参数 - 默认行为:
- 并行运行
- 所有测试
- 捕获(不显示)所有输出,使读取与测试结果相关的输出更容易
- 命令行参数:
- 针对
cargo test
的参数:紧跟cargo test
后 - 针对测试可执行程序:放在
--
之后
- 针对
cargo test --help
cargo test -- --help
并行/连续运行测试
并行运行测试
- 运行多个测试:默认使用多个线程并行运行
- 运行快
- 确保测试之间:
- 不会相互依赖
- 不依赖于某个共享状态(环境、工作目录、环境变量等等)
–test-threads 参数
- 传递给二进制文件
- 不想以并行方式运行测试,或想对线程数进行细粒度控制
- 可以使用
--test-threads
参数,后边跟着线程的数量 - 例如:
cargo test --test-threads=1
显式函数输出
- 默认:如果测试通过,Rust的test库会捕获所有打印到标准输出的内容
- 例如:如果被测试代码中用到了 println! :
- 如果测试通过:不会在终端看到 println! 打印的内容
- 如果测试失败:会看到 println! 打印的内容 和 失败的信息
- 如果想在成功的测试中看到打印的内容:
cargo test -- --show-output
按名称运行测试
按名称运行测试的子集
- 选择运行的测试:将测试的名称(一个或多个)作为
cargo test
的参数 - 运行单个测试:指定测试名称
- 例如:
cargo test one_hundred
- 例如:
- 运行多个测试:指定测试名的一部分(模块名也行)
- 例如:
cargo test add
- 例如:
忽略测试
忽略某些测试,运行剩余测试
ignore
属性(attribute)
#[test]
#[ignore]
fn it_works() {}
- 运行被忽略的测试:
cargo test -- --ignored
测试的组织
测试的分类
- Rust对测试的分类:
- 单元测试
- 集成测试
- 单元测试:
- 小、专注
- 一次对一个模块进行隔离的测试
- 可测试private接口
- 集成测试:
- 在库外部。和其他外部代码一样使用你的代码
- 只能使用public接口
- 可能在每个测试中使用到多个模块
单元测试
#[cfg(test)] 标注
- tests 模块上的
#[cfg(test)]
标注:- 只有运行
cargo test
才编译和运行代码 - 运行
cargo build
则不会
- 只有运行
- 集成测试在不同的目录,它不需要
#[cfg(test)]
标注 - cfg:configuration(配置)
- 告诉Rust下面的条目只有在指定的配置选项下才被包含
- 只有
cargo test
才会编译代码,包括模块中的helper
函数和#[test]
标注的函数
- 只有
- 告诉Rust下面的条目只有在指定的配置选项下才被包含
测试私有函数
- Rust允许测试私有函数
集成测试
集成测试
- 在Rust中,集成测试完全位于被测试库的外部
- 目的:是测试被测试库的多个部分是否能正确的一起工作
- 集成测试的覆盖率很重要
tests 目录
- 创建测试目录:tests 目录
- tests 目录下的每个测试文件都是单独的一个crate
- 需要将被测试库导入
- 无需标注
#[cfg(test)]
,tests 目录被特殊对待- 只有
cargo test
,才会编译 tests 目录下的文件
- 只有
运行指定的集成测试
- 运行一个特定的集成测试:
cargo test 函数名
- 运行某个测试文件内的所有测试:
cargo test --test 文件名
集成测试中的子模块
- tests目录下的每个文件被编译成单独的crate
- 这些文件不共享行为(与src目录下的文件规则不同)
针对 binary crate 的集成测试
- 如果项目是binary crate,只含有src/main.rs没有src/lib.rs:
- 不能在tests目录下创建集成测试
- 无法把main.rs的函数导入作用域
- 只有 library crate 才能暴露函数给其他 crate 用
- binary crate 意味着独立运行
项目实例:命令行参数
main.rs
use std::{env, process};
use minigrep::Config;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
if let Err(e) = minigrep::run(config) {
eprintln!("Application error: {}", e);
process::exit(1);
};
}
lib.rs
use std::error::Error;
use std::{env, fs};
use std::io::BufRead;
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
let results = if config.case_sensitive {
search(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
println!("With text:\n{}", contents);
Ok(())
}
pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("Not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config { query, filename, case_sensitive })
}
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
let query = query.to_lowercase();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn case_sensitive() {
let query = "dust";
let contents = "\
Rust:\
safe, fast, productive.\
Pick three.\
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents))
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:\
safe, fast, productive.\
Pick three.\
Trust me.";
assert_eq!(vec!["Rust:", "Trust me."], search_case_insensitive(query, contents))
}
}
函数式语言特性:迭代器和闭包
闭包
什么是闭包
- 闭包:可以捕获其所在环境的匿名函数
- 闭包:
- 是匿名函数
- 保存为变量、作为参数
- 可以在一个地方创建闭包,然后在另一个上下文中调用闭包来完成运算
- 可以从其定义的作用域内捕获值
闭包的类型推断
- 变薄不要求标注参数和返回值的类型
- 闭包通常很短小,只在狭小的上下文中工作,编译器通常能推断出类型
- 可以手动添加类型标注
let expensive_clouser = |num: u32| -> u32 {}
注意:闭包的定义最终只会为参数/返回值推断出唯一具体的类型
函数和闭包的定义语法
fn add_one_v1 (x: u32) -> u32 { x + 1 }
fn add_one_v2 = |x: u32| -> u32 { x + 1 };
fn add_one_v3 = |x| { x + 1 };
fn add_one_v4 = |x| x + 1;
如何让struct持有闭包
- struct的定义需要知道所有字段的类型
- 需要指明闭包的类型
- 每个闭包实例都有自己唯一的匿名类型,即使两个闭包签名完全一样
- 所以需要使用:泛型和Trait bound
Fn Trait
- Fn Trait 由标准库提供
- 所有闭包都至少实现了以下trait之一:
Fn
FnMut
FnOnce
使用缓存器(Cacher)实现的限制
- Cacher实例假定针对不同的参数arg,value方法总会得到相同的值
- 可以使用HashMap代替单个值:
- key:arg参数
- value:执行闭包的结果
- 可以使用HashMap代替单个值:
- 只能接收一个u32类型的参数和u32类型的返回值
闭包可以捕获它们所在的环境
- 闭包可以访问定义它的作用域内的变量,而普通函数则不能
- 会产生内存开销
闭包从所在环境捕获值的方式
- 与函数获得参数的三种方式一样:
- 取得所有权:
FnOnce
- 可变借用:
FnMut
- 不可变借用:
Fn
- 取得所有权:
- 创建闭包时,通过闭包对环境值的使用,Rust推断出具体使用哪个trait:
- 所有闭包都实现了
FnOnce
- 没有移动捕获变量的实现了
FnMut
- 无需可变访问捕获变量的闭包实现了
Fn
- 所有闭包都实现了
move 关键字
- 在参数列表前使用
move
关键字,可以强制闭包取得它所使用的环境值的所有权- 当将闭包传递给新线程以移动数据使其归新线程所有时,此技术最为有用
最佳实践
- 当指定Fn Trait bound之一时,首先使用
Fn
,基于闭包体里的情况,如果需要FnOnce
或FnMut
,编译器会再告诉你
迭代器
什么时迭代器
- 迭代器模式:对一系列项执行某些任务
- 迭代器负责:
- 遍历每个项
- 确定序列(遍历)何时完成
- Rust的迭代器:
- 惰性的:除非调用消费迭代器的方法,否则迭代器本身没有任何效果
Iterator trait
- 所有的迭代器都实现了Iterator trait
- Iterator trait定义于标准库,定义大致如下:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// method eith default implementations elided
}
- type Item 和 Self::Item 定义了与此trait关联的类型
- 实现Iterator trait 需要定义一个Item类型,它用于next方法的返回值类型(迭代器的返回类型)
- Iterator trait仅要求实现一个方法:
next
next
:- 每次返回迭代器中的一项
- 返回结果包裹在
Some
里 - 迭代结束,返回
None
- 可直接在迭代器上调用
next
方法
几个迭代方法
iter
方法:在不可变引用上创建迭代器into_iter
方法:创建的迭代器会获得所有权iter_mut
方法:迭代可变的引用
消耗迭代器的方法
- 在标准库中,Iterator trait有一些带默认实现的方法
- 其中有一些方法会调用
next
方法- 实现Iterator trait时必须实现
next
方法的原因之一
- 实现Iterator trait时必须实现
- 调用
next
的方法叫做“消耗性适配器”- 因为调用它们会把迭代器消耗尽
- 例如:
sum
方法(就会耗尽迭代器)- 取得迭代器的所有权
- 通过反复调用
next
,遍历所有元素 - 每次迭代,就把当前元素添加到一个总和里,迭代结束,返回总和
产生其他迭代器的方法
- 定义在Iterator trait上的另一些方法叫做“迭代器适配器”
- 把迭代器转换为不同类型的迭代器
- 可以通过链式调用使用多个迭代器适配器来执行复杂的操作,这种调用可读性高
- 例如:
map
- 接受一个闭包,闭包作用于每个元素
- 产生一个新的迭代器
collect
方法:消耗性适配器,把结果收集到一个集合类型中
使用闭包捕获环境
filter
方法:- 接收一个闭包
- 这个闭包在遍历迭代器的每个元素时,返回bool类型
- 如果闭包返回true:当前元素将会包含在filter产生的迭代器中
- 如果闭包返回false:当前元素不会包含在filter产生的迭代器中
使用Iterator trait来创建自定义的迭代器
- 实现
next
方法
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter {count: 0}
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Seof::Item> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
讨论循环和迭代器的运行时性能
零开销抽象(Zero-Cost Abstraction)
- 使用抽象时不会引入额外的运行时开销
音频解码器的例子
let buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;
for i in 12..buffer.len() {
let pridiction = coefficients.iter()
.zip(&buffer[i - 12..i])
.map(|(&c, &s)| c * s as i64)
.sum::<i64>() >> qlp_shift;
let delta = buffer[i];
buffer[i] = prediction as i32 + delta;
}
cargo、crates.io
通过 release profile 来自定义构建
release profile
- release profile:
- 是预定义的
- 可自定义:可使用不不同的配置,对代码编译拥有更多的控制
- 每个 profile 的配置都独立于其他的 profile
- Cargo 主要有两个 profile:
dev profile
:适用于开发,cargo build
release profile
:适用于发布,cargo build -release
自定义 profile
- 针对每个 profile,Cargo 都提供了默认的配置
- 如果想自定义 xxxx profile 的配置:
- 可以在 Cargo.toml 里添加
[profile.xxxx]
区域,在里面覆盖默认配置的子集
- 可以在 Cargo.toml 里添加
- 对于每个配置的默认值和完整选项,请参见:https://doc.rust-lang.org/cargo/.
在 https://crates.io/ 上发布库
crates.io
- 可以通过发布包来共享你的代码
- crate 的注册表在 https://crates.io/
- 它会分发已注册的包的源代码
- 主要托管开源的代码
文档注释
- 文档注释:用于生成文档
- 生成HTML文档
- 显式公共API的文档注释:如何使用API
- 使用
///
- 支持Markdown
- 放置在被说明的条目之前
生成HTML文档的命令
cargo doc
- 它会运行 rustdoc 工具(Rust 安装包自带)
- 把生成的HTML文档放在 target/doc 目录下
cargo doc --open
- 构建当前crate的文档(也包含crate依赖项的文档)
- 在浏览器打开文档
常用章节
# Examples
- 其他常用的章节:
# Panics
:函数可能发生 panic 的场景# Errors
:如果函数返回 Result,描述可能的错误种类,以及可导致错误的条件# Safety
:如果函数处于 unsafe 调用,就应该解释函数 unsafe 的原因,以及调用者确保的使用前提
文档注释作为测试
- 示例代码块的附加值:
- 运行
cargo test
:将把文档中的示例代码作为测试来运行
- 运行
为包含注释的项添加文档注释
- 符号:
//!
- 这类注释通常用于描述 crate 和模块:
- crate root (按惯例 src/lib.rs)
- 一个模块内,将 crate 或模块作为一个整体进行记录
使用 pub use 导出方便使用的公共API
- 问题:crate 的程序结构在开发时对于开发者很合理,但对于它的使用者不够方便
- 开发者会把程序结构分为很多层,使用者想找到这种深层结构中的某个类型很费劲
- 例如:
- 麻烦:my_crate::some_module::another_module::UsefulType;
- 方便:my_crate::UsefulType;
- 解决办法:
- 不需要重新组织内部代码结构
- 使用
pub use
:可以重新导出,创建一个于内部私有结构不同的对外公共结构
创建并设置 Crates.io 账号
- 发布 crate 前,需要在crates.io 创建账号并获得 API token
- 运行命令:
cargo login [你的API token]
- 通知 cargo,你的 API token 存储在本地
~/.cargo/credentials
- 通知 cargo,你的 API token 存储在本地
- API token 可以在 https://crates.io/ 进行撤销
为新的 crate 添加元数据
- 在发布 crate 前,需要在 Cargo.toml 的 [package] 区域为 crate 添加一些元数据:
- crate 需要唯一的名称:name
- descroption:一两句话即可,会出现在 crate 搜索的结果里
- license:需要提供许可证标识值(可到 http://spdx.org/licenses/ 查找)
- 可指定多个license:用 OR
- version
- author
- 发布:
cargo publish
命令
发布到 Crates.io
- crate 一旦发布,就是永久性的:该版本无法覆盖,代码无法删除
发布已存在 crate 的新版本
- 修改 crate 后,需要先修改 Cargo.toml 里面的 version 值,再进行重新发布
- 参照http://semver.org/ 来使用你的语义版本
- 再执行
cargo publish
进行发布
使用 cargo yank 从 Crates.io 撤回版本
- 不可以删除 crate 之前的版本
- 但可以防止其他项目把它作为新的依赖:yank(撤回)一个 crate 版本
- 防止新项目依赖于该版本
- 已经存在的项目可以继续将其作为依赖(并可下载)
- yank 意味着:
- 所有已经产生 Cargo.lock 的项目都不会中断
- 任何将来生成的 Cargo.lock 文件都不会使用被 yank 的版本
- 命令:
- yank 一个版本(不会删除任何代码):
cargo yank --vers 1.0.1
- 取消 yank:
cargo yank --vars 1.0.1 --undo
- yank 一个版本(不会删除任何代码):
通过 workspaces 组织大工程
Cargo 工作空间(Workspaces)
- cargo 工作空间:帮助管理多个相互关联且需要协同开发的 crate
- cargo 工作空间是一套共享同一个 Cargo.lock 和输出文件夹的包
创建工作空间
- 有多种方式来组建工作空间:1个二进制 crate,2个库 crate
- 二进制 crate:main 函数,依赖于其他2个库 crate
- 其中1个库 crate 提供 add_one 函数
- 另外1个库 crate 提供 add_two 函数
在工作空间中依赖外部的 crate
- 工作空见只有一个 Cargo.lock 文件,在工作空间的顶层目录
- 保证工作空间内所有 crate 使用的依赖的版本都相同
- 工作空间内所有 crate 相互兼容
从 https://crates.io/ 来安装库
从 Crates.io 安装二进制 crate
- 命令:
cargo install
- 来源:https://crates.io
- 限制:只能安装具有二进制目标(binary target)的 crate
- 二进制目标 binary target:是一个可执行程序
- 由拥有 src/main.rs 或其他被指定为二进制文件的 crate 生成
- 通常:README 里有关于 crate 的描述:
- 拥有 library target
- 拥有 bianry target
- 两者兼备
cargo install
cargo install
安装的二进制文件存放在根目录的 bin 文件夹- 如果你用 rustup 安装的 Rust,没有任何自定义配置,那么二进制存放目录是
$HOME/.cargo/bin
- 要确保该目录在环境变量 $PATH 中
使用自定义命令扩展 cargo
- cargo 被设计成可以使用子命令来扩展
- 例:如果 $PATH 中的某个二进制是 cargo-something,你可以像子命令一样运行:
cargo something
- 类似这样的自定义命令可以通过该命令列出:
cargo --list
- 优点:可使用
cargo install
来安装扩展,像内置工具一样来运行
智能指针
相关的概念
- 指针:一个变量在内存中包含的是一个地址(指向其他数据)
- Ruat 中最常见的指针就是“引用”
- 引用:
- 使用
&
- 借用它指向的值
- 没有其余开销
- 最常见的指针类型
- 使用
智能指针
- 智能指针是这样一些数据结构:
- 行为和指针类似
- 有额外的元数据和功能
引用计数(reference counting)智能指针类型
- 通过记录所有者的数量,使一份数据被多个所有者同时持有
- 并在没有任何所有者时自动清理数据
引用和智能指针的其他不同
- 引用:值借用数据
- 智能指针:很多时候都拥有它所指向的数据
智能指针的例子
- String 和 Vec
- 都拥有一片内存区域,且允许用户对其操作
- 还拥有元数据(例如容量等)
- 提供额外的功能或保障(String 保障其数据时合法的 UTF-8 编码)
智能指针的实现
- 智能指针通常使用 struct 实现,并且实现了:
- Deref 和 Drop 这两个 trait
- Deref trait :允许智能指针 struct 的实例像引用一样使用
- Drop trait :允许你自定义当智能指针实例走出作用域时的代码
本章内容
- 介绍标准库中常见的智能指针
Box<T>
:在 heap 内存上分配值Rc<T>
:启用多重所有权的引用计数类型Ref<T>
和RefMut<T>
,通过RefCell<T>
访问:在运行时而不是编译时强制借用规则的类型
- 此外:
- 内部可变模式(interior mutabillity pattern):不可变类型暴露出可修改其内部值的API
- 引用循环(reference cycles):它们如何泄露内存,以及如何防止其发生
使用Box来指向 Heap 上的数据
Box
Box<T>
时最简单的智能指针:- 允许你在 heap 上存储数据(而不是 stack)
- stack 上是指向 heap 数据的指针
- 没有性能开销
- 没有其他额外功能
- 实现了 Deref trait 和 Drop trait
Box 的使用场景
- 在编译时,某类型的大小无法确定,但使用该类型时,上下文却需要知道它的确切大小
- 当你有大量数据,想移交所有权,但需要确保在操作时数据不会被复制
- 使用某个值时,你只关心它是否实现了特定的 trait,而不关心它的具体类型
使用 Box 在 heap 上存储数据
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
使用 Box 赋能递归类型
- 在编译时,Rust 需要知道一个类型所占的空间大小
- 而递归类型的大小无法在编译时确定
- 但 Box 类型的大小确定
- 在递归类型中使用 Box 就可解决上述问题
- 函数式语言中的 Cons list
关于 Cons List
- Cons List 是来自 Lisp 语言的一种数据结构
- Cons List 里每个成员由两个元素组成
- 当前项的值
- 下一个元素
- Cons List 里最后一个成员只包含一个 Nill 值,没有下一个元素
Cons List 并不是 Rust 的常用集合
- 通常情况下:
Vec<T>
是更好的选择 - 例:创建一个 Cons List
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
enum List {
Cons(i32, List),
Nil,
}
- Rust 如何确定为枚举分配的空间大小——最大元素的空间大小
enum Message {
Quit,
Move {x: i32, y: i32},
Write(String),
ChangeColor(i32, i32, i32),
}
使用 Box 来获得确定大小的递归类型
Box<T>
是一个指针,Rust 知道它需要多少空间,因为:- 指针的大小不会基于它指向的数据的大小变化而变化
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
enum List {
Cons(i32, Box<List>),
Nil,
}
Box<T>
:- 只提供了“间接”存储和 heap 内存分配的功能
- 没有其他额外功能
- 没有性能开销
- 适用于需要 “间接” 存储的场景,例如 Cons List
- 实现了 Deref trait 和 Drop trait
Deref Trait
Deref Trait
- 实现 Deref Trait 使我们可以自定义解引用运算符==
*
的行为== - 通过实现 Deref,智能指针可像常规引用一样来处理
解引用运算符
- 常规引用是一种指针
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
把 Box 当作引用
Box<T>
可以代替上例中的引用
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
定义自己的智能指针
Box<T>
被定义成拥有一个元素的 tuple struct- 例子:MyBox
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type target = T;
fn deref(&delf) -> &T {
&self.0
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
实现 Deref Trait
- 标准库中的 Deref trait 要求我们实现一个
deref
方法:- 该方法借用
self
- 返回一个指向内部数据的引用
- 该方法借用
函数和方法的隐式解引用转化(Deref Coercion)
- 隐式解引用转化(Deref Coercion)是为函数和方法提供的一种便捷特性
- 假设 T 实现了 Deref trait:
- Deref Coercion 可以把 T 的引用始终转化为 T 经过 Deref 操作后生成的引用
- 当把某类型的引用传递给函数或方法时,但它的类型于定义的参数类型不匹配:
- Deref Coercion 就会自动发生
- 编译器会对 deref 进行一系列调用,来把它转化为所需的参数类型
- 在编译时完成,没有额外性能开销
解引用与可变性
- 可使用 DerefMut trait 重载可变引用的 * 运算符
- 类型和 trait 在下列三种情况发生时,Rust会执行 deref coercion:
- 当 T:Deref<Target=U>,允许 &T 转换为 &U
- 当 T:DerefMut<Target=U>,允许 &mut T 转换为 &mut U
- 当 T:Deref<Target=U>,允许 &mut T 转换为 &U
Drop Trait
Drop Trait
- 实现Drop Trait,可以让我们自定义当值将要离开作用域时发生的动作
- 例如:文件、网络资源释放等
- 任何类型都可以实现 Drop Trait
- Drop Trait 只要求实现
drop
方法- 参数:对self的可变引用
- Drop trait 在预导入模块里(prelude)
使用 std::mem::drop 来提前 drop 值
- 很难直接禁用自动的drop功能,也没必要
- Drop trait 的目的就是进行自动的释放处理逻辑
- Rust 不允许手动调用 Drop trait 里的 drop 方法
- 但可以调用标准库的
std::mem::drop
函数,来提前 drop 值
Rc:引用计数智能指针
Rc:引用计数智能指针
- 有时,一个值会有多个持有者
- 为了支持多重所有权:
Rc<T>
- reference counting(引用计数)
- 追踪所有到值的引用
- 0个引用:该值可以被清理掉
Rc<T>
不在预导入模块(prelude)Rc::clone(&a)
函数:增加引用计数Rc<T>::strong_count(&a)
:获得引用计数Rc<T>::weak_count
函数:弱引用计数
Rc 使用场景
- 需要在 heap 上分配数据,这些数据被程序的多个部分读取(只读),但在编译时无法确定哪个部分最后使用玩这些数据
Rc<T>
只能用于单线程场景
Rc::clone() 和 类型的 clone() 方法
Rc::clone()
:增加引用,不会执行数据的深度拷贝操作- 类型的
clone()
:很多会执行数据的深度拷贝操作
Rc
Rc<T>
通过不可变引用,使你可以在程序不同部分之间共享只读数据
RefCell 和内部可变性
内部可变性(interior mutability)
- 内部可变性是 Rust 的设计模式之一
- 它允许你在只持有不可变引用的前提下对数据进行修改
- 数据结构中使用了 unsafe 代码来绕过 Rust 正常的可变性和借用规则
RefCell
- 与
Rc<T>
不同,RefCell<T>
类型代表了其持有数据的唯一所有权
RefCell 和 Box 的区别
Box | RefCell |
---|---|
编译阶段强制代码遵守借用规则 | 只会在运行时检查借用规则 |
否则出现错误 | 否则触发panic |
借用规则在不同阶段进行检查的比较
编译阶段 | 运行时 |
---|---|
尽早暴露问题 | 问题暴露延后,甚至到生产环境 |
没有任何运行时开销 | 因借用计数产生些许性能损失 |
对大多数场景时最佳选择 | 实现某些特定的内存安全场景(不可变环境中修改自身数据) |
是Rust的默认行为 |
RefCell
- 与
Rc<T>
相似,只能用于单线程场景
选择 Box、Rc、RefCell 的依据
Box | Rc | RefCell | |
---|---|---|---|
同一个数据的所有者 | 一个 | 多个 | 一个 |
可变性、借用检查 | 可变、不可变借用(编译时检查) | 不可变借用(编译时检查) | 可变、不可变借用(运行时检查) |
使用 RefCell 在运行时记录借用信息
- 两个方法(安全接口):
borrow()
方法- 返回智能指针 Ref,它实现了 Deref
borrow_mut()
方法:- 返回智能指针 RefMut,它实现了 Deref
- RefCell 会记录当前存在多少个活跃的 Ref 和 RefMut 智能指针:
- 每次调用 borrow :不可变借用计数加1
- 任何一个 Ref 的值离开作用域被释放时:不可变借用计数减1
- 每次调用 borrow_mut :可变借用计数加1
- 任何一个 RefMut 的值离开作用域被释放时:可变借用计数减1
- 以此技术来维护借用检查规则:
- 任何一个给定的时间里,只允许拥有多个不可变借用或一个可变借用
其他可实现内部可变性的类型
Cell<T>
:通过复制类访问数据Mutex<T>
:用于实现跨线程情形下的内部可变性模式
循环引用可导致内存泄露
Rust 可能发生内存泄漏
- Rust 的内存安全机制可以保证很难发生内存泄漏,但不是不可能
- 例如使用 Rc 和 RegCell 就可能创造出循环引用,从而发生内存泄漏:
- 每个项的引用数量不会变成0,值也不会被处理掉
防止内存泄漏的解决办法
- 依靠开发者来保证,不能依靠Rust
- 重新组织数据结构:一些引用来表达所有权,一些引用部表达所有权
- 循环引用中的一部分具有所有权关系,另一部分不涉及所有权关系
- 而只有所有权关系才影响值的清理
防止循环引用——把 Rc 换成 Weak
- Rc::clone 为 Ec 实例的 strong_count 加1,Rc 的实例只有在 strong_count 为0的时候才会被清理
- Rc 实例通过调用 Rc::downgrade 方法可以创建值的 Weak Reference (弱引用)
- 返回类型是 Weak (智能指针)
- 调用 Rc::downgrade 会为 weak_count 加1
- Rc 使用 weak_count 来追踪存在多少 Weak
- weak_count 不为0并不影响 Rc 实例的清理
Strong vs Weak
- Strong Reference (强引用)是关于如何分享 Rc 实例的所有权
- Weak Reference (弱引用)并不表达上述意思
- 使用 Weak Reference 并不会创建循环引用:
- 当 Strong Reference 数量为0的时候,Weak Reference 会自动断开
- 在使用 Weak 之前,需要保证它指向的值仍然存在:
- 在 Weak 实例上调用 upgrade 方法,返回 Option<Rc>
无畏并发
并发
- Concurrent:程序的不同部分之间独立的执行
- Parallel:程序的不同部分同时运行
- Rust无畏并发:允许你编写没有细微Bug的代码,并在不引入新Bug的情况下易于重构
使用线程同时运行代码
进程与线程
- 在大部分OS里,代码运行在进程(process)中,OS同时管理多个进程
- 在你的程序里,各独立部分可以同时运行,运行这些独立部分的就是线程(thread)
- 多线程运行:
- 提升性能表现
- 增加复杂性:无法保障各线程的执行顺序
多线程可导致的问题
- 竞争状态:线程以不一致的顺序访问数据或资源
- 死锁:两个线程彼此等待对方使用完所持有的资源,线程无法继续
- 只在某些情况下才会发生的Bug,很难可靠地复制形象和修复
实现线程的方式
- 通过调用OS的API来创建线程:1:1模型
- 需要较小的运行时
- 语言自己实现的线程(绿色线程):M:N模型
- 需要更大的运行时
- Rust:需要权衡运行时的支持
- Rust标准库仅提供 1:1模型的线程
通过 spawn 创建新线程
- 通过
thread::spawn
函数可以创建新线程:- 参数:一个闭包(在新线程里运行的代码)
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} form the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} form the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
thread::sleep
会导致当前线程暂停执行
通过 join Handle 来等待所有线程的完成
thread::spawn
函数的返回值类型时 JoinHandle- JoinHandle持有值的所有权
- 调用其
join
方法,可以等待对应的其他线程的完成
- 调用其
join
方法:调用 handle 的 join 方法会阻止当前运行线程的执行,直到 handle 所表示的这些线程终结
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} form the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} form the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
使用 move 闭包
- move 闭包通常和
thread::spawn
函数一起使用,它允许你使用其他线程的数据 - 创建线程时,把值的所有权从一个线程转移到另一个线程
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here is a vector: {:?}", v);
});
handle.join().unwrap();
}
使用消息传递来跨线程传递数据
消息传递
- 一种流行且能保证安全并发的技术就是:消息传递
- 线程(或 Actor)通过彼此发送消息(数据)来进行通信
- Go语言的名言:不要用共享内存来通信,要用通信来共享内存
- Rust:
Channel
(标准库提供)
Channel
- Channel 包含:发送端、接收端
- 调用发送端的方法:发送数据
- 接收端会检查和接收到达的数据
- 如果发送端、接收端中任意一端被丢弃了,那么 Channel 就”关闭“了
创建 Channel
- 使用
mpsc::channel
函数来创建 Channel- mpsc 表示 multiple producer, single consumer(多个生产者,一个消费者)
- 返回一个 tuple (元组):里面元素分别是发送端、接收端
fn main() {
let (tx, rx) = mspc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
发送端的 send 方法
- 参数:想要发送的数据
- 返回:Result<T, E>
- 如果有问题(例如接收端已被丢弃),就返回一个错误
接收端的方法
recv()
方法:阻止当前线程执行,直到 Channel 中有值被送来- 一旦有值收到,就返回 Result<T, E>
- 当发送端关闭,就会收到一个错误
try_recv()
方法:不会阻塞- 立即返回 Result<T, E>
- 有数据到达:返回 Ok,里面包含着数据
- 否则,返回错误
- 通常会使用循环调用来检查 try_recv 的结果
- 立即返回 Result<T, E>
Channel 和所有权转移
- 所有权在消息传递中非常重要,能帮你板鞋安全、并发的代码
发送多个值,看到接收者在等待
fn main() {
let (tx, rx) = mspc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::fron_millis(200));
}
});
for received in rx {
println!("Got: {}", received);
}
}
通过克隆创建多个发送者
fn main() {
let (tx, rx) = mspc::channel();
let tx1 = mspc::Sender::clone(&tx);
thread::spawn(move || {
let vals = vec![
String::from("1: hi"),
String::from("1: from"),
String::from("1: the"),
String::from("1: thread"),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::fron_millis(200));
}
});
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::fron_millis(200));
}
});
for received in rx {
println!("Got: {}", received);
}
}
共享状态的并发
使用共享来实现并发
- Rust支持通过共享状态来实现并发
- Channel 类似单所有权:一旦将值的所有权转移至 Channel,就无法使用它了
- 共享内存并发类似多所有权:多个线程可以同时访问同一块内存
使用 Mutex 来每次只允许一个线程来访问数据
- Mutex 是 mutual exclusion(互斥锁)的简写
- 在同一时刻,Mutex 只允许一个线程来访问某些数据
- 想要访问数据:
- 线程必须首先获取互斥锁(lock)
- lock 数据结构是 mutex 的一部分,它能跟踪谁对数据拥有独占有访问权
- mutex 通常被描述为:通过锁定系统来保护它所持有的数据
- 线程必须首先获取互斥锁(lock)
Mutex 的两条规则
- 在使用数据之前,必须尝试获取锁(lock)
- 使用完 mutex 所保护的数据,必须对数据进行解锁,以便其他线程可以获取锁
Mutex 的API
- 通过
Mutex::new(数据)
来创建 Mutex- Mutex 是一个智能指针
- 访问数据前,通过
lock()
方法来获取锁- 会阻塞当前线程
- lock 可能会失败
- 返回的是 MutexGuard(智能指针,实现了 Deref 和 Drop)
使用 Arc 来进行原子引用计数
- Arc 和 Rc 类似,它可以用于并发情景
- A:atomic,原子的
- 为什么所有的基础类型都不是原子的,为什么标准库类型不默认使用 Arc?
- 需要性能作为代价
- Arc 和 Rc 的 API 是相同的
RefCell/Rc vs Mutex/Arc
- Mutex 提供了内部的可变性,和Cell家族一样
- 我们使用 RefCell 来改变 Rc 里面的内容
- 我们使用 Mutex 来改变 Arc 里面的内容
- 注意:使用 Mutex 有死锁风险
通过 Send 和 Sunc Trait 来扩展并发
Send 和 Sync trait
- Rust语言的并发特性较少,目前讲的并发特性都来自标准库(而不是语言本身)
- 无需局限于标准库的并发,可以自己实现并发
- 但在Rust语言中有两个并发概念:
std::marker::Sync
和std::marker::Send
这两个trait
Send:允许线程间转移所有权
- 实现 Send trait 的类型可在线程间转移所有权
- Rust中几乎所有类型都实现了Send
- 但 Rc 没有实现 Send,它只用于单线程情景
- 任何完全由Send类型组成的类型也被标记为Send
- 除了原始指针之外,几乎所有的基础类型都是Send
Sync:允许从多线程访问
- 实现Sync的类型可以安全的被多个线程引用
- 也就是说:如果 T 是 Sync,那么 &T 就是 Send
- 引用可以被安全的送往另一个线程
- 基础类型都是Sync
- 完全由Sync类型组成的类型也是Sync
- 但:Rc 不是 Sync
- RefCell 和 Cell 家族也不是 Sync 的
- 而 Mutex是Sync的
手动实现Send和Sync是不安全的
- 记住上面这句哈即可
Rust的面向对象编程特性
面向对象语言的特性
Rust是面向对象编程语言吗?
- Rust受到多种编程范式的影响,包括面向对象
- 面向对象通常包含以下特性:命名对象、封装、继承
对象包含数据和行为
- 面向对象的程序由对象组成
- 对象包装了数据和操作这些数据的过程,这些过程通常被称作方法或操作
- 基于此定义:Rust是面向对象的
- struct、enum 包含数据
- impl块为之提供了方法
- 但带有方法的struct、enum并没有被称为对象
封装
- 封装:调用对象外部的代码无法直接访问对象内部的实现细节,唯一可以与对象进行交互的方法就是通过它公开的API
- Rust:
pub
关键字
继承
- 继承:使对象可以沿用另外一个对象的数据和行为,且无需重新定义相关代码
- Rust:没有继承
- 使用继承的原因:
- 代码复用
- Rust:默认 trait 方法来进行代码共享
- 多态
- Rust:泛型和 trait 约束(限定参数化多态 bounded paramertric)
- 代码复用
使用trait对象来存储不同类型的值
为共有行为定义一个trait
- Rust避免将struct或enum称为对象,因为它们与impl块是分开的
- trait对象有些类似于其他语言中的对象:
- 它们某种程度上组合了数据与行为
- trait对象与传统对象不同的地方:
- 无法为trait对象添加数据
- trait对象被专门用于抽象某些共有行为,它没其他语言中的对象那么通用
trait对象执行的是动态派发
- 将trait约束作用于泛型,Rust编译器会执行单态化:
- 编译器会为我们用来替换泛型类型参数的每一个具体类型生成对应函数和方法的非泛型实现
- 通过单态化生成的代码会执行静态派发(static dispatch),在编译过程中确定调用的具体方法
- 动态派发(dynamic dispatch):
- 无法在编译过程中确定你调用的究竟是哪一种方法
- 编译器会产生额外的代码以便在运行时找出希望调用的方法
- 使用trait对象,会执行动态派发:
- 产生运行时开销
- 阻止编译器内联方法代码,使得部分优化操作无法进行
trait对象必须保证对象安全
- 只能把满足对象安全(object-safe)的trait转化为trait对象
- Rust采用一系列规则来判断某个对象是否安全,只需记住两条:
- 方法的返回类型不是Self
- 方法中不包含任何泛型类型参数
实现面向对象的设计模式
状态模式
- 状态模式(state pattern)是一种面向对象设计模式
- 一个值拥有的内部状态由数个状态对象(state object)表达而成,而值的行为则随着内部状态的改变而改变
- 使用状态模式意味着:
- 业务需求变化时,不需要持有状态的值的代码,或者使用这个值的代码
- 只需要更新状态对象内部的代码,以便改变其规则,或者增加一些新的状态对象
状态模式的取舍权衡
- 缺点:
- 某些状态之间是相互耦合的
- 需要重复实现一些逻辑代码
将状态和行为编码为类型
- 将状态编码为不同的类型:
- Rust类型检查系统会通过编译时错误来组织用户使用无效的状态
总结
- Rust不仅能实现面向对象的设计模式,还可以支持更多的模式
- 例如:将状态和行为编码为类型
- 面向对象的经典模式并不总是Rust编程实践中的最佳选择,因为Rust具有所有权等其他面向对象语言没有的特性!
模式匹配
模式
- 模式是Rust中的一种特殊语法,用于匹配复杂和简单类型的结构
- 将模式与匹配表达式和其他构造结合使用,可以更好地控制程序的控制流
- 模式由以下元素(的一些组合)组成:
- 字面值
- 解构的数组、enum、struct 和 tuple
- 变量
- 通配符
- 占位符
- 想要使用模式,需要将其与某个值进行比较:
- 如果模式匹配,就可以在代码中使用这个值的相应部分
用到模式的地方
match 的 Arm
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
- match表达式的要求:
- 详尽(包含所有的可能性)
- 一个特殊的模式:_ (下划线):
- 它会匹配任何东西
- 不会绑定到变量
- 通常用于match的最后一个arm:或用于忽略某些值
条件 if let 表达式
- if let 表达式主要是作为一种简短的方式来等价的代替只有一个匹配项的match
- if let 可选的还可以拥有 else,包括:
- else if
- else if let
- 但,if let 不会检查穷尽性
while let 条件循环
- 只要模式继续满足匹配的条件,那它允许while循环一直运行
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("{}", top);
}
for循环
- for循环是Rust中最常见的循环
- for循环中,模式就是紧随for关键字后的值
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{} is at index ().", value, index);
}
let 语句
- let 语句也是模式
let PATTERN = ESPRESSION;
let a = 5;
let (x, y, z) = (1, 2, 3);
函数参数
- 函数参数也可以是模式
fn foo(x: i32) {
}
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({}, {})", x, y);
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
可辩驳性:模式是否会无法匹配
模式的两种形式
- 模式有两种形式:可辨驳的、无可辩驳的
- 能匹配任何可能传递的值的模式:无可辩驳的
- 例如:
let x = 5;
- 例如:
- 对某些可能的值,无法进行匹配的模式:可辩驳的
- 例如:
if let Some(x) = a_value
- 例如:
- 函数参数、let 语句、for 循环只接受无可辩驳的模式
- it let 和 while let 接受可辨驳和无可辩驳的模式
模式语法
匹配字面值
- 模式可以直接匹配字面值
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anthing"),
}
匹配命名变量
- 命令的变量是可匹配任何值的无可辩驳模式
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50."),
Some(y) => println!("Matched, y = {:?}", y),
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {:?}", x, y);
多重模式
- 在match表达式中,使用
|
语法(就是或的意思),可以匹配多种模式
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anthing"),
}
使用 …= 来匹配某个范围的值
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("sonething else"),
}
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("sonething else"),
}
解构以分解值
- 可以使用模式类解构struct、enum、tuple,从而引用这些类型值的不同部分
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 }
let Point { x: a, y: b} = p;
assert_eq!(0, a);
assert_eq!(7, b);
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
match p {
Point { x, y: 0 } => println!("On the x axis at {}", x),
Point { x: 0, y } => println!("On the y axis at {}", y),
Point { x, y } => println!("On neither axis: ({}, {})", x, y),
}
}
解构 enum
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => println!("The Quit variant has no data to destructure."),
Message::Move { x, y } => println!("Move in the x direction {} and in the y direction {}", x, y),
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(r, g, b) => println!("Change the color to red {}, green {}, blue {}", r, g, b),
}
}
解构嵌套的 struct 和 enum
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change the color to red {}, green {}, blue {}", r, g, b);
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change the color to hue {}, saturation {}, and value {}", h, s, v);
}
_ => (),
}
}
解构 struct 和 tuple
struct Point {
x: i32,
y: i32,
}
fn main() {
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
在模式中忽略值
- 有几种方式可以在模式中忽略整个值或部分值:
_
_
配合其他模式- 使用以
_
开头的名称 ..
(忽略值的剩余部分)
使用 _ 忽略整个值
fn foo(_: i32, y: i32) {
println!("This is only uses the y parameter: {}", y);
}
fn main() {
foo(3, 4);
}
使用嵌套的 _ 来忽略值的一部分
fn main() {
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
(Some(_), Some(_)) => {
println!("Can't overwrite an existing customized value.");
}
_ => {
setting_value = new_setting_value;
}
}
println!("setting is {:?}", setting_value);
}
通过使用以 _ 开头命名来忽略未使用的变量
fn main() {
let _x = 5;
let y = 10;
}
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{:?}", s);
}
使用 … 来忽略值的剩余部分
struct Point {
x: i32,
y: i32,
z: i32,
}
fn main() {
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { s, .. } => println!("s is {}", x);
}
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {}, {}", first, last);
}
}
}
使用 match 守卫来提供额外的条件
- match守卫就是match arm模式后额外的if条件,想要匹配该条件也必须满足
- match守卫适用于比单独的模式更复杂的场景
fn main() {
let num = Some(4);
match num {
Some(x) if x < 5 => println!("less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}
}
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50."),
Some(n) if n == y => println!("Matched, n = {:?}", n),
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {:?}", x, y);
}
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
@绑定
@
符号让我们可以创建一个变量,该变量可以测试某个值是否与模式匹配的同时保存该值
enum Message {
Hello { id: i32 },
}
fn main() {
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello {
id: id_variable @ 3..=7,
} => {
println!("Found an id in range: {}", id_variable)
}
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => {
println!("Found some other id: {}", id)
}
}
}
高级特性
不安全Rust
匹配命名变量
- 隐藏者第二个语言,它没有强制内存安全保证:Unsafe Rust(不安全的Rust)
- 和普通的Rust一样,但提供了额外的“超能力”
- Unsafe Rust存在的原因:
- 静态分析是保守的
- 使用Unsafe Rust:我知道自己在做什么,并承担相应风险
- 计算机硬件本身是不安全的,Rust需要能够进行底层系统编程
- 静态分析是保守的
Unsafe 超能力
- 使用
unsafe
关键字来切换到unsafe rust,开启一个块,里面放着unsafe代码 - Unsafe Rust里可执行的四个动作(unsafe超能力):
- 解引用原始指针
- 调用unsafe函数或方法
- 访问或修改可变的静态变量
- 实现unsafe trait
- 注意:
unsafe
并没有关闭借用检查或停用其他安全检查- 任何内存相关的错误必须留在unsafe块里
- 尽可能隔离unsafe代码,最好将其封装在安全的抽象里,提供安全的API
解引用原始指针
- 原始指针
- 可变的:
*mut T
- 不可变的:
*const T
。意味着指针在解引用后不能直接对其进行赋值 - 注意:这里的
*
不是解引用符号,它是类型名的一部分
- 可变的:
- 与引用不同,原始指针:
- 允许通过同时具有不可变和可变指针或多个指向同一位置的可变指针来忽略借用规则
- 无法保证能指向合理的内存
- 允许为null
- 不实现任何自动清理
- 放弃保证的安全,换取更好的性能/与其他语言或硬件接口的能力
- 为什么要用原始指针?
- 与C语言进行接口
- 构建借用检查器无法理解的安全抽象
调用 unsafe 函数或方法
- unsafe 函数或方法:在定义前加上了
unsafe
关键字- 调用前需手动满足一些条件(主要靠看文档),因为Rust无法对这些条件进行验证
- 需要在unsafe块里进行调用
创建 unsafe 代码的安全抽象
- 函数包含unsafe代码并不意味着需要将整个函数标记为unsafe
- 将unsafe代码包裹在安全函数中是一个常见的抽象
使用 extern 函数调用外部代码
extern
关键字:简化创建和使用外部函数接口(FFI)的过程- 外部函数接口(FFI,Foreign Function Interface):它允许一种编程语言定义函数,并让其他编程语言能调用这些函数
- 应用二进制接口(ABI,Application Binary Interface):定义函数在汇编层的调用方式
- “C”ABI是最常见的ABI,它遵循C语言的ABI
从其他语言调用Rust函数
- 可以使用
extern
创建接口,其他语言通过它们可以调用Rust的函数 - 在 fn 前加
extern
关键字,并指定ABI - 还需添加
#[no_mangle]
注解:避免Rust在编译时改变它的名称
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function form C!");
}
访问或修改一个可变静态变量
- Rust支持全局变量,但因为所有权机制可能产生某些问题,例如数据竞争
- 在Rust里,全局变量叫做静态(static)变量
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("name is: {}", HELLO_WORLD);
}
静态变量
- 静态变量与常量类似
- 命名:SCAREAMING_SNAKE_CASE
- 必须标注类型
- 静态变量只能存储
'static
生命周期的引用,无需显式标注
常量和不可变静态变量的区别
- 静态变量:有固定的内存地址,使用它的值总会访问同样的数据
- 常量:允许使用它们的时候对数据进行复制
- 静态变量:可以是可变的,访问和修改静态可变变量是不安全(Unsafe)的
实现不安全(unsafe)trait
- 当某个trait中至少存在一个方法拥有编译器无法校验的不安全因素时,就成这个trait时不安全的
- 声明 unsafe trait:在定义前加
unsafe
关键字- 该trait只能在unsafe代码块中实现
何时使用 unsafe 代码
- 编译器无法保证内存安全,保证unsafe代码正确并不简单
- 有充足理由使用unsafe代码时,就可以这样做
- 通过显式标记unsafe,可以在出问题时轻松定位
高级trait
在trait定义中使用关联类型来指定占位类型
- 关联类型(associated type)是trait中的类型占位符,它可以用于trait的方法签名中:
- 可以定义出包含某些类型的trait,而在实现前无需知道这些类型是什么
关联类型和泛型的区别
泛型 | 关联类型 |
---|---|
每次实现trait时需要标注类型 | 无需标注类型 |
可以为一个类型多次实现某个trait(不同的泛型参数) | 无法为单个类型多次实现某个trait |
默认泛型参数和运算符重载
- 可以在使用泛型参数时为泛型指定一个默认的具体类型
- 语法:
<PlaceholderType=ConcreteType>
- 这种技术常用于运算符重载(operator overloading)
- Rust不允许创建自己的运算符及重载任意的运算符
- 但可以通过实现
std::ops
中列出的那些trait来重载一部分相应的运算符
默认泛型参数的主要应用场景
- 扩展一个类型而部破环现有代码
- 允许在大部分用户都不需要的特定场景下进行自定义
完全限定语法(Fully Qualified Syntax)如何调用同名方法
- 完全限定语法:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
- 可以在任何调用函数或方法的地方使用
- 允许忽略那些从其他上下文能推到出来的部分
- 当Rust无法区分你期望调用哪个方法的时候,才需使用这种方法
使用 supertrait 来要求 trait 附带其他 trait 的功能
- 需要在一个trait中使用其他trait的功能:
- 需要被依赖的trait也被实现
- 哪个被间接依赖的trait就是当前trait的supertrait
使用 newtype 模式在外部类型上实现外部trait
- 孤儿规则:只有当trait或类型定义在本地包时,才能为该类型实现这个trait
- 可以通过 newtype模式来绕过这一规则
- 利用 tuple struct(元组结构体)创建一个新的类型
高级类型
使用 newtype 模式实现类型安全和抽象
- newtype模式可以:
- 用来静态的保证各种值之间不会混淆并表明值的单位
- 为类型的某些细节提供抽象能力
- 通过轻量级的封装来隐藏内部实现细节
使用类型别名创建类型同义词
- Rust提供了类型别名的功能:
- 为现有类型产生另外的名称(同义词)
- 并不是一个独立的类型
- 使用
type
关键字
- 主要用途:减少代码字符重复
Never 类型
- 有一个名为
!
的特殊类型:- 它没有任何值,行话称为空类型(empty type)
- 我们倾向于叫它 never 类型,因为它在不返回的函数当中充当返回类型
- 不返回值的函数被称作发散函数(diverging function)
动态大小和sized trait
- Rust需要在编译时确定为一个特定类型的值分配多少空间
- 动态大小的类型(Dynamically Sized Types, DST)的概念:
- 编写代码时使用只有在运行时才能确定大小的值
- str 是动态大小的类型(注意不是 &str):只有运行时才能确定字符串的长度
- 下列代码无法正常工作:
let s1:str = "Hello there!";
let s2:str = "how it going?";
- 使用 &str 来解决:
- str 的地址
- str 的长度
- 下列代码无法正常工作:
Rust 使用动态大小类型的通用方式
- 附带一些额外的元数据类存储动态信息的大小
- 使用动态大小类型时总会把它的值放在某种指针后边
另外一种动态大小类型:trait
- 每个trait都是一个动态大小的类型,可以通过名称对其进行引用
- 为了将trait用作trait对象,必须将它放置在某种指针之后
- 例如:
&dyn Trait
或Box<dyn Trait>(Rc<dyn Trait>)
之后
- 例如:
Sized Trait
- 为了处理动态大小的类型,Rust提供了一个Sized Trait来确定一个类型的大小在编译时是否已知
- 编译时可以计算出大小的类型会自动实现这一trait
- Rust还会为每一个泛型函数隐式的添加Sized约束
- 默认情况下,泛型函数只能被用于编译时已知大小的类型,可以通过特殊语法解除这一限制
?Sized trait 约束
fn generic<T: ?Sized>(t: &T) {}
- T可能是也可能不是Sized
- 这个语法只能用在Sized上面,不能被用于其他trait
高级函数和闭包
函数指针
- 可以将函数传递给其他函数
- 函数在传递过程中会被强制转换为 fn 类型
- fn 类型就是“函数指针(function pointer)”
函数指针与闭包的不同
- fn 是一个类型,不是一个trait
- 可以直接指定 fn 为参数类型,不用声明一个以 fn trait为约束的泛型参数
- 函数指针实现了全部3中闭包trait(Fn、FnMut、FnOnce)
- 总是可以把函数指针用作参数传递给一个接收闭包的函数
- 所以,倾向于搭配闭包trait的泛型来编写函数:可以同时接收闭包和普通函数
- 某些情景,只想接受 fn 而不接收闭包:
- 与外部不支持闭包的代码交互:C函数
返回闭包
- 闭包使用trait进行表达,无法在函数中直接返回一个闭包,可以将一个实现了该trait的具体类型做为返回值
宏
宏 macro
- 宏在Rust里指的是一组相关特性的集合称谓:
- 使用 macro_rules! 构建是声明宏(declarative macro)
- 3种过程宏
- 自定义
#[derive]
宏,用于struct或enum,可以为其指定随 derive 属性添加的代码 - 类似属性的宏,可以在任何条目上添加自定义属性
- 类似函数的宏,看起来像函数调用,对其指定为参数的 token 进行操作
- 自定义
函数和宏的差别
- 本质上,宏是用来编写可以生成其他代码的代码(元编程,metaprogramming)
- 函数在定义签名时,必须声明参数个数和类型,宏可以处理可变的参数
- 编译器会在解释代码前展开宏
- 宏的定义要比函数复杂的多,难以阅读、理解、维护
- 在某个文件调用宏时,必须提前定义宏或将宏引入当前作用域
- 函数可以在任何位置定义并在任何位置使用
macro_rules! 声明宏(弃用)
- Rust种最常见的宏形式:声明宏
- 类似match的模式匹配
- 需要使用
macro_rules!
基于属性来生成代码的过程宏
- 这种形式更像函数(某种形式的过程)一些
- 接收并操作输入的Rust代码
- 生成另外一些Rust代码作为结果
- 3种过程宏:
- 自定义派生
- 属性宏
- 函数宏
- 创建过程宏时:
- 宏定义必须单独放在它们自己的包中,并使用特殊的包类型
类似属性的宏
- 属性宏与自定义 derive 宏类似
- 允许创建新的属性
- 但不是为 derive 属性生成代码
- 属性宏更加灵活:
- derive 只能用于struct和enum
- 属性宏可以用于任意的条目
类似函数的宏
- 函数宏定义类似函数调用的宏,但比普通函数更加灵活
- 函数宏可以接收TokenStream作为参数
- 与另外两个过程宏一样,在定义中使用Rust代码来操作TokenStream