赞
踩
Rust官网:https://www.rust-lang.org/zh-CN/
Rust模块 库:https://crates.io/
官方文档:https://rustwiki.org/zh-CN/std/all.html
Rust Cookbook:https://rust-lang-nursery.github.io/rust-cookbook/
Rust 爬虫:https://zhuanlan.zhihu.com/p/516033159
[Rust] Scraper 爬虫简单使用:https://zhuanlan.zhihu.com/p/595712847
所有内容全部来自 《Rust 程序设计语言》和 《通过例子学 Rust》!!!!!
Rust 语言的主要目标之一是解决传统 系统级编程语言(如 C 和 C++)中常见的安全性问题,例如空指针引用、数据竞争等。为了实现这个目标,Rust 引入了一种称为 "所有权" 的概念,通过静态检查来确保内存安全和线程安全。此外,Rust 还具有其他一些特性,如模式匹配、代数数据类型、函数式编程风格的特性(如闭包和高阶函数)等。它还提供了丰富的标准库和包管理器 Cargo,使得开发者可以轻松构建和管理他们的项目。
Rust 是一门注重安全(safety)、速度(speed)和并发(concurrency)的现代系统编程语言。Rust 通过内存安全来实现以上目标,但不使用垃圾回收机制(garbage collection, GC)。
Rust 是 静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。
rustup update
获取最新版本的 Rust。文档:https://rust-lang.github.io/rustup/下载:https://www.rust-lang.org/tools/install
安装:https://www.rust-lang.org/zh-CN/learn/get-started
默认情况,Rust 依赖 C++ build tools,没有安装也关系。安装过程需要保证网络正常。
在 Rust 开发环境中,所有工具都安装在 ~/.cargo/bin 目录中,可以在这里找到包括 rustc、cargo 和 rustup 在内的 Rust 工具链。在安装过程中,rustup 会尝试配置 PATH,如果 rustup 对 PATH 的修改不生效,可以手动添加路径到 PATH
~/.cargo/bin
~/.rustup/bin
以下是一些常用的命令:
rustup 相关
rustup -h # 查看帮助
rustup show # 显示当前安装的工具链信息
rustup update # 检查安装更新
rustup self uninstall # 卸载
rustup default stable-x86_64-pc-windows-gnu # 设置当前默认工具链rustup toolchain list # 查看工具链
rustup toolchain install stable-x86_64-pc-windows-gnu # 安装工具链
rustup toolchain uninstall stable-x86_64-pc-windows-gnu # 卸载工具链
rustup toolchain link <toolchain-name> "<toolchain-path>" # 设置自定义工具链rustup override list # 查看已设置的默认工具链
rustup override set <toolchain> --path <path> # 设置该目录以及其子目录的默认工具链
rustup override unset --path <path> # 取消目录以及其子目录的默认工具链rustup target list # 查看目标列表
rustup target add <target> # 安装目标
rustup target remove <target> # 卸载目标
rustup target add --toolchain <toolchain> <target> # 为特定工具链安装目标rustup component list # 查看可用组件
rustup component add <component> # 安装组件
rustup component remove <component> # 卸载组件
rustc 相关
rustc --version # 查看rustc版本
cargo 相关
cargo --version # 查看cargo版本
cargo new <project_name> # 新建项目
cargo build # 构建项目
cargo run # 运行项目
cargo check # 检查项目
cargo -h # 查看帮助
配置工具链安装位置
在系统环境变量中添加如下变量:
CARGO_HOME 指定 cargo 的安装目录
RUSTUP_HOME 指定 rustup 的安装目录
默认分别安装到用户目录下的.cargo 和.rustup 目录
配置国内镜像
配置 rustup 国内镜像。在系统环境变量中添加如下变量(选一个就可以,可以组合):
# 清华大学
RUSTUP_DIST_SERVER:https://mirrors.tuna.tsinghua.edu.cn/rustup
RUSTUP_UPDATE_ROOT:https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup# 中国科学技术大学
RUSTUP_DIST_SERVER:https://mirrors.ustc.edu.cn/rust-static
RUSTUP_UPDATE_ROOT:https://mirrors.ustc.edu.cn/rust-static/rustup
配置 cargo 国内镜像。在 cargo 安装目录下新建 config 文件(注意 config 没有任何后缀),文件内容如下:
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'tuna'# 清华大学
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"# 中国科学技术大学
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"
# 设置代理
[http]
proxy = "127.0.0.1:8889"
[https]
proxy = "127.0.0.1:8889"
Windows 交叉编译 Linux 程序。目标服务器是 Linux(CentOS 7) 64bit
, 所以我们添加的 target 应该是x86_64-unknown-linux-gnu
(动态依赖) 或者x86_64-unknown-linux-musl
(静态依赖)
1). 添加需要的 target
rustup target add x86_64-unknown-linux-musl
2). 在 cargo 安装目录下新建 config 文件(注意 config 没有任何后缀),添加的文件内容如下:
[target.x86_64-unknown-linux-musl]
linker = "rust-lld"
3). 构建
cargo build --target x86_64-unknown-linux-musl
用 Cargo 创建一个新项目。在终端中执行:cargo new hello-rust,会生成一个名为 hello-rust
的新目录,其中包含以下文件:
Cargo.toml
为 Rust 的清单文件。其中包含了项目的元数据和依赖库。src/main.rs
为编写应用代码的地方。进入新创建的目录中,执行命令运行此程序:cargo run
现在来为程序添加依赖。可以在 crates.io,即 Rust 包的仓库中找到所有类别的库。在 Rust 中通常把 "包" 称作 "crates"。在本项目中,使用了名为 ferris-says 的库。
在 Cargo.toml
文件中添加以下信息(从 crate 页面上获取):
[dependencies]
ferris-says = "0.3.1"
运行:cargo build , Cargo 就会安装该依赖。运行 build 会创建一个新文件
Cargo.lock
,该文件记录了本地所用依赖库的精确版本。
使用该依赖库:可以打开
main.rs
,删除其中所有的内容(它不过是个示例而已),然后在其中添加下面这行代码:use ferris_says::say;这样就可以使用
ferris-says
crate 中导出的say
函数了。
现在用上面的依赖库编写一个小应用。在 main.rs
中添加以下代码:
- use ferris_says::say; // from the previous step
- use std::io::{stdout, BufWriter};
-
- fn main() {
- let stdout = stdout();
- let message = String::from("Hello fellow Rustaceans!");
- let width = message.chars().count();
-
- let mut writer = BufWriter::new(stdout.lock());
- say(&message, width, &mut writer).unwrap();
- }
保存完毕后,执行命令运行程序:cargo run
成功执行后,会打印一个字符形式的螃蟹图案。
Ferris ( 费理斯 ) 是 Rust 社区的非官方吉祥物。
:https://www.rust-lang.org/zh-CN/learn
以下所有文档都可以用 rustup doc
命令在本地阅读,它会在浏览器中离线打开这些资源!
详尽的 Rust 标准库 API 手册。:https://doc.rust-lang.org/std/index.html
Rust 版本指南。:https://doc.rust-lang.org/edition-guide/index.html
Rust 的包管理器和构建系统。:https://doc.rust-lang.org/cargo/index.html
Cargo 有两个主要的配置:运行 cargo build
时采用的 dev
配置和运行 cargo build --release
的 release
配置。dev
配置为开发定义了良好的默认配置,release
配置则为发布构建定义了良好的默认配置。
可以在项目中使用 crates.io 上的包作为依赖,也可以发布自己的包来向他人分享代码。crates.io 用来分发包的源代码,所以它主要托管开源代码。
文档注释使用三斜杠 ///
使用 pub use 导出合适的公有 API
- //! # Art
- //!
- //! A library for modeling artistic concepts.
-
- pub mod kinds {
- /// The primary colors according to the RYB color model.
- pub enum PrimaryColor {
- Red,
- Yellow,
- Blue,
- }
-
- /// The secondary colors according to the RYB color model.
- pub enum SecondaryColor {
- Orange,
- Green,
- Purple,
- }
- }
-
- pub mod utils {
- use crate::kinds::*;
-
- /// Combines two primary colors in equal amounts to create
- /// a secondary color.
- pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
- // --snip--
- }
- }
学习如何为 crate 编写完美的文档。:https://doc.rust-lang.org/rustdoc/index.html
熟悉 Rust 编译器中可用的选项。:https://doc.rust-lang.org/rustc/index.html
深入解释遇到的编译错误。:https://doc.rust-lang.org/error_codes/error-index.html
用 Rust 构建高效的命令行应用。:https://rust-cli.github.io/book/index.html
通过 WebAssembly 用 Rust 构建浏览器原生的库。:https://rustwasm.github.io/docs/book/
Rust 编写嵌入式程序。:https://doc.rust-lang.org/stable/embedded-book/
- // 这是注释,单行注释...
- /* ...这是多行注释 */
-
- ///
- // 1. 基础 //
- ///
-
- // 函数 (Functions)
- // `i32` 是有符号 32 位整数类型(32-bit signed integers)
- fn add2(x: i32, y: i32) -> i32 {
- // 隐式返回 (不要分号)
- x + y
- }
-
- // 主函数(Main function)
- fn main() {
- // 数字 (Numbers) //
-
- // 不可变绑定
- let x: i32 = 1;
-
- // 整形/浮点型数 后缀
- let y: i32 = 13i32;
- let f: f64 = 1.3f64;
-
- // 类型推导
- // 大部分时间,Rust 编译器会推导变量类型,所以不必把类型显式写出来。
- // 这个教程里面很多地方都显式写了类型,但是只是为了示范。
- // 绝大部分时间可以交给类型推导。
- let implicit_x = 1;
- let implicit_f = 1.3;
-
- // 算术运算
- let sum = x + y + 13;
-
- // 可变变量
- let mut mutable = 1;
- mutable = 4;
- mutable += 2;
-
- // 字符串 (Strings) //
-
- // 字符串字面量
- let x: &str = "hello world!";
-
- // 输出
- println!("{} {}", f, x); // 1.3 hello world
-
- // 一个 `String` – 在堆上分配空间的字符串
- let s: String = "hello world".to_string();
-
- // 字符串分片(slice) - 另一个字符串的不可变视图
- // 基本上就是指向一个字符串的不可变指针,它不包含字符串里任何内容,只是一个指向某个东西的指针
- // 比如这里就是 `s`
- let s_slice: &str = &s;
-
- println!("{} {}", s, s_slice); // hello world hello world
-
- // 数组 (Vectors/arrays) //
-
- // 长度固定的数组 (array)
- let four_ints: [i32; 4] = [1, 2, 3, 4];
-
- // 变长数组 (vector)
- let mut vector: Vec<i32> = vec![1, 2, 3, 4];
- vector.push(5);
-
- // 分片 - 某个数组(vector/array)的不可变视图
- // 和字符串分片基本一样,只不过是针对数组的
- let slice: &[i32] = &vector;
-
- // 使用 `{:?}` 按调试样式输出
- println!("{:?} {:?}", vector, slice); // [1, 2, 3, 4, 5] [1, 2, 3, 4, 5]
-
- // 元组 (Tuples) //
-
- // 元组是固定大小的一组值,可以是不同类型
- let x: (i32, &str, f64) = (1, "hello", 3.4);
-
- // 解构 `let`
- let (a, b, c) = x;
- println!("{} {} {}", a, b, c); // 1 hello 3.4
-
- // 索引
- println!("{}", x.1); // hello
-
- //
- // 2. 类型 (Type) //
- //
-
- // 结构体(Sturct)
- struct Point {
- x: i32,
- y: i32,
- }
-
- let origin: Point = Point { x: 0, y: 0 };
-
- // 匿名成员结构体,又叫“元组结构体”(‘tuple struct’)
- struct Point2(i32, i32);
-
- let origin2 = Point2(0, 0);
-
- // 基础的 C 风格枚举类型(enum)
- enum Direction {
- Left,
- Right,
- Up,
- Down,
- }
-
- let up = Direction::Up;
-
- // 有成员的枚举类型
- enum OptionalI32 {
- AnI32(i32),
- Nothing,
- }
-
- let two: OptionalI32 = OptionalI32::AnI32(2);
- let nothing = OptionalI32::Nothing;
-
- // 泛型 (Generics) //
-
- struct Foo<T> { bar: T }
-
- // 这个在标准库里面有实现,叫 `Option`
- enum Optional<T> {
- SomeVal(T),
- NoVal,
- }
-
- // 方法 (Methods) //
-
- impl<T> Foo<T> {
- // 方法需要一个显式的 `self` 参数
- fn get_bar(self) -> T {
- self.bar
- }
- }
-
- let a_foo = Foo { bar: 1 };
- println!("{}", a_foo.get_bar()); // 1
-
- // 接口(Traits) (其他语言里叫 interfaces 或 typeclasses) //
-
- trait Frobnicate<T> {
- fn frobnicate(self) -> Option<T>;
- }
-
- impl<T> Frobnicate<T> for Foo<T> {
- fn frobnicate(self) -> Option<T> {
- Some(self.bar)
- }
- }
-
- let another_foo = Foo { bar: 1 };
- println!("{:?}", another_foo.frobnicate()); // Some(1)
-
- ///
- // 3. 模式匹配 (Pattern matching) //
- ///
-
- let foo = OptionalI32::AnI32(1);
- match foo {
- OptionalI32::AnI32(n) => println!("it’s an i32: {}", n),
- OptionalI32::Nothing => println!("it’s nothing!"),
- }
-
- // 高级模式匹配
- struct FooBar { x: i32, y: OptionalI32 }
- let bar = FooBar { x: 15, y: OptionalI32::AnI32(32) };
-
- match bar {
- FooBar { x: 0, y: OptionalI32::AnI32(0) } =>
- println!("The numbers are zero!"),
- FooBar { x: n, y: OptionalI32::AnI32(m) } if n == m =>
- println!("The numbers are the same"),
- FooBar { x: n, y: OptionalI32::AnI32(m) } =>
- println!("Different numbers: {} {}", n, m),
- FooBar { x: _, y: OptionalI32::Nothing } =>
- println!("The second number is Nothing!"),
- }
-
- ///
- // 4. 流程控制 (Control flow) //
- ///
-
- // `for` 循环
- let array = [1, 2, 3];
- for i in array {
- println!("{}", i);
- }
-
- // 区间 (Ranges)
- for i in 0u32..10 {
- print!("{} ", i);
- }
- println!("");
- // 输出 `0 1 2 3 4 5 6 7 8 9 `
-
- // `if`
- if 1 == 1 {
- println!("Maths is working!");
- } else {
- println!("Oh no...");
- }
-
- // `if` 可以当表达式
- let value = if true {
- "good"
- } else {
- "bad"
- };
-
- // `while` 循环
- while 1 == 1 {
- println!("The universe is operating normally.");
- }
-
- // 无限循环
- loop {
- println!("Hello!");
- }
-
-
- // 5. 内存安全和指针 (Memory safety & pointers) //
-
-
- // 独占指针 (Owned pointer) - 同一时刻只能有一个对象能“拥有”这个指针
- // 意味着 `Box` 离开他的作用域后,会被安全地释放
- let mut mine: Box<i32> = Box::new(3);
- *mine = 5; // 解引用
- // `now_its_mine` 获取了 `mine` 的所有权。换句话说,`mine` 移动 (move) 了
- let mut now_its_mine = mine;
- *now_its_mine += 2;
-
- println!("{}", now_its_mine); // 7
- // println!("{}", mine); // 编译报错,因为现在 `now_its_mine` 独占那个指针
-
- // 引用 (Reference) – 引用其他数据的不可变指针
- // 当引用指向某个值,我们称为“借用”这个值,因为是被不可变的借用,所以不能被修改,也不能移动
- // 借用一直持续到生命周期结束,即离开作用域
- let mut var = 4;
- var = 3;
- let ref_var: &i32 = &var;
-
- println!("{}", var); //不像 `mine`, `var` 还可以继续使用
- println!("{}", *ref_var);
- // var = 5; // 编译报错,因为 `var` 被借用了
- // *ref_var = 6; // 编译报错,因为 `ref_var` 是不可变引用
-
- // 可变引用 (Mutable reference)
- // 当一个变量被可变地借用时,也不可使用
- let mut var2 = 4;
- let ref_var2: &mut i32 = &mut var2;
- *ref_var2 += 2;
-
- println!("{}", *ref_var2); // 6
- // var2 = 2; // 编译报错,因为 `var2` 被借用了
- }
格式化输出:https://rustwiki.org/zh-CN/rust-by-example/hello/print.html
在 Rust 中,打印的占位符由格式化宏提供,最常用的是 println!
和 format!
。下面是一些常见的占位符及其用法:
这只是一小部分常见的占位符用法,你还可以根据需要使用其他格式化选项。Rust 的格式化宏提供了非常灵活和强大的格式化功能,可以满足大多数打印需求。
- fn main() {
- let name = "Alice";
- let age = 25;
- let height = 1.65;
-
- println!("Name: {}", name);
- println!("Age: {}", age);
- println!("Height: {:.2}", height); // 格式化为小数点后两位
-
- let point = (3, 5);
- println!("Point: {:?}", point);
- }
打印 "枚举、结构体"
- #[derive(Debug)]
- enum MyEnum {
- Variant1,
- Variant2(u32),
- Variant3 { name: String, age: u32 },
- }
-
- #[derive(Debug)]
- struct MyStruct{
- field_1: String,
- field_2: usize,
- }
-
- impl MyStruct {
- fn init_field(&self){
- let name = &self.field_1;
- let age = self.field_2;
- println!("{name} ---> {age}")
- }
- }
-
- fn main() {
- let my_enum = MyEnum::Variant2(42);
- println!("{:?}", my_enum);
- let my_struct = MyStruct{
- field_1: String::from("king"),
- field_2: 100
- };
- my_struct.init_field();
- println!("{:?}", my_struct);
- }
英文文档:https://doc.rust-lang.org/book/
中文文档:https://kaisery.github.io/trpl-zh-cn/
《Rust 程序设计语言》被亲切地称为“圣经”。给出了 Rust 语言的概览。在阅读的过程中构建几个项目,读完后,就能扎实地掌握 Rust 语言。
let
定义变量let
定义变量。使用注解来描述变量的类型。当编译器可以自动推导出变量的类型时,注解可以省略。mut
修饰语后变量就可以改变。{}
包围的语句集合。另外也允许变量遮蔽(variable shadowing)。- fn main() {
- let mut _mutable_integer = 7i32;
-
- {
- // 被不可变的 `_mutable_integer` 遮蔽
- let _mutable_integer = _mutable_integer;
-
- // 报错!`_mutable_integer` 在本作用域被冻结
- _mutable_integer = 50;
- // 改正 ^ 注释掉上面这行
-
- // `_mutable_integer` 离开作用域
- }
-
- // 正常运行! `_mutable_integer` 在这个作用域没有冻结
- _mutable_integer = 3;
- }
示例
- fn main() {
- // let 变量赋值
- // let PATTERN = EXPRESSION;
- let x = 5;
- let (a, b, c) = (1, 2, 3);
- let (x1, y2, _) = (1, 2, 3);
-
- }
as
关键字进行显式类型转换(casting)。type
语句给已有的类型取个新的名字。类型的名字必须遵循驼峰命名法(像是 CamelCase
这样),否则编译器将给出警告。原生类型是例外,比如: usize
、f32
,等等。内置类型列表:
true
或 false
char
) 和 字符串切片(str
)!
— 没有值的类型- // 不显示类型转换产生的溢出警告。
- #![allow(overflowing_literals)]
-
- fn main() {
- let decimal = 65.4321_f32;
-
- // 错误!不提供隐式转换
- let integer: u8 = decimal;
- // 改正 ^ 注释掉这一行
-
- // 可以显式转换
- let integer = decimal as u8;
- let character = integer as char;
-
- println!("Casting: {} -> {} -> {}", decimal, integer, character);
-
- // 当把任何类型转换为无符号类型 T 时,会不断加上或减去 (std::T::MAX + 1)
- // 直到值位于新类型 T 的范围内。
-
- // 1000 已经在 u16 的范围内
- println!("1000 as a u16 is: {}", 1000 as u16);
-
- // 1000 - 256 - 256 - 256 = 232
- // 事实上的处理方式是:从最低有效位(LSB,least significant bits)开始保留
- // 8 位,然后剩余位置,直到最高有效位(MSB,most significant bit)都被抛弃。
- // 译注:MSB 就是二进制的最高位,LSB 就是二进制的最低位,按日常书写习惯就是
- // 最左边一位和最右边一位。
- println!("1000 as a u8 is : {}", 1000 as u8);
- // -1 + 256 = 255
- println!(" -1 as a u8 is : {}", (-1i8) as u8);
-
- // 对正数,这就和取模一样。
- println!("1000 mod 256 is : {}", 1000 % 256);
-
- // 当转换到有符号类型时,(位操作的)结果就和 “先转换到对应的无符号类型,
- // 如果 MSB 是 1,则该值为负” 是一样的。
-
- // 当然如果数值已经在目标类型的范围内,就直接把它放进去。
- println!(" 128 as a i16 is: {}", 128 as i16);
- // 128 转成 u8 还是 128,但转到 i8 相当于给 128 取八位的二进制补码,其值是:
- println!(" 128 as a i8 is : {}", 128 as i8);
-
- // 重复之前的例子
- // 1000 as u8 -> 232
- println!("1000 as a u8 is : {}", 1000 as u8);
- // 232 的二进制补码是 -24
- println!(" 232 as a i8 is : {}", 232 as i8);
- }
- // `NanoSecond` 是 `u64` 的新名字。
- type NanoSecond = u64;
- type Inch = u64;
-
- // 通过这个属性屏蔽警告。
- #[allow(non_camel_case_types)]
- type u64_t = u64;
- // 试一试 ^ 移除上面那个属性
-
- fn main() {
- // `NanoSecond` = `Inch` = `u64_t` = `u64`.
- let nanoseconds: NanoSecond = 5 as u64_t;
- let inches: Inch = 2 as u64_t;
-
- // 注意类型别名*并不能*提供额外的类型安全,因为别名*并不是*新的类型。
- println!("{} nanoseconds + {} inches = {} unit?",
- nanoseconds,
- inches,
- nanoseconds + inches);
- }
Rust 2大类数据类型:标量、复合
- fn main() {
- // 变量可以给出类型说明。
- let logical: bool = true;
-
- let a_float: f64 = 1.0; // 常规说明
- let an_integer = 5i32; // 后缀说明
-
- // 否则会按默认方式决定类型。
- let default_float = 3.0; // `f64`
- let default_integer = 7; // `i32`
-
- // 类型也可根据上下文自动推断。
- let mut inferred_type = 12; // 根据下一行的赋值推断为 i64 类型
- inferred_type = 12345i64;
-
- // 可变的(mutable)变量,其值可以改变。
- let mut mutable = 12; // Mutable `i32`
- mutable = 21;
-
- // 报错!变量的类型并不能改变。
- mutable = true;
-
- // 但可以用遮蔽(shadow)来覆盖前面的变量。
- let mutable = true;
- }
Rust 有两个原生的 复合(compound)类型:元组(tuple)、数组(array)。
()
来构造(construct),而每个元组自身又是一个类型标记为 (T1, T2, ...)
的值,其中 T1
、T2
是每个元素的类型。函数可以使用元组来返回多个值,因为元组可以拥有任意多个值。元组长度固定,一旦声明,其长度不会增大或缩小。一元元组的元素类型后面需要有一个逗号。元组类型的示例:
()
空元组(empty tuple),也被称为“单位类型”(unit type),类似于空结构体,不包含任何值。单位类型 ()
在 Rust 中用于表示没有返回值的函数或表达式。一个函数不返回任何值的时候,其返回类型将是 ()。类似地,当调用一个没有返回值的函数或方法时,表达式的类型也将是 ()。(f64, f64)
(String, i32)
(i32, String)
(跟前一个示例类型不一样)(i32, f64, Vec<String>, Option<bool>)
- // 元组可以充当函数的参数和返回值
- fn reverse(pair: (i32, bool)) -> (bool, i32) {
- // 可以使用 `let` 把一个元组的成员绑定到一些变量
- let (integer, boolean) = pair;
-
- (boolean, integer)
- }
-
- // 在 “动手试一试” 的练习中要用到下面这个结构体。
- #[derive(Debug)]
- struct Matrix(f32, f32, f32, f32);
-
- fn main() {
- // 包含各种不同类型的元组
- let long_tuple = (1u8, 2u16, 3u32, 4u64,
- -1i8, -2i16, -3i32, -4i64,
- 0.1f32, 0.2f64,
- 'a', true);
-
- // 通过元组的下标来访问具体的值
- println!("long tuple first value: {}", long_tuple.0);
- println!("long tuple second value: {}", long_tuple.1);
-
- // 元组也可以充当元组的元素
- let tuple_of_tuples = ((1u8, 2u16, 2u32), (4u64, -1i8), -2i16);
-
- // 元组可以打印
- println!("tuple of tuples: {:?}", tuple_of_tuples);
-
- // 但很长的元组无法打印
- // let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
- // println!("too long tuple: {:?}", too_long_tuple);
- // 试一试 ^ 取消上面两行的注释,阅读编译器给出的错误信息。
-
- let pair = (1, true);
- println!("pair is {:?}", pair);
-
- println!("the reversed pair is {:?}", reverse(pair));
-
- // 创建单元素元组需要一个额外的逗号,这是为了和被括号包含的字面量作区分。
- println!("one element tuple: {:?}", (5u32,));
- println!("just an integer: {:?}", (5u32));
-
- // 元组可以被解构(deconstruct),从而将值绑定给变量
- let tuple = (1, "hello", 4.5, true);
-
- let (a, b, c, d) = tuple;
- println!("{:?}, {:?}, {:?}, {:?}", a, b, c, d);
-
- let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
- println!("{:?}", matrix)
-
- }
[]
来创建,是一组拥有相同类型 T
的对象的集合,在内存中是连续存储的。- use std::mem;
-
- // 此函数借用一个 slice
- fn analyze_slice(slice: &[i32]) {
- println!("first element of the slice: {}", slice[0]);
- println!("the slice has {} elements", slice.len());
- }
-
- fn main() {
- // 定长数组(类型标记是多余的)
- let xs: [i32; 5] = [1, 2, 3, 4, 5];
-
- // 所有元素可以初始化成相同的值
- let ys: [i32; 500] = [0; 500];
-
- // 下标从 0 开始
- println!("first element of the array: {}", xs[0]);
- println!("second element of the array: {}", xs[1]);
-
- // `len` 返回数组的大小
- println!("array size: {}", xs.len());
-
- // 数组是在栈中分配的
- println!("array occupies {} bytes", mem::size_of_val(&xs));
-
- // 数组可以自动被借用成为 slice
- println!("borrow the whole array as a slice");
- analyze_slice(&xs);
-
- // slice 可以指向数组的一部分
- println!("borrow a section of the array as a slice");
- analyze_slice(&ys[1 .. 4]);
-
- // 越界的下标会引发致命错误(panic)
- println!("{}", xs[5]);
- }
- #![allow(unused)]
- fn main() {
- // 一个栈分配的数组
- let array: [i32; 3] = [1, 2, 3];
-
- // 一个堆分配的数组,被自动强转成切片
- let boxed_array: Box<[i32]> = Box::new([1, 2, 3]);
- }
&[T]
。句法
SliceType :
[
Type ]
切片是一种动态尺寸类型(dynamically sized type),它代表类型为 T
的元素组成的数据序列的一个“视图(view)”。切片类型写为 [T]
。
要使用切片类型,通常必须放在指针后面使用,例如:
&[T]
,共享切片('shared slice'),常被直接称为切片(slice
),它不拥有它指向的数据,只是借用。&mut [T]
,可变切片('mutable slice'),可变借用它指向的数据。Box<[T]>
, boxed切片('boxed slice')。- #![allow(unused)]
- fn main() {
- // 一个堆分配的数组,被自动强转成切片
- let boxed_array: Box<[i32]> = Box::new([1, 2, 3]);
-
- // 数组上的(共享)切片
- let slice: &[i32] = &boxed_array[..];
- }
Rust 自定义数据类型主要是通过下面这两个关键字来创建:
- #[derive(Debug)]
- struct Person {
- name: String,
- age: u8,
- }
-
- // 单元结构体
- struct Unit;
-
- // 元组结构体
- struct Pair(i32, f32);
-
- // 带有两个字段的结构体
- struct Point {
- x: f32,
- y: f32,
- }
-
- // 结构体可以作为另一个结构体的字段
- #[allow(dead_code)]
- struct Rectangle {
- // 可以在空间中给定左上角和右下角在空间中的位置来指定矩形。
- top_left: Point,
- bottom_right: Point,
- }
-
- fn main() {
- // 使用简单的写法初始化字段,并创建结构体
- let name = String::from("Peter");
- let age = 27;
- let peter = Person { name, age };
-
- // 以 Debug 方式打印结构体
- println!("{:?}", peter);
-
- // 实例化结构体 `Point`
- let point: Point = Point { x: 10.3, y: 0.4 };
-
- // 访问 point 的字段
- println!("point coordinates: ({}, {})", point.x, point.y);
-
- // 使用结构体更新语法创建新的 point,
- // 这样可以用到之前的 point 的字段
- let bottom_right = Point { x: 5.2, ..point };
-
- // `bottom_right.y` 与 `point.y` 一样,因为这个字段就是从 `point` 中来的
- println!("second point: ({}, {})", bottom_right.x, bottom_right.y);
-
- // 使用 `let` 绑定来解构 point
- let Point { x: left_edge, y: top_edge } = point;
-
- let _rectangle = Rectangle {
- // 结构体的实例化也是一个表达式
- top_left: Point { x: left_edge, y: top_edge },
- bottom_right: bottom_right,
- };
-
- // 实例化一个单元结构体
- let _unit = Unit;
-
- // 实例化一个元组结构体
- let pair = Pair(1, 0.1);
-
- // 访问元组结构体的字段
- println!("pair contains {:?} and {:?}", pair.0, pair.1);
-
- // 解构一个元组结构体
- let Pair(integer, decimal) = pair;
-
- println!("pair contains {:?} and {:?}", integer, decimal);
- }
- // 该属性用于隐藏对未使用代码的警告。
- #![allow(dead_code)]
-
- // 创建一个 `enum`(枚举)来对 web 事件分类。注意变量名和类型共同指定了 `enum`
- // 取值的种类:`PageLoad` 不等于 `PageUnload`,`KeyPress(char)` 不等于
- // `Paste(String)`。各个取值不同,互相独立。
- enum WebEvent {
- // 一个 `enum` 可以是单元结构体(称为 `unit-like` 或 `unit`),
- PageLoad,
- PageUnload,
- // 或者一个元组结构体,
- KeyPress(char),
- Paste(String),
- // 或者一个普通的结构体。
- Click { x: i64, y: i64 }
- }
-
- // 此函数将一个 `WebEvent` enum 作为参数,无返回值。
- fn inspect(event: WebEvent) {
- match event {
- WebEvent::PageLoad => println!("page loaded"),
- WebEvent::PageUnload => println!("page unloaded"),
- // 从 `enum` 里解构出 `c`。
- WebEvent::KeyPress(c) => println!("pressed '{}'.", c),
- WebEvent::Paste(s) => println!("pasted \"{}\".", s),
- // 把 `Click` 解构给 `x` and `y`。
- WebEvent::Click { x, y } => {
- println!("clicked at x={}, y={}.", x, y);
- },
- }
- }
-
- fn main() {
- let pressed = WebEvent::KeyPress('x');
- // `to_owned()` 从一个字符串切片中创建一个具有所有权的 `String`。
- let pasted = WebEvent::Paste("my text".to_owned());
- let click = WebEvent::Click { x: 20, y: 80 };
- let load = WebEvent::PageLoad;
- let unload = WebEvent::PageUnload;
-
- inspect(pressed);
- inspect(pasted);
- inspect(click);
- inspect(load);
- inspect(unload);
- }
- enum EnumTest {
- Add,
- Subtract,
- }
-
- impl EnumTest {
- fn run(&self, x: i32, y: i32) -> i32 {
- match self {
- Self::Add => x + y,
- Self::Subtract => x - y,
- }
- }
- }
-
- fn main() {
- let temp: EnumTest = EnumTest::Add;
- let result = temp.run(10,20);
- println!("{result}");
- }
enum
的一个常见用法就是创建链表(linked-list):
- use List::*;
-
- enum List {
- // Cons:元组结构体,包含链表的一个元素和一个指向下一节点的指针
- Cons(u32, Box<List>),
- // Nil:末结点,表明链表结束
- Nil,
- }
-
- // 可以为 enum 定义方法
- impl List {
- // 创建一个空的 List 实例
- fn new() -> List {
- // `Nil` 为 `List` 类型(译注:因 `Nil` 的完整名称是 `List::Nil`)
- Nil
- }
-
- // 处理一个 List,在其头部插入新元素,并返回该 List
- fn prepend(self, elem: u32) -> List {
- // `Cons` 同样为 List 类型
- Cons(elem, Box::new(self))
- }
-
- // 返回 List 的长度
- fn len(&self) -> u32 {
- // 必须对 `self` 进行匹配(match),因为这个方法的行为取决于 `self` 的
- // 取值种类。
- // `self` 为 `&List` 类型,`*self` 为 `List` 类型,匹配一个具体的 `T`
- // 类型要好过匹配引用 `&T`。
- match *self {
- // 不能得到 tail 的所有权,因为 `self` 是借用的;
- // 因此使用一个对 tail 的引用
- Cons(_, ref tail) => 1 + tail.len(),
- // (递归的)基准情形(base case):一个长度为 0 的空列表
- Nil => 0
- }
- }
-
- // 返回列表的字符串表示(该字符串是堆分配的)
- fn stringify(&self) -> String {
- match *self {
- Cons(head, ref tail) => {
- // `format!` 和 `print!` 类似,但返回的是一个堆分配的字符串,
- // 而不是打印结果到控制台上
- format!("{}, {}", head, tail.stringify())
- },
- Nil => {
- format!("Nil")
- },
- }
- }
- }
-
- fn main() {
- // 创建一个空链表
- let mut list = List::new();
-
- // 追加一些元素
- list = list.prepend(1);
- list = list.prepend(2);
- list = list.prepend(3);
-
- // 显示链表的最后状态
- println!("linked list has length: {}", list.len());
- println!("{}", list.stringify());
- }
常量(constant)可以通过 const 和 static 关键字来创建。
Rust 有两种常量,可以在任意作用域声明,包括全局作用域。它们都需要显式的类型声明:
const
:不可改变的值(通常使用这种)。static
:具有 'static 生命周期的,可以是可变的变量(译注:须使用 static mut
关键字)。有个特例就是 "string"
字面量。它可以不经改动就被赋给一个 static
变量,因为它的类型标记:&'static str
就包含了所要求的生命周期 'static
。其他的引用类型都必须特地声明,使之拥有 'static
生命周期。这两种引用类型的差异似乎也无关紧要,因为无论如何,static
变量都得显式地声明。
- // 全局变量是在所有其他作用域之外声明的。
- static LANGUAGE: &'static str = "Rust";
- const THRESHOLD: i32 = 10;
-
- fn is_big(n: i32) -> bool {
- // 在一般函数中访问常量
- n > THRESHOLD
- }
-
- fn main() {
- let n = 16;
-
- // 在 main 函数(主函数)中访问常量
- println!("This is {}", LANGUAGE);
- println!("The threshold is {}", THRESHOLD);
- println!("{} is {}", n, if is_big(n) { "big" } else { "small" });
-
- // 报错!不能修改一个 `const` 常量。
- THRESHOLD = 5;
- // 改正 ^ 注释掉此行
- }
三个在 Rust 程序中被广泛使用的集合:
String
类型,不过在本章我们将深入了解。对于标准库提供的其他类型的集合,请查看文档。
标准库提供了很多自定义类型,在原生类型基础上进行了大量扩充。这是部分自定义类型:
String
(字符串),如: "hello world"
[1, 2, 3]
Option<i32>
Result<i32, i32>
Box<i32>
String
)字符串(String
)类型由 Rust 标准库提供,而不是编入核心语言。
更多 str
/String
方法可以在 std::str 和 std::string 模块中找到。若需要在编码间转换,请使用 encoding crate。Rust 参考中的 Tokens 一章详细地列出了书写字符串字面量和转义字符的方法。
Rust 中有两种字符串类型:String
和 &str
。
&str
:表示 "字符串字面值",使用双引号括起来,例如:"Hello, World!"。这是一种字符串类型的常量表示方法。字符串字面值是静态不可变的,不能修改其中的内容。你可以直接使用字符串字面值进行一些简单的操作,如拼接、切割等,但无法修改它们的值。字符串字面值就是 String 的 slice str,通常以被借用的形式出现,&str
。String
:表示 String 类型,是动态可变的字符串。String
被存储为由字节组成的 vector(Vec<u8>
),但保证了它一定是一个有效的 UTF-8 序列。String
是堆分配的,可增长的,且不是零结尾的(null terminated)。- fn main() {
- let s1 = String::from("Hello, ");
- let s2 = String::from("world!");
- let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用
-
- println!("s2 ---> {s2}");
- println!("s3 ---> {s3}");
-
- let s1 = String::from("tic");
- let s2 = String::from("tac");
- let s3 = String::from("toe");
- // 宏 format! 生成的代码使用引用所以不会获取任何参数的所有权。
- let s = format!("{s1}-{s2}-{s3}");
-
- 索引字符串
- println!("s ---> {s}");
- println!("s1 ---> {s1}");
- println!("s2 ---> {s2}");
- println!("s3 ---> {s3}");
- }
示例:
- fn main() {
- // (所有的类型标注都不是必需的)
- // 一个对只读内存中分配的字符串的引用
- let pangram: &'static str = "the quick brown fox jumps over the lazy dog";
- println!("Pangram: {}", pangram);
-
- // 逆序迭代单词,这里并没有分配新的字符串
- println!("Words in reverse");
- for word in pangram.split_whitespace().rev() {
- println!("> {}", word);
- }
-
- // 复制字符到一个 vector,排序并移除重复值
- let mut chars: Vec<char> = pangram.chars().collect();
- chars.sort();
- chars.dedup();
-
- // 创建一个空的且可增长的 `String`
- let mut string = String::new();
- for c in chars {
- // 在字符串的尾部插入一个字符
- string.push(c);
- // 在字符串尾部插入一个字符串
- string.push_str(", ");
- }
-
- // 这个缩短的字符串是原字符串的一个切片,所以没有执行新的分配操作
- let chars_to_trim: &[char] = &[' ', ','];
- let trimmed_str: &str = string.trim_matches(chars_to_trim);
- println!("Used characters: {}", trimmed_str);
-
- // 堆分配一个字符串
- let alice = String::from("I like dogs");
- // 分配新内存并存储修改过的字符串
- let bob: String = alice.replace("dog", "cat");
-
- println!("Alice says: {}", alice);
- println!("Bob says: {}", bob);
- }
slice 数据结构仅仅储存了开始位置和 slice 的长度。所以虽然 &T
是一个储存了 T
所在的内存位置的单个值,&str
则是 两个 值:str
的地址和其长度。
字面量中出现的字符串或字符定界符必须转义:"\""、'\''。
也可以使用原始字符串(raw string): r"Escapes don't work here: \x3F \u{211D}"
vector 是大小可变的数组。和 slice(切片)类似,它们的大小在编译时是未知的,但它们可以随时扩大或缩小。一个 vector 使用 3 个词来表示:一个指向数据的指针,vector 的长度,还有它的容量。此容量指明了要为这个 vector 保留多少内存。vector 的长度只要小于该容量,就可以随意增长;当需要超过这个阈值时,会给 vector 重新分配一段更大的容量。
更多 Vec
方法可以在 std::vec 模块中找到。
- fn main() {
-
- let mut v = Vec::new();
- v.push(5);
- v.push(6);
- v.push(7);
- v.push(8);
- println!("{:?}", v);
-
- let v = vec![1, 2, 3, 4, 5];
- let third: &i32 = &v[2];
- println!("The third element is {third}");
-
- let third: Option<&i32> = v.get(2);
- match third {
- Some(third) => println!("The third element is {third}"),
- None => println!("There is no third element."),
- }
- }
- fn main() {
- let mut v = vec![100, 32, 57];
- for i in &mut v {
- // 为了修改可变引用所指向的值,在使用 += 运算符之前
- // 必须使用解引用运算符(*)获取 i 中的值。
- *i += 100;
- }
- for i in &v{
- println!("{i}")
- }
- }
示例:
- fn main() {
-
- let mut v = Vec::new();
- v.push(5);
- v.push(6);
- v.push(7);
- v.push(8);
- println!("{:?}", v);
-
- let v = vec![1, 2, 3, 4, 5];
- let third: &i32 = &v[2];
- println!("The third element is {third}");
-
- let third: Option<&i32> = v.get(2);
- match third {
- Some(third) => println!("The third element is {third}"),
- None => println!("There is no third element."),
- }
-
- let mut v = vec![100, 32, 57];
- for i in &mut v {
- *i += 100;
- }
- for i in &v{
- println!("{i}")
- }
-
- #[derive(Debug)]
- enum SpreadsheetCell {
- Int(i32),
- Float(f64),
- Text(String),
- }
-
- // 使用枚举来存储多个类型,类比 Python 的 list
- let row = vec![
- SpreadsheetCell::Int(3),
- SpreadsheetCell::Text(String::from("blue")),
- SpreadsheetCell::Float(10.12),
- ];
-
- for cell in &row {
- match cell {
- SpreadsheetCell::Int(value) => println!("整数值: {}", value),
- SpreadsheetCell::Text(value) => println!("文本值: {}", value),
- SpreadsheetCell::Float(value) => println!("浮点数值: {}", value),
- }
- }
- }
- fn main() {
- // 迭代器可以被收集到 vector 中
- let collected_iterator: Vec<i32> = (0..10).collect();
- println!("Collected (0..10) into: {:?}", collected_iterator);
-
- // `vec!` 宏可用来初始化一个 vector
- let mut xs = vec![1i32, 2, 3];
- println!("Initial vector: {:?}", xs);
-
- // 在 vector 的尾部插入一个新的元素
- println!("Push 4 into the vector");
- xs.push(4);
- println!("Vector: {:?}", xs);
-
- // 报错!不可变 vector 不可增长
- // collected_iterator.push(0);
- // 改正 ^ 将此行注释掉
-
- // `len` 方法获得一个 vector 的当前大小
- println!("Vector size: {}", xs.len());
-
- // 下标使用中括号表示(从 0 开始)
- println!("Second element: {}", xs[1]);
-
- // `pop` 移除 vector 的最后一个元素并将它返回
- println!("Pop last element: {:?}", xs.pop());
-
- // 超出下标范围将抛出一个 panic
- println!("Fourth element: {}", xs[3]);
- // 改正 ^ 注释掉此行
-
- // 迭代一个 `Vector` 很容易
- println!("Contents of xs:");
- for x in xs.iter() {
- println!("> {}", x);
- }
-
- // 可以在迭代 `Vector` 的同时,使用独立变量(`i`)来记录迭代次数
- for (i, x) in xs.iter().enumerate() {
- println!("In position {} we have value {}", i, x);
- }
-
- // 多亏了 `iter_mut`,可变的 `Vector` 在迭代的同时,其中每个值都能被修改
- for x in xs.iter_mut() {
- *x *= 3;
- }
- println!("Updated vector: {:?}", xs);
- }
Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加 match
意味着 Rust 能在编译时就保证总是会处理所有可能的情况,
如果在编写程序时不能确切无遗地知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,
vector 通过整型下标来存储值,而 HashMap
(散列表)通过键(key)来存储值。HashMap
的键可以是布尔型、整型、字符串,或任意实现了 Eq
和 Hash
trait 的其他类型。HashMap<K, V>
类型储存了一个键类型 K
对应一个值类型 V
的映射。它通过一个 哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表、关联数组、Python的字典(Dict) 等。
和 vector 类似,HashMap
也是可增长的,但 HashMap 在占据了多余空间时还可以缩小自己。可以使用 HashMap::with_capacity(unit)
创建具有一定初始容量的 HashMap,也可以使用 HashMap::new()
来获得一个带有默认初始容量的 HashMap(推荐方式)。
- use std::collections::HashMap;
-
- fn call(number: &str) -> &str {
- match number {
- "798-1364" => "We're sorry, the call cannot be completed as dialed.
- Please hang up and try again.",
- "645-7689" => "Hello, this is Mr. Awesome's Pizza. My name is Fred.
- What can I get for you today?",
- _ => "Hi! Who is this again?"
- }
- }
-
- fn main() {
- let mut scores = HashMap::new();
- scores.insert(String::from("Blue"), 10);
- scores.insert(String::from("Yellow"), 50);
-
- let mut contacts = HashMap::new();
- contacts.insert("Daniel", "798-1364");
- contacts.insert("Ashley", "645-7689");
- contacts.insert("Katie", "435-8291");
- contacts.insert("Robert", "956-1745");
- // 接受一个引用并返回 Option<&V>
- match contacts.get(&"Daniel") {
- Some(&number) => println!("Calling Daniel: {}", call(number)),
- _ => println!("Don't have Daniel's number."),
- }
- // 如果被插入的值为新内容,那么 `HashMap::insert()` 返回 `None`,
- // 否则返回 `Some(value)`
- contacts.insert("Daniel", "164-6743");
- match contacts.get(&"Ashley") {
- Some(&number) => println!("Calling Ashley: {}", call(number)),
- _ => println!("Don't have Ashley's number."),
- }
- contacts.remove(&("Ashley"));
- // `HashMap::iter()` 返回一个迭代器,该迭代器以任意顺序举出
- // (&'a key, &'a value) 对。
- for (contact, &number) in contacts.iter() {
- println!("Calling {}: {}", contact, call(number));
- }
- }
必须首先 use
标准库中集合部分的 HashMap
。在这三个常用集合中,HashMap
是最不常用的,所以并没有被 prelude 自动引用。标准库中对 HashMap
的支持也相对较少,例如,并没有内建的构建宏。类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
任何实现了 Eq
和 Hash
trait 的类型都可以充当 HashMap
的键。这包括:
bool
(当然这个用处不大,因为只有两个可能的键)int
,unit
,以及其他整数类型String
和 &str
(友情提示:如果使用 String
作为键来创建 HashMap
,则可以 将 &str
作为散列表的 .get()
方法的参数,以获取值)注意到 f32
和 f64
没有实现 Hash
,这很大程度上是由于若使用浮点数作为散列表的键,浮点精度误差会很容易导致错误。
对于所有的集合类(collection class),如果它们包含的类型都分别实现了 Eq
和 Hash
,那么这些集合类也就实现了 Eq
和 Hash
。例如,若 T
实现了 Hash
,则 Vec<T>
也实现了 Hash
。
对自定义类型可以轻松地实现 Eq
和 Hash
,只需加上一行代码:#[derive(PartialEq, Eq, Hash)]
。
编译器将会完成余下的工作。如果你想控制更多的细节,你可以手动实现 Eq
和/或 Hash
。
- use std::collections::HashMap;
-
- // Eq 要求你对此类型推导 PartiaEq。
- #[derive(PartialEq, Eq, Hash)]
- struct Account<'a>{
- username: &'a str,
- password: &'a str,
- }
-
- struct AccountInfo<'a>{
- name: &'a str,
- email: &'a str,
- }
-
- type Accounts<'a> = HashMap<Account<'a>, AccountInfo<'a>>;
-
- fn try_logon<'a>(accounts: &Accounts<'a>,
- username: &'a str, password: &'a str){
- println!("Username: {}", username);
- println!("Password: {}", password);
- println!("Attempting logon...");
-
- let logon = Account {
- username: username,
- password: password,
- };
-
- match accounts.get(&logon) {
- Some(account_info) => {
- println!("Successful logon!");
- println!("Name: {}", account_info.name);
- println!("Email: {}", account_info.email);
- },
- _ => println!("Login failed!"),
- }
- }
-
- fn main(){
- let mut accounts: Accounts = HashMap::new();
-
- let account = Account {
- username: "j.everyman",
- password: "password123",
- };
-
- let account_info = AccountInfo {
- name: "John Everyman",
- email: "j.everyman@email.com",
- };
-
- accounts.insert(account, account_info);
-
- try_logon(&accounts, "j.everyman", "psasword123");
-
- try_logon(&accounts, "j.everyman", "password123");
- }
(HashSet<T>
实际上只是对 HashMap<T, ()>
的封装)。不会出现重复的 值。 如果插入的值已经存在于 HashSet
中(也就是,新值等于已存在的值,并且拥有相同的散列值),那么新值将会替换旧的值。集合(set)可以做更多的事。
集合(set)拥有 4 种基本操作(下面的调用全部都返回一个迭代器):
union
(并集):获得两个集合中的所有元素(不含重复值)。
difference
(差集):获取属于第一个集合而不属于第二集合的所有元素。
intersection
(交集):获取同时属于两个集合的所有元素。
symmetric_difference
(对称差):获取所有只属于其中一个集合,而不同时属于 两个集合的所有元素。
- use std::collections::HashSet;
-
- fn main() {
- let mut a: HashSet<i32> = vec!(1i32, 2, 3).into_iter().collect();
- let mut b: HashSet<i32> = vec!(2i32, 3, 4).into_iter().collect();
-
- assert!(a.insert(4));
- assert!(a.contains(&4));
-
- // 如果值已经存在,那么 `HashSet::insert()` 返回 false。
- assert!(b.insert(4), "Value 4 is already in set B!");
- // 改正 ^ 将此行注释掉。
-
- b.insert(5);
-
- // 若一个集合(collection)的元素类型实现了 `Debug`,那么该集合也就实现了 `Debug`。
- // 这通常将元素打印成这样的格式 `[elem1, elem2, ...]
- println!("A: {:?}", a);
- println!("B: {:?}", b);
-
- // 乱序打印 [1, 2, 3, 4, 5]。
- println!("Union: {:?}", a.union(&b).collect::<Vec<&i32>>());
-
- // 这将会打印出 [1]
- println!("Difference: {:?}", a.difference(&b).collect::<Vec<&i32>>());
-
- // 乱序打印 [2, 3, 4]。
- println!("Intersection: {:?}", a.intersection(&b).collect::<Vec<&i32>>());
-
- // 打印 [1, 5]
- println!("Symmetric Difference: {:?}",
- a.symmetric_difference(&b).collect::<Vec<&i32>>());
- }
Rust 使用 trait 解决类型之间的转换问题。最一般的转换会用到 From 和 Into 两个 trait。不过,即便常见的情况也可能会用到特别的 trait,尤其是从 String
转换到别的类型,以及把别的类型转换到 String
时。
From 和 Into 两个 trait 是内部相关联的。如果我们能够从类型 B 得到类型 A,那么很容易相信我们也能够把类型 B 转换为类型 A。
From trait 允许一种类型定义 “怎么根据另一种类型生成自己”,因此它提供了一种类型转换的简单机制。在标准库中有无数 From 的实现,规定原生类型及其他常见类型的转换功能。
比如,可以很容易地把 str 转换成 String:
- #![allow(unused)]
- fn main() {
- let my_str = "hello";
- let my_string = String::from(my_str);
- }
也可以为我们自己的类型定义转换机制:
- use std::convert::From;
-
- #[derive(Debug)]
- struct Number {
- value: i32,
- }
-
- impl From<i32> for Number {
- fn from(item: i32) -> Self {
- Number { value: item }
- }
- }
-
- fn main() {
- let num = Number::from(30);
- println!("My number is {:?}", num);
- }
Into trait 就是把 From trait 倒过来而已。 就是根据自己类型怎么生成其他类型。使用 Into
trait 通常要求指明要转换到的类型,因为编译器大多数时候不能推断它。
- use std::convert::From;
-
- #[derive(Debug)]
- struct Number {
- value: i32,
- }
-
- impl From<i32> for Number {
- fn from(item: i32) -> Self {
- Number { value: item }
- }
- }
-
- fn main() {
- let int = 5;
- // 试试删除类型说明
- let num: Number = int.into();
- println!("My number is {:?}", num);
- }
TryFrom
和 TryInto
trait 用于易出错的转换,也正因如此,其返回值是 Result 型。
- use std::convert::TryFrom;
- use std::convert::TryInto;
-
- #[derive(Debug, PartialEq)]
- struct EvenNumber(i32);
-
- impl TryFrom<i32> for EvenNumber {
- type Error = ();
-
- fn try_from(value: i32) -> Result<Self, Self::Error> {
- if value % 2 == 0 {
- Ok(EvenNumber(value))
- } else {
- Err(())
- }
- }
- }
-
- fn main() {
- // TryFrom
-
- assert_eq!(EvenNumber::try_from(8), Ok(EvenNumber(8)));
- assert_eq!(EvenNumber::try_from(5), Err(()));
-
- // TryInto
-
- let result: Result<EvenNumber, ()> = 8i32.try_into();
- assert_eq!(result, Ok(EvenNumber(8)));
- let result: Result<EvenNumber, ()> = 5i32.try_into();
- assert_eq!(result, Err(()));
- }
要把任何类型转换成 String
,只需要实现那个类型的 ToString trait。如果不实现,也可以实现 fmt::Display trait,它会自动提供 ToString,并且还可以用来打印类型,就像 print! 一节中讨论的那样。
- use std::fmt;
-
- struct Circle {
- radius: i32
- }
-
- impl fmt::Display for Circle {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "Circle of radius {}", self.radius)
- }
- }
-
- fn main() {
- let circle = Circle { radius: 6 };
- println!("{}", circle.to_string());
- }
- use std::string::ToString;
-
- struct Circle {
- radius: i32
- }
-
- impl ToString for Circle {
- fn to_string(&self) -> String {
- format!("Circle of radius {:?}", self.radius)
- }
- }
-
- fn main() {
- let circle = Circle { radius: 6 };
- println!("{}", circle.to_string());
- }
解析字符串。经常需要把字符串转成数字。完成这项工作的标准手段是用 parse 函数。我们得提供要转换到的类型,这可以通过不使用类型推断,或者用 “涡轮鱼” 语法(turbo fish,<>)实现。
- fn main() {
- let parsed: i32 = "5".parse().unwrap();
- let turbo_parsed = "10".parse::<i32>().unwrap();
-
- let sum = parsed + turbo_parsed;
- println!{"Sum: {:?}", sum};
- }
所有权(系统)是 Rust 最为与众不同的特性,它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全,因此理解 Rust 中所有权如何工作是十分重要的。
Rust 强制实行 RAII(Resource Acquisition Is Initialization,资源获取即初始化),所以任何对象在离开作用域时,它的析构函数(destructor)就被调用,然后它占有的资源就被释放。这种行为避免了资源泄漏(resource leak),所以不用手动释放内存或担心内存泄漏(memory leak)!
快速入门示例:
- // raii.rs
- fn create_box() {
- // 在堆上分配一个整型数据
- let _box1 = Box::new(3i32);
-
- // `_box1` 在这里被销毁,内存得到释放
- }
-
- fn main() {
- // 在堆上分配一个整型数据
- let _box2 = Box::new(5i32);
-
- // 嵌套作用域:
- {
- // 在堆上分配一个整型数据
- let _box3 = Box::new(4i32);
-
- // `_box3` 在这里被销毁,内存得到释放
- }
-
- // 创建一大堆 box(只是因为好玩)。
- // 完全不需要手动释放内存!
- for _ in 0u32..1_000 {
- create_box();
- }
-
- // `_box2` 在这里被销毁,内存得到释放
- }
Rust 中的析构函数概念是通过 Drop trait 提供的。当资源离开作用域,就调用析构函数。你无需为每种类型都实现 Drop trait,只要为那些需要自己的析构函数逻辑的类型实现就可以了。
示例:当 main
函数中的变量离开作用域,自定义的析构函数就会被调用:
- struct ToDrop;
-
- impl Drop for ToDrop {
- fn drop(&mut self) {
- println!("ToDrop is being dropped");
- }
- }
-
- fn main() {
- let x = ToDrop;
- println!("Made a ToDrop!");
- }
因为变量要负责释放它们拥有的资源,所以资源只能拥有一个所有者。这也防止了资源的重复释放。注意并非所有变量都拥有资源(例如引用)。
在进行 赋值(let x = y) 或 通过值来传递函数参数(foo(x)) 的时候,资源的 所有权(ownership)会发生转移。按照 Rust 的说法,这被称为资源的移动(move)。在移动资源之后,原来的所有者不能再被使用,这可避免悬挂指针(dangling pointer)的产生。
- // 此函数取得堆分配的内存的所有权
- fn destroy_box(c: Box<i32>) {
- println!("Destroying a box that contains {}", c);
-
- // `c` 被销毁且内存得到释放
- }
-
- fn main() {
- // 栈分配的整型
- let x = 5u32;
-
- // 将 `x` *复制*到 `y`——不存在资源移动
- let y = x;
-
- // 两个值各自都可以使用
- println!("x is {}, and y is {}", x, y);
-
- // `a` 是一个指向堆分配的整数的指针
- let a = Box::new(5i32);
-
- println!("a contains: {}", a);
-
- // *移动* `a` 到 `b`
- let b = a;
- // 把 `a` 的指针地址(而非数据)复制到 `b`。现在两者都指向
- // 同一个堆分配的数据,但是现在是 `b` 拥有它。
-
- // 报错!`a` 不能访问数据,因为它不再拥有那部分堆上的内存。
- //println!("a contains: {}", a);
- // 试一试 ^ 去掉此行注释
-
- // 此函数从 `b` 中取得堆分配的内存的所有权
- destroy_box(b);
-
- // 此时堆内存已经被释放,这个操作会导致解引用已释放的内存,而这是编译器禁止的。
- // 报错!和前面出错的原因一样。
- //println!("b contains: {}", b);
- // 试一试 ^ 去掉此行注释
- }
当所有权转移时,数据的可变性可能发生改变。
- fn main() {
- let immutable_box = Box::new(5u32);
-
- println!("immutable_box contains {}", immutable_box);
-
- // 可变性错误
- //*immutable_box = 4;
-
- // *移动* box,改变所有权(和可变性)
- let mut mutable_box = immutable_box;
-
- println!("mutable_box contains {}", mutable_box);
-
- // 修改 box 的内容
- *mutable_box = 4;
-
- println!("mutable_box now contains {}", mutable_box);
- }
在单个变量的解构内,可以同时使用 by-move
和 by-reference
模式绑定。这样做将导致变量的部分移动(partial move),这意味着变量的某些部分将被移动,而其他部分将保留。在这种情况下,后面不能整体使用父级变量,但是仍然可以使用只引用(而不移动)的部分。
- fn main() {
- #[derive(Debug)]
- struct Person {
- name: String,
- age: u8,
- }
-
- let person = Person {
- name: String::from("Alice"),
- age: 20,
- };
-
- // `name` 从 person 中移走,但 `age` 只是引用
- let Person { name, ref age } = person;
-
- println!("The person's age is {}", age);
-
- println!("The person's name is {}", name);
-
- // 报错!部分移动值的借用:`person` 部分借用产生
- //println!("The person struct is {:?}", person);
-
- // `person` 不能使用,但 `person.age` 因为没有被移动而可以继续使用
- println!("The person's age from person struct is {}", person.age);
- }
参考:解构
作用域在所有权(ownership)、借用(borrow)和生命周期(lifetime)中起着重要作用。作用域告诉编译器什么时候借用是合法的、什么时候资源可以释放、以及变量何时被创建或销毁。
变量是否有效与作用域的关系跟其他编程语言是类似的。
fn main() {
{ // s 在这里无效,它尚未声明
let s = "hello"; // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,s 不再有效,清理并drop(释放掉)内存空间
}这里有两个重要的时间点:
- 当
s
进入作用域 时,它就是有效的。- 这一直持续到它 离开作用域 为止。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 在 C++ 中,这里会发生浅拷贝,不过在 Rust 中会使第一个变量无效,这个操作被称为 移动(move),而不是叫做浅拷贝。为了确保内存安全,在let s2 = s1;
之后,Rust 认为s1
不再有效,因此 Rust 不需要在s1
离开作用域后清理任何东西。println!("{}, world!", s1); // 看看在
s2
被创建之后尝试使用s1
会发生什么,这里会报错, // 因为只有s2
是有效的,当其离开作用域,它就释放自己的内存
}这里隐含了 Rust 的一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”。
变量的 "移动(转移)"
只要进行 "赋值(=)、函数传参" 都会有 移动
- 移动 ( 转移 ):变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。
变量的 克隆
确实 需要深度复制
String
中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做clone
的通用函数。fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();println!("s1 = {}, s2 = {}", s1, s2);
}
只在栈上的数据:拷贝
fn main() {
let x = 5;
let y = x;println!("x = {}, y = {}", x, y);
}一个通用的规则,任何一组简单标量值的组合都可以实现 Copy,任何不需要分配内存或某种形式资源的类型都可以实现 Copy 。如下是一些 Copy 的类型:
- 所有整数类型,比如
u32
。- 布尔类型,
bool
,它的值是true
和false
。- 所有浮点数类型,比如
f64
。- 字符类型,
char
。- 元组,当且仅当其包含的类型也都实现
Copy
的时候。比如,(i32, i32)
实现了Copy
,但(i32, String)
就没有。
将值传递给函数与给变量赋值的原理相似。向函数传递值可能会移动或者复制,就像赋值语句一样。
fn main() {
let s = String::from("hello"); // s 进入作用域takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,
// 所以在后面可继续使用 x} // 这里,x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 没有特殊之处fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。
// 占用的内存被释放fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。没有特殊之处
返回值也可以转移所有权。
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 转移给 s1let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
} // 这里,s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 离开作用域并被丢弃fn gives_ownership() -> String { // gives_ownership 会将
// 返回值移动给
// 调用它的函数let some_string = String::from("yours"); // some_string 进入作用域。
some_string // 返回 some_string
// 并移出给调用的函数
//
}// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
//a_string // 返回 a_string 并移出给调用的函数
}
"借用 (引用)" 其实就相当于一个 "别名、外号"
Rust 借用(borrowing):只访问数据,但同时不获取其所有权。也就是 对象通过引用(&T
)来传递,而不是通过值(T
)来传递。编译器(通过借用检查)静态地保证了引用总是指向有效的对象。即当存在引用指向一个对象时,该对象不能被销毁。
理解 Rust 的 引用与借用:https://www.jianshu.com/p/ac519d8c5ec9
fn main() {
let s1 = String::from("hello");let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}fn calculate_length(s: &String) -> usize { // s 是 String 的引用
s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
// 所以什么也不会发生变量
s
有效的作用域与函数参数的作用域一样,不过当s
停止使用时并不丢弃引用指向的数据,因为s
并没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。
- 总结:将创建一个引用的行为称为 借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。因为并不拥有它。
正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。
如果想要修改引用的值,就需要用到 可变引用(mutable reference)。可变引用有一个很大的限制:如果创建了一个变量的可变引用,就不能再创建对该变量的引用。不可变引用的值本身就不希望被改变,一个变量可以有多个不可变引用。
fn main() {
let mut s = String::from("hello");let r1 = &mut s;
let r2 = &mut s;println!("{}, {}", r1, r2);
}
这个报错说这段代码是无效的,因为我们不能在同一时间多次将s
作为可变变量借用。第一个可变的借入在r1
中,并且必须持续到在println!
中使用它,但是在那个可变引用的创建和它的使用之间,我们又尝试在r2
中创建另一个可变引用,该引用借用与r1
相同的数据。这一限制以一种非常小心谨慎的方式允许可变性,防止同一时间对同一数据存在多个可变引用。新 Rustacean 们经常难以适应这一点,因为大部分语言中变量任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争(data race)类似于竞态条件,它可由这三个行为造成:
- 两个或更多指针同时访问同一数据。
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!
可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能 同时 拥有:
fn main() {
let mut s = String::from("hello");{
let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用let r2 = &mut s;
}
Rust 在同时使用可变与不可变引用时也采用的类似的规则。这些代码会导致一个错误:fn main() {
let mut s = String::from("hello");let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题println!("{}, {}, and {}", r1, r2, r3);
} // r1、r2、r3 作用域都是到这里结束,但是上面打印时,r1、r2 生效时 r3 也生效,所以报错。因为 rust 会自动判断 变量引用的作用域是否重叠,所以可以调整 println 的顺序即可。fn main() {
let mut s = String::from("hello");let r1 = &s; // 没问题
let r2 = &s; // 没问题println!("{}, {}", r1, r2);
let r3 = &mut s; // 大问题
println!("{}", r3);
}不可变引用
r1
和r2
的作用域在println!
最后一次使用之后结束,这也是创建可变引用r3
的地方。它们的作用域没有重叠,所以代码是可以编译的。编译器可以在作用域结束之前判断不再使用的引用。记住这是 Rust 编译器在提前指出一个潜在的 bug 的规定。
&T
对数据进行借用,借用者只能读数据,而不能修改- #[allow(dead_code)]
- #[derive(Clone, Copy)]
- struct Book {
- // `&'static str` 是一个对分配在只读内存区的字符串的引用
- author: &'static str,
- title: &'static str,
- year: u32,
- }
-
- // 此函数接受一个对 Book 类型的引用
- fn borrow_book(book: &Book) {
- println!("I immutably borrowed {} - {} edition", book.title, book.year);
- }
-
- // 此函数接受一个对可变的 Book 类型的引用,它把年份 `year` 改为 2014 年
- fn new_edition(book: &mut Book) {
- book.year = 2014;
- println!("I mutably borrowed {} - {} edition", book.title, book.year);
- }
-
- fn main() {
- // 创建一个名为 `immutabook` 的不可变的 Book 实例
- let immutabook = Book {
- // 字符串字面量拥有 `&'static str` 类型
- author: "Douglas Hofstadter",
- title: "Gödel, Escher, Bach",
- year: 1979,
- };
-
- // 创建一个 `immutabook` 的可变拷贝,命名为 `mutabook`
- let mut mutabook = immutabook;
-
- // 不可变地借用一个不可变对象
- borrow_book(&immutabook);
-
- // 不可变地借用一个可变对象
- borrow_book(&mutabook);
-
- // 可变地借用一个可变对象
- new_edition(&mut mutabook);
-
- // 报错!不能可变地借用一个不可变对象
- new_edition(&mut immutabook);
- // 改正 ^ 注释掉此行
- }
在 Rust 中,&
和 ref
都与 引用 相关,借用(&)的对象是必须存在的,ref引用的对象可以虚拟的,后期附上对象。区别:ref 关键字用于模式匹配中,将值绑定到引用上。其它地方,借用(&) 等价于 引用(ref)
- fn main() {
- let x = 42;
- if let Some(ref y) = Some(x) { // 使用 ref 将值 x 绑定到引用 y 上
- println!("Got a reference: {}", y);
- }
- }
赋值语句中左边的 ref 关键字等价于右边的 & 符号。即 ref 可以不用 & 从而创建引用。
- fn main() {
- let val = String::from("abc");
-
- let a = &val; // 直接引用
- println!("{:?}", a);
-
- let ref c = val; // 通过 ref, 对具体值创建引用。
- println!("{:?}", c);
- }
- #[derive(Clone, Copy)]
- struct Point { x: i32, y: i32 }
-
- fn main() {
- let c = 'Q';
-
- // 赋值语句中左边的 `ref` 关键字等价于右边的 `&` 符号。
- let ref ref_c1 = c;
- let ref_c2 = &c;
-
- println!("ref_c1 equals ref_c2: {}", *ref_c1 == *ref_c2);
-
- let point = Point { x: 0, y: 0 };
-
- // 在解构一个结构体时 `ref` 同样有效。
- let _copy_of_x = {
- // `ref_to_x` 是一个指向 `point` 的 `x` 字段的引用。
- let Point { x: ref ref_to_x, y: _ } = point;
-
- // 返回一个 `point` 的 `x` 字段的拷贝。
- *ref_to_x
- };
-
- // `point` 的可变拷贝
- let mut mutable_point = point;
-
- {
- // `ref` 可以与 `mut` 结合以创建可变引用。
- let Point { x: _, y: ref mut mut_ref_to_y } = mutable_point;
-
- // 通过可变引用来改变 `mutable_point` 的字段 `y`。
- *mut_ref_to_y = 1;
- }
-
- println!("point is ({}, {})", point.x, point.y);
- println!("mutable_point is ({}, {})", mutable_point.x, mutable_point.y);
-
- // 包含一个指针的可变元组
- let mut mutable_tuple = (Box::new(5u32), 3u32);
-
- {
- // 解构 `mutable_tuple` 来改变 `last` 的值。
- let (_, ref mut last) = mutable_tuple;
- *last = 2u32;
- }
-
- println!("tuple is {:?}", mutable_tuple);
- }
as_ref() 是一个方法,通常用于将某个类型转换为其引用类型,以便在函数参数或方法调用中接受引用类型而不是拥有所有权的类型。as_ref()
方法是由标准库中 AsRef
trait 定义的,并可用于所有实现了该 trait 的类型。它返回一个对原始值的引用,而不是拷贝整个值。
- fn print_length<T: AsRef<str>>(text: T) {
- let text_ref: &str = text.as_ref();
- println!("Length: {}", text_ref.len());
- }
-
- fn main() {
- let text = "Hello, world!";
- print_length(text); // T类型 ---> &T类型
- }
悬垂引用(Dangling References)
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
fn main() {
let reference_to_nothing = dangle();
}fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用,但是引用不转移所有权,所以函数结束时,s 被销毁释放内存
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!
正确的做法:不返回引用。fn main() {
let string = no_dangle();
}fn no_dangle() -> String {
let s = String::from("hello");
s // 这样就没有任何错误了。所有权被移动出去,所以没有值被释放。
}
Slice 类型:slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。slice 是一类引用,所以它同样没有所有权。
*
&
、ref
、和 ref mut
- fn main() {
- // 获得一个 `i32` 类型的引用。`&` 表示取引用。
- let reference = &4;
-
- match reference {
- // 如果用 `&val` 这个模式去匹配 `reference`,就相当于做这样的比较:
- // `&i32`(译注:即 `reference` 的类型)
- // `&val`(译注:即用于匹配的模式)
- // ^ 我们看到,如果去掉匹配的 `&`,`i32` 应当赋给 `val`。
- // 译注:因此可用 `val` 表示被 `reference` 引用的值 4。
- &val => println!("Got a value via destructuring: {:?}", val),
- }
-
- // 如果不想用 `&`,需要在匹配前解引用。
- match *reference {
- val => println!("Got a value via dereferencing: {:?}", val),
- }
-
- // 如果一开始就不用引用,会怎样? `reference` 是一个 `&` 类型,因为赋值语句
- // 的右边已经是一个引用。但下面这个不是引用,因为右边不是。
- let _not_a_reference = 3;
-
- // Rust 对这种情况提供了 `ref`。它更改了赋值行为,从而可以对具体值创建引用。
- // 下面这行将得到一个引用。
- let ref _is_a_reference = 3;
-
- // 相应地,定义两个非引用的变量,通过 `ref` 和 `ref mut` 仍可取得其引用。
- let value = 5;
- let mut mut_value = 6;
-
- // 使用 `ref` 关键字来创建引用。
- // 译注:下面的 r 是 `&i32` 类型,它像 `i32` 一样可以直接打印,因此用法上
- // 似乎看不出什么区别。但读者可以把 `println!` 中的 `r` 改成 `*r`,仍然能
- // 正常运行。前面例子中的 `println!` 里就不能是 `*val`,因为不能对整数解
- // 引用。
- match value {
- ref r => println!("Got a reference to a value: {:?}", r),
- }
-
- // 类似地使用 `ref mut`。
- match mut_value {
- ref mut m => {
- // 已经获得了 `mut_value` 的引用,先要解引用,才能改变它的值。
- *m += 10;
- println!("We added 10. `mut_value`: {:?}", m);
- },
- }
- }
生命周期(lifetime)是这样一种概念,编译器(中的借用检查器)用它来保证所有的借用都是有效的。确切地说,一个变量的生命周期在它创建的时候开始,在它销毁的时候结束。虽然生命周期和作用域经常被一起提到,但它们并不相同。
和闭包类似,使用生命周期需要泛型。若要给类型显式地标注生命周期,其语法会像是 &'a T
foo<'a> // foo 的生命周期 小于等于 'a 的周期。
foo<'a, 'b> // foo 的生命周期 小于等于 'a 和 'b 中任一个的周期。
显式生命周期标注的运用:
- // `print_refs` 接受两个 `i32` 的引用,它们有不同的生命周期 `'a` 和 `'b`。
- // 这两个生命周期都必须至少要和 `print_refs` 函数一样长。
- fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) {
- println!("x is {} and y is {}", x, y);
- }
-
- // 不带参数的函数,不过有一个生命周期参数 `'a`。
- fn failed_borrow<'a>() {
- let _x = 12;
-
- // 报错:`_x` 的生命周期不够长
- //let y: &'a i32 = &_x;
- // 在函数内部使用生命周期 `'a` 作为显式类型标注将导致失败,因为 `&_x` 的
- // 生命周期比 `y` 的短。短生命周期不能强制转换成长生命周期。
- }
-
- fn main() {
- // 创建变量,稍后用于借用。
- let (four, nine) = (4, 9);
-
- // 两个变量的借用(`&`)都传进函数。
- print_refs(&four, &nine);
- // 任何被借用的输入量都必须比借用者生存得更长。
- // 也就是说,`four` 和 `nine` 的生命周期都必须比 `print_refs` 的长。
-
- failed_borrow();
- // `failed_borrow` 未包含引用,因此不要求 `'a` 长于函数的生命周期,
- // 但 `'a` 寿命确实更长。因为该生命周期从未被约束,所以默认为 `'static`。
- }
带上生命周期的函数签名有一些限制:
static
)。另外要注意,如果没有输入的函数返回引用,有时会导致返回的引用指向无效数据,这种情况下禁止它返回这样的引用。下面例子展示了一些合法的带有生命周期的函数:
- // 一个拥有生命周期 `'a` 的输入引用,其中 `'a` 的存活时间
- // 至少与函数的一样长。
- fn print_one<'a>(x: &'a i32) {
- println!("`print_one`: x is {}", x);
- }
-
- // 可变引用同样也可能拥有生命周期。
- fn add_one<'a>(x: &'a mut i32) {
- *x += 1;
- }
-
- // 拥有不同生命周期的多个元素。对下面这种情形,两者即使拥有
- // 相同的生命周期 `'a` 也没问题,但对一些更复杂的情形,可能
- // 就需要不同的生命周期了。
- fn print_multi<'a, 'b>(x: &'a i32, y: &'b i32) {
- println!("`print_multi`: x is {}, y is {}", x, y);
- }
-
- // 返回传递进来的引用也是可行的。
- // 但必须返回正确的生命周期。
- fn pass_x<'a, 'b>(x: &'a i32, _: &'b i32) -> &'a i32 { x }
-
- //fn invalid_output<'a>() -> &'a String { &String::from("foo") }
- // 上面代码是无效的:`'a` 存活的时间必须比函数的长。
- // 这里的 `&String::from("foo")` 将会创建一个 `String` 类型,然后对它取引用。
- // 数据在离开作用域时删掉,返回一个指向无效数据的引用。
-
- fn main() {
- let x = 7;
- let y = 9;
-
- print_one(&x);
- print_multi(&x, &y);
-
- let z = pass_x(&x, &y);
- print_one(z);
-
- let mut t = 3;
- add_one(&mut t);
- print_one(&t);
- }
方法一般是不需要标明生命周期的,因为 self
的生命周期会赋给所有的输出生命周期参数,
方法的标注和函数类似:
- struct Owner(i32);
-
- impl Owner {
- // 标注生命周期,就像独立的函数一样。
- fn add_one<'a>(&'a mut self) { self.0 += 1; }
- fn print<'a>(&'a self) {
- println!("`print`: {}", self.0);
- }
- }
-
- fn main() {
- let mut owner = Owner(18);
-
- owner.add_one();
- owner.print();
- }
在结构体中标注生命周期也和函数的类似:
- // 一个 `Borrowed` 类型,含有一个指向 `i32` 类型的引用。
- // 该引用必须比 `Borrowed` 寿命更长。
- #[derive(Debug)]
- struct Borrowed<'a>(&'a i32);
-
- // 和前面类似,这里的两个引用都必须比这个结构体长寿。
- #[derive(Debug)]
- struct NamedBorrowed<'a> {
- x: &'a i32,
- y: &'a i32,
- }
-
- // 一个枚举类型,其取值不是 `i32` 类型就是一个指向 `i32` 的引用。
- #[derive(Debug)]
- enum Either<'a> {
- Num(i32),
- Ref(&'a i32),
- }
-
- fn main() {
- let x = 18;
- let y = 15;
-
- let single = Borrowed(&x);
- let double = NamedBorrowed { x: &x, y: &y };
- let reference = Either::Ref(&x);
- let number = Either::Num(y);
-
- println!("x is borrowed in {:?}", single);
- println!("x and y are borrowed in {:?}", double);
- println!("x is borrowed in {:?}", reference);
- println!("y is *not* borrowed in {:?}", number);
- }
trait 方法中生命期的标注基本上与函数类似。注意,impl
也可能有生命周期的标注。
- // 带有生命周期标注的结构体。
- #[derive(Debug)]
- struct Borrowed<'a> {
- x: &'a i32,
- }
-
- // 给 impl 标注生命周期。
- impl<'a> Default for Borrowed<'a> {
- fn default() -> Self {
- Self {
- x: &10,
- }
- }
- }
-
- fn main() {
- let b: Borrowed = Default::default(); // 实现多态
- println!("b is {:?}", b);
- }
就如泛型类型能够被约束一样,生命周期(它们本身就是泛型)也可以使用约束。:
字符的意义在这里稍微有些不同,不过 +
是相同的。注意下面的说明:
T: 'a
:在 T
中的所有引用都必须比生命周期 'a
活得更长。T: Trait + 'a
:T
类型必须实现 Trait
trait,并且在 T
中的所有引用都必须比 'a
活得更长。示例:
- use std::fmt::Debug; // 用于约束的 trait。
-
- #[derive(Debug)]
- struct Ref<'a, T: 'a>(&'a T);
- // `Ref` 包含一个指向泛型类型 `T` 的引用,其中 `T` 拥有一个未知的生命周期
- // `'a`。`T` 拥有生命周期限制, `T` 中的任何*引用*都必须比 `'a` 活得更长。另外
- // `Ref` 的生命周期也不能超出 `'a`。
-
- // 一个泛型函数,使用 `Debug` trait 来打印内容。
- fn print<T>(t: T) where
- T: Debug {
- println!("`print`: t is {:?}", t);
- }
-
- // 这里接受一个指向 `T` 的引用,其中 `T` 实现了 `Debug` trait,并且在 `T` 中的
- // 所有*引用*都必须比 `'a'` 存活时间更长。另外,`'a` 也要比函数活得更长。
- fn print_ref<'a, T>(t: &'a T) where
- T: Debug + 'a {
- println!("`print_ref`: t is {:?}", t);
- }
-
- fn main() {
- let x = 7;
- let ref_x = Ref(&x);
-
- print_ref(&ref_x);
- print(ref_x);
- }
一个较长的生命周期可以强制转成一个较短的生命周期,使它在一个通常情况下不能工作的作用域内也能正常工作。强制转换可由编译器隐式地推导并执行,也可以通过声明不同的生命周期的形式实现。
- // 在这里,Rust 推导了一个尽可能短的生命周期。
- // 然后这两个引用都被强制转成这个生命周期。
- fn multiply<'a>(first: &'a i32, second: &'a i32) -> i32 {
- first * second
- }
-
- // `<'a: 'b, 'b>` 读作生命周期 `'a` 至少和 `'b` 一样长。
- // 在这里我们我们接受了一个 `&'a i32` 类型并返回一个 `&'b i32` 类型,这是
- // 强制转换得到的结果。
- fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 {
- first
- }
-
- fn main() {
- let first = 2; // 较长的生命周期
-
- {
- let second = 3; // 较短的生命周期
-
- println!("The product is {}", multiply(&first, &second));
- println!("{} is the first", choose_first(&first, &second));
- };
- }
'static
生命周期是可能的生命周期中最长的,它会在整个程序运行的时期中存在。'static
生命周期也可被强制转换成一个更短的生命周期。有两种方式使变量拥有 'static
生命周期,它们都把数据保存在可执行文件的只读内存区:
static
声明来产生常量(constant)。&'static str
类型的 string
字面量。- // 产生一个拥有 `'static` 生命周期的常量。
- static NUM: i32 = 18;
-
- // 返回一个指向 `NUM` 的引用,该引用不取 `NUM` 的 `'static` 生命周期,
- // 而是被强制转换成和输入参数的一样。
- fn coerce_static<'a>(_: &'a i32) -> &'a i32 {
- &NUM
- }
-
- fn main() {
- {
- // 产生一个 `string` 字面量并打印它:
- let static_string = "I'm in read-only memory";
- println!("static_string: {}", static_string);
-
- // 当 `static_string` 离开作用域时,该引用不能再使用,不过
- // 数据仍然存在于二进制文件里面。
- }
-
- {
- // 产生一个整型给 `coerce_static` 使用:
- let lifetime_num = 9;
-
- // 将对 `NUM` 的引用强制转换成 `lifetime_num` 的生命周期:
- let coerced_static = coerce_static(&lifetime_num);
-
- println!("coerced_static: {}", coerced_static);
- }
-
- println!("NUM: {} stays accessible!", NUM);
- }
参考:'static 常量
有些生命周期的模式太常用了,所以借用检查器将会隐式地添加它们以减少程序输入量和增强可读性。这种隐式添加生命周期的过程称为省略(elision)。在 Rust 使用省略仅仅是因为这些模式太普遍了。对于省略的详细描述,可以参考官方文档的生命周期省略。
- // `elided_input` 和 `annotated_input` 事实上拥有相同的签名,
- // `elided_input` 的生命周期会被编译器自动添加:
- fn elided_input(x: &i32) {
- println!("`elided_input`: {}", x)
- }
-
- fn annotated_input<'a>(x: &'a i32) {
- println!("`annotated_input`: {}", x)
- }
-
- // 类似地,`elided_pass` 和 `annotated_pass` 也拥有相同的签名,
- // 生命周期会被隐式地添加进 `elided_pass`:
- fn elided_pass(x: &i32) -> &i32 { x }
-
- fn annotated_pass<'a>(x: &'a i32) -> &'a i32 { x }
-
- fn main() {
- let x = 3;
-
- elided_input(&x);
- annotated_input(&x);
-
- println!("`elided_pass`: {}", elided_pass(&x));
- println!("`annotated_pass`: {}", annotated_pass(&x));
- }
参考:省略
- fn main() {
- enum IpAddr {
- V4(String),
- V6(String),
- }
-
- let home = IpAddr::V4(String::from("127.0.0.1"));
- let loopback = IpAddr::V6(String::from("::1"));
-
- match home {
- IpAddr::V4(ip) => println!("Home IPv4 地址是: {}", ip),
- IpAddr::V6(ip) => println!("Home IPv6 地址是: {}", ip),
- }
-
- match loopback {
- IpAddr::V4(ip) => println!("Loopback IPv4 地址是: {}", ip),
- IpAddr::V6(ip) => println!("Loopback IPv6 地址是: {}", ip),
- }
- }
Option
类型提供了一种表示可能存在或不存在的值的方式。Option<T>
是一个枚举类型,它有两个变体:Some(T)
表示存在一个值,None
表示不存在值。Option
类型还提供了一些方法来处理包含值的情况。其中之一是 Some
函数,它被用于将一个值封装在 Some
变体中。
- fn get_name() -> Option<String> {
- let name = "Alice".to_string();
- Some(name)
- }
-
- fn main() {
- let name_option = get_name();
-
- match name_option {
- Some(name) => println!("Name: {}", name),
- None => println!("No name found"),
- }
- }
在 Rust 中,Result
是一个枚举类型,它代表了可能产生错误的操作的结果。Result
枚举有两个变体:Ok
和 Err
。
Ok
变体表示操作成功,并包含操作返回的值。Err
变体表示操作失败,并包含一个错误值,用于描述错误的原因。通常,Result
类型被用于表示可能会发生错误的函数的返回类型。这样,调用者可以通过检查 Result
来处理操作的成功或失败。简单的示例,演示如何使用 Result
:
- fn divide(a: i32, b: i32) -> Result<i32, String> {
- if b == 0 {
- return Err(String::from("除数不能为零"));
- }
-
- Ok(a / b)
- }
-
- fn main() {
- let result = divide(10, 2);
-
- match result {
- Ok(value) => println!("结果是: {}", value),
- Err(error) => println!("出现错误: {}", error),
- }
- }
示例 2:
- use std::io;
- use std::io::stdin;
-
- fn main() {
- let mut input_str = String::from("");
- stdin().read_line(&mut input_str).expect("获取输入失败");
- let input_int:usize = match input_str.trim().parse() {
- Ok(n) => n,
- Err(_) => {
- println!("无效的输入");
- return;
- }
- };
- let result = input_int * 100;
- println!("{result}")
- }
Rust 的 "Ok、Err" 宏。这些宏用于将一个值包装在 Ok 或 Err 变体中,并返回相应的 Result 类型
- fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
- if b == 0 {
- Err("除数不能为零")
- } else {
- Ok(a / b)
- }
- }
-
- fn main() {
- // let ret_val = divide(10, 2);
- let ret_val = divide(10, 0);
- let result = match ret_val {
- Ok(v) => v,
- Err(e) => {
- println!("{}", e);
- return; // 添加 return 语句以结束程序
- }
- };
- println!("结果: {}", result);
- }
读文件 示例:
- #![allow(unused)] //禁止编译器对未使用的变量进行检查
-
- use std::io;
- use std::fs;
-
- fn main() {
- fn read_username_from_file() -> Result<String, io::Error> {
- fs::read_to_string("d:/hello.txt")
- }
-
- let ret_val = read_username_from_file();
- let result = match ret_val {
- Ok(v) => v,
- Err(e) => {
- println!("{}", e);
- return;
- }
- };
- println!("{result}");
- }
match
关键字来提供模式匹配,和 C 语言的 switch
用法类似。第一个匹配分支会被比对,并且所有可能的值都必须被覆盖。匹配模式 可由字面值、变量、通配符和许多其他内容构成。Option<T>
时,是为了从 Some
中取出其内部的 T
值;Option<T>
有两个变量:- fn main() {
- fn plus_one(x: Option<i32>) -> Option<i32> {
- match x {
- None => None,
- Some(i) => Some(i + 1),
- }
- }
-
- let five = Some(5);
- let six = plus_one(five);
- let none = plus_one(None);
-
- // 使用模式匹配
- match six {
- Some(value) => println!("Some 值是: {}", value),
- None => println!("None"),
- }
-
- // 使用 unwrap() 方法
- if let Some(value) = five {
- println!("Some 值是: {}", value);
- } else {
- println!("None");
- }
-
- if let Some(value) = six {
- println!("Some 值是: {}", value);
- } else {
- println!("None");
- }
- }
- fn main() {
- let number = 13;
- // 试一试 ^ 将不同的值赋给 `number`
-
- println!("Tell me about {}", number);
- match number {
- // 匹配单个值
- 1 => println!("One!"),
- // 匹配多个值
- 2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
- // 试一试 ^ 将 13 添加到质数列表中
- // 匹配一个闭区间范围
- 13..=19 => println!("A teen"),
- // 处理其他情况
- _ => println!("Ain't special"),
- // 试一试 ^ 注释掉这个总括性的分支
- }
-
- let boolean = true;
- // match 也是一个表达式
- let binary = match boolean {
- // match 分支必须覆盖所有可能的值
- false => 0,
- true => 1,
- // 试一试 ^ 将其中一条分支注释掉
- };
-
- println!("{} -> {}", boolean, binary);
- }
模式(Patterns)是 Rust 中特殊的语法,它用来匹配类型中的结构,无论类型是简单还是复杂。结合使用模式和 match
表达式以及其他结构可以提供更多对程序控制流的支配权。模式由如下一些内容组合而成:
一些模式的例子包括x
, (a, 3)
和 Some(Color::Red)
。在模式为有效的上下文中,这些部分描述了数据的形状。接着可以用其匹配值来决定程序是否拥有正确的数据来运行特定部分的代码。
一个模式常用的位置是 match
表达式的分支。
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
示例:匹配变量 x
中 Option<i32>
值的 match
表达式:
- fn main() {
- let x = Option::Some(5);
- let result = match x {
- None => None,
- Some(i) => Some(i + 1),
- };
- println!("{:?}", result);
- }
match
表达式必须是 穷尽(exhaustive)的,意为 match
表达式所有可能的值都必须被考虑到。
有一个特定的模式 _
可以匹配所有情况,不过它从不绑定任何变量。“忽略模式中的值” 详细介绍 _
模式
元组可以在 match
中解构。参考:元组
- fn main() {
- let triple = (0, -2, 3);
- // 试一试 ^ 将不同的值赋给 `triple`
-
- println!("Tell me about {:?}", triple);
- // match 可以解构一个元组
- match triple {
- // 解构出第二个和第三个元素
- (0, y, z) => println!("First is `0`, `y` is {:?}, and `z` is {:?}", y, z),
- (1, ..) => println!("First is `1` and the rest doesn't matter"),
- // `..` 可用来忽略元组的其余部分
- _ => println!("It doesn't matter what they are"),
- // `_` 表示不将值绑定到变量
- }
- }
解构 enum。参考:
#[allow(...)], 色彩模型 和 enum
- // 需要 `allow` 来消除警告,因为只使用了枚举类型的一种取值。
- #[allow(dead_code)]
- enum Color {
- // 这三个取值仅由它们的名字(而非类型)来指定。
- Red,
- Blue,
- Green,
- // 这些则把 `u32` 元组赋予不同的名字,以色彩模型命名。
- RGB(u32, u32, u32),
- HSV(u32, u32, u32),
- HSL(u32, u32, u32),
- CMY(u32, u32, u32),
- CMYK(u32, u32, u32, u32),
- }
-
- fn main() {
- let color = Color::RGB(122, 17, 40);
- // 试一试 ^ 将不同的值赋给 `color`
-
- println!("What color is it?");
- // 可以使用 `match` 来解构 `enum`。
- match color {
- Color::Red => println!("The color is Red!"),
- Color::Blue => println!("The color is Blue!"),
- Color::Green => println!("The color is Green!"),
- Color::RGB(r, g, b) =>
- println!("Red: {}, green: {}, and blue: {}!", r, g, b),
- Color::HSV(h, s, v) =>
- println!("Hue: {}, saturation: {}, value: {}!", h, s, v),
- Color::HSL(h, s, l) =>
- println!("Hue: {}, saturation: {}, lightness: {}!", h, s, l),
- Color::CMY(c, m, y) =>
- println!("Cyan: {}, magenta: {}, yellow: {}!", c, m, y),
- Color::CMYK(c, m, y, k) =>
- println!("Cyan: {}, magenta: {}, yellow: {}, key (black): {}!",
- c, m, y, k),
- // 不需要其它分支,因为所有的情形都已覆盖
- }
- }
解构 指针和引用。
对指针来说,解构(destructure)和解引用(dereference)要区分开,因为这两者的概念是不同的,和 C
那样的语言用法不一样。参考:ref 模式
*
&
、ref
、和 ref mut
- fn main() {
- // 获得一个 `i32` 类型的引用。`&` 表示取引用。
- let reference = &4;
-
- match reference {
- // 如果用 `&val` 这个模式去匹配 `reference`,就相当于做这样的比较:
- // `&i32`(译注:即 `reference` 的类型)
- // `&val`(译注:即用于匹配的模式)
- // ^ 我们看到,如果去掉匹配的 `&`,`i32` 应当赋给 `val`。
- // 译注:因此可用 `val` 表示被 `reference` 引用的值 4。
- &val => println!("Got a value via destructuring: {:?}", val),
- }
-
- // 如果不想用 `&`,需要在匹配前解引用。
- match *reference {
- val => println!("Got a value via dereferencing: {:?}", val),
- }
-
- // 如果一开始就不用引用,会怎样? `reference` 是一个 `&` 类型,因为赋值语句
- // 的右边已经是一个引用。但下面这个不是引用,因为右边不是。
- let _not_a_reference = 3;
-
- // Rust 对这种情况提供了 `ref`。它更改了赋值行为,从而可以对具体值创建引用。
- // 下面这行将得到一个引用。
- let ref _is_a_reference = 3;
-
- // 相应地,定义两个非引用的变量,通过 `ref` 和 `ref mut` 仍可取得其引用。
- let value = 5;
- let mut mut_value = 6;
-
- // 使用 `ref` 关键字来创建引用。
- // 译注:下面的 r 是 `&i32` 类型,它像 `i32` 一样可以直接打印,因此用法上
- // 似乎看不出什么区别。但读者可以把 `println!` 中的 `r` 改成 `*r`,仍然能
- // 正常运行。前面例子中的 `println!` 里就不能是 `*val`,因为不能对整数解
- // 引用。
- match value {
- ref r => println!("Got a reference to a value: {:?}", r),
- }
-
- // 类似地使用 `ref mut`。
- match mut_value {
- ref mut m => {
- // 已经获得了 `mut_value` 的引用,先要解引用,才能改变它的值。
- *m += 10;
- println!("We added 10. `mut_value`: {:?}", m);
- },
- }
- }
- fn main() {
- struct Foo { x: (u32, u32), y: u32 }
-
- // 解构结构体的成员
- let foo = Foo { x: (1, 2), y: 3 };
- let Foo { x: (a, b), y } = foo;
-
- println!("a = {}, b = {}, y = {} ", a, b, y);
-
- // 可以解构结构体并重命名变量,成员顺序并不重要
-
- let Foo { y: i, x: j } = foo;
- println!("i = {:?}, j = {:?}", i, j);
-
- // 也可以忽略某些变量
- let Foo { y, .. } = foo;
- println!("y = {}", y);
-
- // 这将得到一个错误:模式中没有提及 `x` 字段
- // let Foo { y } = foo;
- }
加上 match
卫语句(guard) 来过滤分支。就是 match
分支模式之后的额外 if
条件,它也必须被满足才能选择此分支。
- fn main() {
- let pair = (2, -2);
- // 试一试 ^ 将不同的值赋给 `pair`
-
- println!("Tell me about {:?}", pair);
- match pair {
- (x, y) if x == y => println!("These are twins"),
- // ^ `if` 条件部分是一个卫语句
- (x, y) if x + y == 0 => println!("Antimatter, kaboom!"),
- (x, _) if x % 2 == 1 => println!("The first one is odd"),
- _ => println!("No correlation..."),
- }
- }
- 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 = {y}", x);
-
- let x = 4;
- let y = false;
- match x {
- 4 | 5 | 6 if y => println!("yes"),
- _ => println!("no"),
- }
- }
在 match
中,若间接地访问一个变量,则不经过重新绑定就无法在分支中再使用它。match
提供了 @
符号来绑定变量到名称:at 运算符(@
)可以在一个模式中同时测试和保存变量值。
- fn main() {
- enum Message {
- Hello { id: i32 },
- }
-
- 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),
- }
-
- }
- // `age` 函数,返回一个 `u32` 值。
- fn age() -> u32 {
- 15
- }
-
- fn main() {
- println!("Tell me what type of person you are");
-
- match age() {
- 0 => println!("I haven't celebrated my first birthday yet"),
- // 可以直接匹配(`match`) 1 ..= 12,但那样的话孩子会是几岁?
- // 相反,在 1 ..= 12 分支中绑定匹配值到 `n` 。现在年龄就可以读取了。
- n @ 1 ..= 12 => println!("I'm a child of age {:?}", n),
- n @ 13 ..= 19 => println!("I'm a teen of age {:?}", n),
- // 不符合上面的范围。返回结果。
- n => println!("I'm an old person of age {:?}", n),
- }
- }
也可以使用绑定来“解构” enum
变体,例如 Option
:
- fn some_number() -> Option<u32> {
- Some(42)
- }
-
- fn main() {
- match some_number() {
- // 得到 `Some` 可变类型,如果它的值(绑定到 `n` 上)等于 42,则匹配。
- Some(n @ 42) => println!("The Answer: {}!", n),
- // 匹配任意其他数字。
- Some(n) => println!("Not interesting... {}", n),
- // 匹配任意其他值(`None` 可变类型)。
- _ => (),
- }
- }
在一些场合下,用 match
匹配枚举类型并不优雅。比如:
-
- #![allow(unused)]
- fn main() {
- // 将 `optional` 定为 `Option<i32>` 类型
- let optional = Some(7);
-
- match optional {
- Some(i) => {
- println!("This is a really long string and `{:?}`", i);
- // ^ 行首需要 2 层缩进。这里从 optional 中解构出 `i`。
- // 译注:正确的缩进是好的,但并不是 “不缩进就不能运行” 这个意思。
- },
- _ => {},
- // ^ 必须有,因为 `match` 需要覆盖全部情况。不觉得这行很多余吗?
- };
-
- }
if let
在这样的场合要简洁得多,可以只匹配关心的分支。还可以组合并匹配 if let
、else if
和 else if let
表达式。
示例:
- fn main() {
- let favorite_color: Option<&str> = None;
- let is_tuesday = false;
- let age: Result<u8, _> = "34".parse();
-
- if let Some(color) = favorite_color {
- println!("Using your favorite color, {color}, as the background");
- } else if is_tuesday {
- println!("Tuesday is green day!");
- } else if let Ok(age) = age {
- if age > 30 {
- println!("Using purple as the background color");
- } else {
- println!("Using orange as the background color");
- }
- } else {
- println!("Using blue as the background color");
- }
- }
- fn main() {
- // 全部都是 `Option<i32>` 类型
- let number = Some(7);
- let letter: Option<i32> = None;
- let emoticon: Option<i32> = None;
-
- // `if let` 结构读作:若 `let` 将 `number` 解构成 `Some(i)`,则执行
- // 语句块(`{}`)
- if let Some(i) = number {
- println!("Matched {:?}!", i);
- }
-
- // 如果要指明失败情形,就使用 else:
- if let Some(i) = letter {
- println!("Matched {:?}!", i);
- } else {
- // 解构失败。切换到失败情形。
- println!("Didn't match a number. Let's go with a letter!");
- };
-
- // 提供另一种失败情况下的条件。
- let i_like_letters = false;
-
- if let Some(i) = emoticon {
- println!("Matched {:?}!", i);
- // 解构失败。使用 `else if` 来判断是否满足上面提供的条件。
- } else if i_like_letters {
- println!("Didn't match a number. Let's go with a letter!");
- } else {
- // 条件的值为 false。于是以下是默认的分支:
- println!("I don't like letters. Let's go with an emoticon :)!");
- };
- }
if let
匹配任何枚举值:
- // 以这个 enum 类型为例
- enum Foo {
- Bar,
- Baz,
- Qux(u32)
- }
-
- fn main() {
- // 创建变量
- let a = Foo::Bar;
- let b = Foo::Baz;
- let c = Foo::Qux(100);
-
- // 变量 a 匹配到了 Foo::Bar
- if let Foo::Bar = a {
- println!("a is foobar");
- }
-
- // 变量 b 没有匹配到 Foo::Bar,因此什么也不会打印。
- if let Foo::Bar = b {
- println!("b is foobar");
- }
-
- // 变量 c 匹配到了 Foo::Qux,它带有一个值,就和上面例子中的 Some() 类似。
- if let Foo::Qux(value) = c {
- println!("c is {}", value);
- }
- }
与 if let
结构类似的是 while let
条件循环,它允许只要模式匹配就一直进行 while
循环。
- fn main() {
- let mut stack = Vec::new();
-
- stack.push(1);
- stack.push(2);
- stack.push(3);
-
- while let Some(top) = stack.pop() {
- println!("{}", top);
- }
-
- }
会打印出 3、2 接着是 1。pop
方法取出 vector 的最后一个元素并返回 Some(value)
。如果 vector 是空的,它返回 None
。while
循环只要 pop
返回 Some
就会一直运行其块中的代码。一旦其返回 None
,while
循环停止。我们可以使用 while let
来弹出栈中的每一个元素。
和 if let
类似,while let
也可以把别扭的 match
改写得好看一些。考虑下面这段使 i
不断增加的代码:
-
- #![allow(unused)]
- fn main() {
- // 将 `optional` 设为 `Option<i32>` 类型
- let mut optional = Some(0);
-
- // 重复运行这个测试。
- loop {
- match optional {
- // 如果 `optional` 解构成功,就执行下面语句块。
- Some(i) => {
- if i > 9 {
- println!("Greater than 9, quit!");
- optional = None;
- } else {
- println!("`i` is `{:?}`. Try again.", i);
- optional = Some(i + 1);
- }
- // ^ 需要三层缩进!
- },
- // 当解构失败时退出循环:
- _ => { break; }
- // ^ 为什么必须写这样的语句呢?肯定有更优雅的处理方式!
- }
- }
- }
使用 while let
可以使这段代码变得更加优雅:
- fn main() {
- // 将 `optional` 设为 `Option<i32>` 类型
- let mut optional = Some(0);
-
- // 这读作:当 `let` 将 `optional` 解构成 `Some(i)` 时,就
- // 执行语句块(`{}`)。否则就 `break`。
- while let Some(i) = optional {
- if i > 9 {
- println!("Greater than 9, quit!");
- optional = None;
- } else {
- println!("`i` is `{:?}`. Try again.", i);
- optional = Some(i + 1);
- }
- // ^ 使用的缩进更少,并且不用显式地处理失败情况。
- }
- // ^ `if let` 有可选的 `else`/`else if` 分句,
- // 而 `while let` 没有。
- }
- fn main() {
- let v = vec!['a', 'b', 'c'];
-
- for (index, value) in v.iter().enumerate() {
- println!("{} is at index {}", value, index);
- }
-
- }
使用 enumerate
方法适配一个迭代器来产生一个值和其在迭代器中的索引,它们位于一个元组中。第一个产生的值是元组 (0, 'a')
。当这个值匹配模式 (index, value)
,index
将会是 0 而 value
将会是 'a'
,并打印出第一行输出。
函数参数也可以是模式。
- fn foo(x: i32) {
- // code goes here
- }
-
- fn print_coordinates(&(x, y): &(i32, i32)) {
- println!("Current location: ({}, {})", x, y);
- }
-
- fn main() {
- let point = (3, 5);
- print_coordinates(&point);
- }
模式有两种形式:
if let Some(x) = a_value
表达式中的 Some(x)
;如果变量 a_value
中的值是 None
而不是 Some
,那么 Some(x)
模式不能匹配。let x = 5;
语句中的 x
,因为 x
可以匹配任何值所以不可能会失败。不可反驳模式的地方使用可反驳模式,直接报错。示例:let Some(x) = some_option_value;
为了修复在需要不可反驳模式的地方使用可反驳模式的情况,可以修改使用模式的代码:不同于使用 let
,可以使用 if let
。如此,如果模式不匹配,大括号中的代码将被忽略,其余代码保持有效。示例
if let Some(x) = some_option_value {
println!("{}", x);
}
匹配字面值
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
匹配命名变量
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 = {y}", x);
多个模式。在 match 表达式中,可以使用 | 语法匹配多个模式,它代表 或(or)运算符模式。
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
通过 ..= 匹配值的范围, 允许你匹配一个闭区间范围内的值。
let x = 5;match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
let x = 'c';match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
解构并分解值。使用模式来解构结构体、枚举和元组,以便使用这些值的不同部分。
// 解构结构体
struct Point {
x: i32,
y: i32,
}fn main() {
let p = Point { x: 0, y: 7 };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})");
}
}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);
}
解构枚举
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 {x} and in the y direction {y}");
}
Message::Write(text) => {
println!("Text message: {text}");
}
Message::ChangeColor(r, g, b) => {
println!("Change the color to red {r}, green {g}, and blue {b}",)
}
}
}
解构嵌套的结构体和枚举
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 color to red {r}, green {g}, and blue {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to hue {h}, saturation {s}, value {v}")
}
_ => (),
}
}
// 解构结构体和元组
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
有时忽略模式中的一些值是有用的,比如 match
中最后捕获全部情况的分支实际上没有做任何事,但是它确实对所有剩余情况负责。
使用
_
模式fn foo(_: i32, y: i32) { // 完全忽略作为第一个参数传递的值 3
println!("This code 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 numbers = (2, 4, 8, 16, 32);
- match numbers {
- (first, _, third, _, fifth) => {
- println!("Some numbers: {first}, {third}, {fifth}")
- }
- }
-
- }
在名字前以一个 _ 开头来忽略未使用的变量。如果你创建了一个变量却不在任何地方使用它,Rust 通常会给你一个警告,因为未使用的变量可能会是个 bug。但是有时创建一个还未使用的变量是有用的,比如你正在设计原型或刚刚开始一个项目。这时你希望告诉 Rust 不要警告未使用的变量,为此可以用下划线作为变量名的开头。
- fn main() {
- let _x = 5;
- let y = 10;
- }
- fn main() {
- let s = Some(String::from("Hello!"));
-
- //###################################
- // if let Some(_s) = s {
- // println!("found a string");
- // }
- // println!("{:?}", s); // 这里报错,因为s所有权被移动到了_s
- //###################################
- if let Some(_) = s {
- println!("found a string");
- }
- println!("{:?}", s); // 不报错,因为没有把s绑定到任何变量;它没有被移动。
- }
使用 ..
表示 "剩余未使用的部分"
- struct Point {
- x: i32,
- y: i32,
- z: i32,
- }
-
- fn main() {
- let origin = Point { x: 0, y: 0, z: 0 };
-
- match origin {
- Point { x, .. } => println!("x is {}", x),
- }
-
- }
这里用 first
和 last
来匹配第一个和最后一个值。..
将匹配并忽略中间的所有值。
- fn main() {
- let numbers = (2, 4, 8, 16, 32);
-
- match numbers {
- (first, .., last) => {
- println!("Some numbers: {first}, {last}");
- }
- }
- }
使用 ..
必须是无歧义的。如果期望匹配和忽略的值是不明确的,Rust 会报错。示例:
- fn main() {
- let numbers = (2, 4, 8, 16, 32);
-
- match numbers {
- (.., second, ..) => {
- println!("Some numbers: {}", second)
- },
- }
- }
Rust 不可能决定在元组中匹配 second
值之前应该忽略多少个值,以及在之后忽略多少个值。这段代码可能表明我们意在忽略 2
,绑定 second
为 4
,接着忽略 8
、16
和 32
;抑或是意在忽略 2
和 4
,绑定 second
为 8
,接着忽略 16
和 32
,以此类推。变量名 second
对于 Rust 来说并没有任何特殊意义,所以会得到编译错误,因为在这两个地方使用 ..
是有歧义的。
fn
关键字来声明。->
之后指定。一个 "不" 返回值的函数。实际上会返回一个单元类型 `()`。当函数返回 `()` 时,函数签名可以省略返回类型self
来访问对象中的数据和其他。方法在 impl
代码块中定义。- struct Point {
- x: f64,
- y: f64,
- }
-
- // 实现的代码块,`Point` 的所有方法都在这里给出
- impl Point {
- // 这是一个静态方法(static method)
- // 静态方法不需要被实例调用
- // 这类方法一般用作构造器(constructor)
- fn origin() -> Point {
- Point { x: 0.0, y: 0.0 }
- }
-
- // 另外一个静态方法,需要两个参数:
- fn new(x: f64, y: f64) -> Point {
- Point { x: x, y: y }
- }
- }
-
- struct Rectangle {
- p1: Point,
- p2: Point,
- }
-
- impl Rectangle {
- // 这是一个实例方法(instance method)
- // `&self` 是 `self: &Self` 的语法糖(sugar),其中 `Self` 是方法调用者的
- // 类型。在这个例子中 `Self` = `Rectangle`
- fn area(&self) -> f64 {
- // `self` 通过点运算符来访问结构体字段
- let Point { x: x1, y: y1 } = self.p1;
- let Point { x: x2, y: y2 } = self.p2;
-
- // `abs` 是一个 `f64` 类型的方法,返回调用者的绝对值
- ((x1 - x2) * (y1 - y2)).abs()
- }
-
- fn perimeter(&self) -> f64 {
- let Point { x: x1, y: y1 } = self.p1;
- let Point { x: x2, y: y2 } = self.p2;
-
- 2.0 * ((x1 - x2).abs() + (y1 - y2).abs())
- }
-
- // 这个方法要求调用者是可变的
- // `&mut self` 为 `self: &mut Self` 的语法糖
- fn translate(&mut self, x: f64, y: f64) {
- self.p1.x += x;
- self.p2.x += x;
-
- self.p1.y += y;
- self.p2.y += y;
- }
- }
-
- // `Pair` 拥有资源:两个堆分配的整型
- struct Pair(Box<i32>, Box<i32>);
-
- impl Pair {
- // 这个方法会 “消耗” 调用者的资源
- // `self` 为 `self: Self` 的语法糖
- fn destroy(self) {
- // 解构 `self`
- let Pair(first, second) = self;
-
- println!("Destroying Pair({}, {})", first, second);
-
- // `first` 和 `second` 离开作用域后释放
- }
- }
-
- fn main() {
- let rectangle = Rectangle {
- // 静态方法使用双冒号调用
- p1: Point::origin(),
- p2: Point::new(3.0, 4.0),
- };
-
- // 实例方法通过点运算符来调用
- // 注意第一个参数 `&self` 是隐式传递的,亦即:
- // `rectangle.perimeter()` === `Rectangle::perimeter(&rectangle)`
- println!("Rectangle perimeter: {}", rectangle.perimeter());
- println!("Rectangle area: {}", rectangle.area());
-
- let mut square = Rectangle {
- p1: Point::origin(),
- p2: Point::new(1.0, 1.0),
- };
-
- // 报错! `rectangle` 是不可变的,但这方法需要一个可变对象
- //rectangle.translate(1.0, 0.0);
- // 试一试 ^ 去掉此行的注释
-
- // 正常运行!可变对象可以调用可变方法
- square.translate(1.0, 1.0);
-
- let pair = Pair(Box::new(1), Box::new(2));
-
- pair.destroy();
-
- // 报错!前面的 `destroy` 调用 “消耗了” `pair`
- //pair.destroy();
- // 试一试 ^ 将此行注释去掉
- }
Rust 中的闭包(closure),也叫做 lambda 表达式或者 lambda,是一类能够捕获周围作用域中变量的函数。例如,一个可以捕获 x 变量的闭包如下:|val| val + x
调用一个闭包和调用一个函数完全相同,不过调用闭包时,输入和返回类型两者都可以自动推导,而输入变量名必须指明。
其他的特点包括:
||
替代 ()
将输入参数括起来。{}
)对于单个表达式是可选的,其他情况必须加上。- fn main() {
- // 通过闭包和函数分别实现自增。
- // 译注:下面这行是使用函数的实现
- fn function(i: i32) -> i32 { i + 1 }
-
- // 闭包是匿名的,这里我们将它们绑定到引用。
- // 类型标注和函数的一样,不过类型标注和使用 `{}` 来围住函数体都是可选的。
- // 这些匿名函数(nameless function)被赋值给合适地命名的变量。
- let closure_annotated = |i: i32| -> i32 { i + 1 };
- let closure_inferred = |i| i + 1;
-
- // 译注:将闭包绑定到引用的说法可能不准。
- // 据[语言参考](https://doc.rust-lang.org/beta/reference/types.html#closure-types)
- // 闭包表达式产生的类型就是 “闭包类型”,不属于引用类型,而且确实无法对上面两个
- // `closure_xxx` 变量解引用。
-
- let i = 1;
- // 调用函数和闭包。
- println!("function: {}", function(i));
- println!("closure_annotated: {}", closure_annotated(i));
- println!("closure_inferred: {}", closure_inferred(i));
-
- // 没有参数的闭包,返回一个 `i32` 类型。
- // 返回类型是自动推导的。
- let one = || 1;
- println!("closure returning one: {}", one());
- }
闭包语法与函数语法有很多相似:
fn add_one_v1 (x: u32) -> u32 { x + 1 } // 个函数定义
let add_one_v2 = |x: u32| -> u32 { x + 1 }; // 完整标注的闭包定义
let add_one_v3 = |x| { x + 1 }; // 闭包定义中省略了类型注解
let add_one_v4 = |x| x + 1 ; // 闭包体只有一个表达式, 去掉可选的大括号
闭包 从周围的作用域中捕获变量,可以通过三种方式 :
&T
不可变借用&mut T
可变借用T
获取所有权直接对应到函数获取参数的三种方式:"不可变借用,可变借用、获取所有权"。
闭包优先通过引用来捕获变量,并且仅在需要时使用其他方式。
- fn main() {
- use std::mem;
-
- let color = String::from("green");
-
- // 这个闭包打印 `color`。它会立即借用(通过引用,`&`)`color` 并将该借用和
- // 闭包本身存储到 `print` 变量中。`color` 会一直保持被借用状态直到
- // `print` 离开作用域。
- //
- // `println!` 只需传引用就能使用,而这个闭包捕获的也是变量的引用,因此无需
- // 进一步处理就可以使用 `println!`。
- let print = || println!("`color`: {}", color);
-
- // 使用借用来调用闭包 `color`。
- print();
-
- // `color` 可再次被不可变借用,因为闭包只持有一个指向 `color` 的不可变引用。
- let _reborrow = &color;
- print();
-
- // 在最后使用 `print` 之后,移动或重新借用都是允许的。
- let _color_moved = color;
-
- let mut count = 0;
- // 这个闭包使 `count` 值增加。要做到这点,它需要得到 `&mut count` 或者
- // `count` 本身,但 `&mut count` 的要求没那么严格,所以我们采取这种方式。
- // 该闭包立即借用 `count`。
- //
- // `inc` 前面需要加上 `mut`,因为闭包里存储着一个 `&mut` 变量。调用闭包时,
- // 该变量的变化就意味着闭包内部发生了变化。因此闭包需要是可变的。
- let mut inc = || {
- count += 1;
- println!("`count`: {}", count);
- };
-
- // 使用可变借用调用闭包
- inc();
-
- // 因为之后调用闭包,所以仍然可变借用 `count`
- // 试图重新借用将导致错误
- // let _reborrow = &count;
- // ^ 试一试:将此行注释去掉。
- inc();
-
- // 闭包不再借用 `&mut count`,因此可以正确地重新借用
- let _count_reborrowed = &mut count;
-
- // 不可复制类型(non-copy type)。
- let movable = Box::new(3);
-
- // `mem::drop` 要求 `T` 类型本身,所以闭包将会捕获变量的值。这种情况下,
- // 可复制类型将会复制给闭包,从而原始值不受影响。不可复制类型必须移动
- // (move)到闭包中,因而 `movable` 变量在这里立即移动到了闭包中。
- let consume = || {
- println!("`movable`: {:?}", movable);
- mem::drop(movable);
- };
-
- // `consume` 消耗了该变量,所以该闭包只能调用一次。
- consume();
- //consume();
- // ^ 试一试:将此行注释去掉。
- }
定义并调用一个捕获不可变引用的闭包
- fn main() {
- let list = vec![1, 2, 3];
- println!("Before defining closure: {:?}", list);
-
- let only_borrows = || println!("From closure: {:?}", list);
-
- println!("Before calling closure: {:?}", list);
- only_borrows();
- println!("After calling closure: {:?}", list);
- }
定义并调用一个捕获可变引用的闭包
- fn main() {
- let mut list = vec![1, 2, 3];
- println!("Before defining closure: {:?}", list);
-
- let mut borrows_mutably = || list.push(7);
-
- borrows_mutably();
- println!("After calling closure: {:?}", list);
- }
在竖线 |
之前使用 move
会强制闭包取得被捕获变量的所有权。
- use std::thread;
-
- fn main() {
- let list = vec![1, 2, 3];
- println!("Before defining closure: {:?}", list);
-
- thread::spawn(move || println!("From thread: {:?}", list))
- .join()
- .unwrap();
- }
在闭包定义前写上 move
关键字来指明 list
应当被移动到闭包中。新线程可能在主线程剩余部分执行完前执行完,或者也可能主线程先执行完。如果主线程维护了 list
的所有权但却在新线程之前结束并且丢弃了 list
,则在线程中的不可变引用将失效。因此,编译器要求 list
被移动到在新线程中运行的闭包中,这样引用就是有效的。试着去掉 move
关键字或在闭包被定义后在主线程中使用 list
看看你会得到什么编译器报错!
闭包 作为 "函数输入参数、输出返回值、类型匿名"
虽然 Rust 无需类型说明就能在大多数时候完成变量捕获,但在编写函数时,这种模糊写法是不允许的。当以闭包作为输入参数时,必须指出闭包的完整类型,它是通过使用以下 trait
中的一种来指定的。其受限制程度按以下顺序递减:
Fn
:表示捕获方式为通过引用(&T
)的闭包FnMut
:表示捕获方式为通过可变引用(&mut T
)的闭包FnOnce
:表示捕获方式为通过值(T
)的闭包Fn
、FnMut
和 FnOnce
这些 trait
明确了闭包如何从周围的作用域中捕获变量。对闭包所要捕获的每个变量,是在满足使用需求的前提下尽量以限制最多的方式捕获。这是因为如果能以移动的方式捕获变量,则闭包也有能力使用其他方式借用变量。注意反过来就不再成立:如果参数的类型说明是 Fn
,那么不允许该闭包通过 &mut T
或 T
捕获变量。
示例:
- // 该函数将闭包作为参数并调用它。
- fn apply<F>(f: F) where
- // 闭包没有输入值和返回值。
- F: FnOnce() {
- // ^ 试一试:将 `FnOnce` 换成 `Fn` 或 `FnMut`。
-
- f();
- }
-
- // 输入闭包,返回一个 `i32` 整型的函数。
- fn apply_to_3<F>(f: F) -> i32 where
- // 闭包处理一个 `i32` 整型并返回一个 `i32` 整型。
- F: Fn(i32) -> i32 {
-
- f(3)
- }
-
- fn main() {
- use std::mem;
-
- let greeting = "hello";
- // 不可复制的类型。
- // `to_owned` 从借用的数据创建有所有权的数据。
- let mut farewell = "goodbye".to_owned();
-
- // 捕获 2 个变量:通过引用捕获 `greeting`,通过值捕获 `farewell`。
- let diary = || {
- // `greeting` 通过引用捕获,故需要闭包是 `Fn`。
- println!("I said {}.", greeting);
-
- // 下文改变了 `farewell` ,因而要求闭包通过可变引用来捕获它。
- // 现在需要 `FnMut`。
- farewell.push_str("!!!");
- println!("Then I screamed {}.", farewell);
- println!("Now I can sleep. zzzzz");
-
- // 手动调用 drop 又要求闭包通过值获取 `farewell`。
- // 现在需要 `FnOnce`。
- mem::drop(farewell);
- };
-
- // 以闭包作为参数,调用函数 `apply`。
- apply(diary);
-
- // 闭包 `double` 满足 `apply_to_3` 的 trait 约束。
- let double = |x| 2 * x;
-
- println!("3 doubled: {}", apply_to_3(double));
- }
使用闭包作为函数参数,这要求闭包是泛型的,闭包定义的方式决定了这是必要的。
-
- #![allow(unused)]
- fn main() {
- // `F` 必须是泛型的。
- fn apply<F>(f: F) where
- F: FnOnce() {
- f();
- }
- }
当闭包被定义,编译器会隐式地创建一个匿名类型的结构体,用以储存闭包捕获的变量,同时为这个未知类型的结构体实现函数功能,通过 Fn
、FnMut
或 FnOnce
三种 trait
中的一种。
若使用闭包作为函数参数,由于这个结构体的类型未知,任何的用法都要求是泛型的。然而,使用未限定类型的参数 <T>
过于不明确,并且是不允许的。事实上,指明为该结构体实现的是 Fn
、FnMut
、或 FnOnce
中的哪种 trait
,对于约束该结构体的类型而言就已经足够了。
- // `F` 必须为一个没有输入参数和返回值的闭包实现 `Fn`,这和对 `print` 的
- // 要求恰好一样。
- fn apply<F>(f: F) where
- F: Fn() {
- f();
- }
-
- fn main() {
- let x = 7;
-
- // 捕获 `x` 到匿名类型中,并为它实现 `Fn`。
- // 将闭包存储到 `print` 中。
- let print = || println!("{}", x);
-
- apply(print);
- }
函数作为参数 (必须满足输入限定)
既然闭包可以作为参数,你很可能想知道函数是否也可以呢。确实可以!如果你声明一个接受闭包作为参数的函数,那么任何满足该闭包的 trait 约束的函数都可以作为其参数。
- // 定义一个函数,可以接受一个由 `Fn` 限定的泛型 `F` 参数并调用它。
- fn call_me<F: Fn()>(f: F) {
- f()
- }
-
- // 定义一个满足 `Fn` 约束的封装函数(wrapper function)。
- fn function() {
- println!("I'm a function!");
- }
-
- fn main() {
- // 定义一个满足 `Fn` 约束的闭包。
- let closure = || println!("I'm a closure!");
-
- call_me(closure);
- call_me(function);
- }
闭包作为输入参数是可能的,所以返回闭包作为输出参数(output parameter)也应该是可能的。然而返回闭包类型会有问题,因为目前 Rust 只支持返回具体(非泛型)的类型。按照定义,匿名的闭包的类型是未知的,所以只有使用impl Trait
才能返回一个闭包。
返回闭包的有效特征是:
Fn
FnMut
FnOnce
除此之外,还必须使用 move
关键字,它表明所有的捕获都是通过值进行的。这是必须的,因为在函数退出时,任何通过引用的捕获都被丢弃,在闭包中留下无效的引用。
- fn create_fn() -> impl Fn() {
- let text = "Fn".to_owned();
-
- move || println!("This is a: {}", text)
- }
-
- fn create_fnmut() -> impl FnMut() {
- let text = "FnMut".to_owned();
-
- move || println!("This is a: {}", text)
- }
-
- fn create_fnonce() -> impl FnOnce() {
- let text = "FnOnce".to_owned();
-
- move || println!("This is a: {}", text)
- }
-
- fn main() {
- let fn_plain = create_fn();
- let mut fn_mut = create_fnmut();
- let fn_once = create_fnonce();
-
- fn_plain();
- fn_mut();
- fn_once();
- }
-
Iterator::any
Iterator::any
是一个函数,若传给它一个迭代器(iterator),当其中任一元素满足谓词(predicate)时它将返回 true
,否则返回 false
(译注:谓词是闭包规定的, true
/false
是闭包作用在元素上的返回值)。它的签名如下:
- pub trait Iterator {
- // 被迭代的类型。
- type Item;
-
- // `any` 接受 `&mut self` 参数(译注:回想一下,这是 `self: &mut Self` 的简写)
- // 表明函数的调用者可以被借用和修改,但不会被消耗。
- fn any<F>(&mut self, f: F) -> bool where
- // `FnMut` 表示被捕获的变量最多只能被修改,而不能被消耗。
- // `Self::Item` 表明变量是通过值传递给闭包(译注:是迭代器对应的元素的类型)
- F: FnMut(Self::Item) -> bool {}
- }
- fn main() {
- let vec1 = vec![1, 2, 3];
- let vec2 = vec![4, 5, 6];
-
- // 对 vec 的 `iter()` 举出 `&i32`。(通过用 `&x` 匹配)把它解构成 `i32`。
- // 译注:注意 `any` 方法会自动地把 `vec.iter()` 举出的迭代器的元素一个个地
- // 传给闭包。因此闭包接收到的参数是 `&i32` 类型的。
- println!("2 in vec1: {}", vec1.iter() .any(|&x| x == 2));
- // 对 vec 的 `into_iter()` 举出 `i32` 类型。无需解构。
- println!("2 in vec2: {}", vec2.into_iter().any(| x| x == 2));
-
- let array1 = [1, 2, 3];
- let array2 = [4, 5, 6];
-
- // 对数组的 `iter()` 举出 `&i32`。
- println!("2 in array1: {}", array1.iter() .any(|&x| x == 2));
- // 对数组的 `into_iter()` 举出 `i32`。
- println!("2 in array2: {}", array2.into_iter().any(|x| x == 2));
- }
Iterator::find
Iterator::find
是一个函数,在传给它一个迭代器时,将用 Option
类型返回第一个满足谓词的元素。它的签名如下:
- pub trait Iterator {
- // 被迭代的类型。
- type Item;
-
- // `find` 接受 `&mut self` 参数,表明函数的调用者可以被借用和修改,
- // 但不会被消耗。
- fn find<P>(&mut self, predicate: P) -> Option<Self::Item> where
- // `FnMut` 表示被捕获的变量最多只能被修改,而不能被消耗。
- // `&Self::Item` 指明了被捕获变量的类型(译注:是对迭代器元素的引用类型)
- P: FnMut(&Self::Item) -> bool {}
- }
- fn main() {
- let vec1 = vec![1, 2, 3];
- let vec2 = vec![4, 5, 6];
-
- // 对 vec1 的 `iter()` 举出 `&i32` 类型。
- let mut iter = vec1.iter();
- // 对 vec2 的 `into_iter()` 举出 `i32` 类型。
- let mut into_iter = vec2.into_iter();
-
- // 对迭代器举出的元素的引用是 `&&i32` 类型。解构成 `i32` 类型。
- // 译注:注意 `find` 方法会把迭代器元素的引用传给闭包。迭代器元素自身
- // 是 `&i32` 类型,所以传给闭包的是 `&&i32` 类型。
- println!("Find 2 in vec1: {:?}", iter .find(|&&x| x == 2));
- // 对迭代器举出的元素的引用是 `&i32` 类型。解构成 `i32` 类型。
- println!("Find 2 in vec2: {:?}", into_iter.find(| &x| x == 2));
-
- let array1 = [1, 2, 3];
- let array2 = [4, 5, 6];
-
- // 对数组的 `iter()` 举出 `&i32`。
- println!("Find 2 in array1: {:?}", array1.iter() .find(|&&x| x == 2));
- // 对数组的 `into_iter()` 通常举出 `&i32``。
- println!("Find 2 in array2: {:?}", array2.into_iter().find(|&x| x == 2));
- }
Rust 提供了高阶函数(Higher Order Function, HOF),指那些输入一个或多个函数,并且/或者产生一个更有用的函数的函数。HOF 和惰性迭代器(lazy iterator)给 Rust 带来了函数式(functional)编程的风格。
- fn is_odd(n: u32) -> bool {
- n % 2 == 1
- }
-
- fn main() {
- println!("Find the sum of all the squared odd numbers under 1000");
- let upper = 1000;
-
- // 命令式(imperative)的写法
- // 声明累加器变量
- let mut acc = 0;
- // 迭代:0,1, 2, ... 到无穷大
- for n in 0.. {
- // 数字的平方
- let n_squared = n * n;
-
- if n_squared >= upper {
- // 若大于上限则退出循环
- break;
- } else if is_odd(n_squared) {
- // 如果是奇数就计数
- acc += n_squared;
- }
- }
- println!("imperative style: {}", acc);
-
- // 函数式的写法
- let sum_of_squared_odd_numbers: u32 =
- (0..).map(|n| n * n) // 所有自然数取平方
- .take_while(|&n| n < upper) // 取小于上限的
- .filter(|&n| is_odd(n)) // 取奇数
- .fold(0, |sum, i| sum + i); // 最后加起来
- println!("functional style: {}", sum_of_squared_odd_numbers);
- }
Option<T>
上的 unwrap_or_else
方法的定义:
- impl<T> Option<T> {
- pub fn unwrap_or_else<F>(self, f: F) -> T
- where
- F: FnOnce() -> T
- {
- match self {
- Some(x) => x,
- None => f(),
- }
- }
- }
T
是表示 Option
中 Some
成员中的值的类型的泛型。类型 T
也是 unwrap_or_else
函数的返回值类型:举例来说,在 Option<String>
上调用 unwrap_or_else
会得到一个 String
。
接着注意到 unwrap_or_else
函数有额外的泛型参数 F
。 F
是 f
参数(即调用 unwrap_or_else
时提供的闭包)的类型。
泛型 F
的 trait bound 是 FnOnce() -> T
,这意味着 F
必须能够被调用一次,没有参数并返回一个 T
。在 trait bound 中使用 FnOnce
表示 unwrap_or_else
将最多调用 f
一次。在 unwrap_or_else
的函数体中可以看到,如果 Option
是 Some
,f
不会被调用。如果 Option
是 None
,f
将会被调用一次。由于所有的闭包都实现了 FnOnce
,unwrap_or_else
能接收绝大多数不同类型的闭包,十分灵活。
注意:函数也可以实现所有的三种
Fn
traits。如果我们要做的事情不需要从环境中捕获值,则可以在需要某种实现了Fn
trait 的东西时使用函数而不是闭包。举个例子,可以在Option<Vec<T>>
的值上调用unwrap_or_else(Vec::new)
以便在值为None
时获取一个新的空的 vector。
std 中闭包示例:迭代器(iterator)
负责遍历序列中的每一项和决定序列何时结束的逻辑。迭代器是 惰性的(lazy),这意味着在调用方法使用迭代器之前它都不会有效果。
iter
方法生成一个不可变引用的迭代器。如果希望迭代可变引用,则调用 iter_mut
into_iter
方法可以获取所有权并返回拥有所有权的迭代器。示例:
- fn main() {
- let v1 = vec![1, 2, 3];
- let v1_iter = v1.iter();
- for val in v1_iter {
- println!("Got: {}", val);
- }
- }
- fn main() {
- let v1 = vec![1, 2, 3];
- let v1_iter = v1.iter();
- let total: i32 = v1_iter.sum();
- assert_eq!(total, 6);
- }
sum 方法方法获取迭代器的所有权并反复调用 next 来遍历迭代器,因而会消费迭代器。当其遍历每一个项时,它将每一个项加总到一个总和并在迭代完成时返回总和。调用 sum
之后不再允许使用 v1_iter
因为调用 sum
时它会获取迭代器的所有权。
- fn main() {
- let v1: Vec<i32> = vec![1, 2, 3];
- let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
- assert_eq!(v2, vec![2, 3, 4]);
- }
map
获取一个闭包,可以指定任何希望在遍历的每个元素上执行的操作。这是一个展示如何使用闭包来自定义行为同时又复用 Iterator
trait 提供的迭代行为的绝佳例子。
发散函数(diverging function)被称为 "绝不会返回(never type)" 。 它们使用 !
标记,这是一个空类型(empty type),因为它没有值。在函数从不返回的时候充当返回值。例如:这读 "函数 bar
从不返回",不能创建 !
类型的值,所以 bar
也不可能返回值。
- fn bar() -> ! {
- // --snip--
- }
和所有其他类型相反,这个类型无法实例化,因为此类型可能具有的所有可能值的集合为空。 注意,它与 ()
类型不同,后者只有一个可能的值。
虽然返回值中没有信息,但此函数会照常返回。
- fn some_fn() {
- ()
- }
-
- fn main() {
- let a: () = some_fn();
- println!("This function returns and you can see this line.")
- }
下面这个函数相反,这个函数永远不会将控制内容返回给调用者。
- #![feature(never_type)]
-
- fn main() {
- let x: ! = panic!("This call never returns.");
- println!("You will never see this line!");
- }
这种类型的主要优点是它可以被转换为任何其他类型,从而可以在需要精确类型的地方使用,例如在 match
匹配分支。
- fn main() {
- fn sum_odd_numbers(up_to: u32) -> u32 {
- let mut acc = 0;
- for i in 0..up_to {
- // 注意这个 match 表达式的返回值必须为 u32,
- // 因为 “addition” 变量是这个类型。
- let addition: u32 = match i%2 == 1 {
- // “i” 变量的类型为 u32,这毫无问题。
- true => i,
- // 另一方面,“continue” 表达式不返回 u32,但它仍然没有问题,
- // 因为它永远不会返回,因此不会违反匹配表达式的类型要求。
- false => continue,
- };
- acc += addition;
- }
- acc
- }
- println!("Sum of odd numbers up to 9 (excluding): {}", sum_odd_numbers(9));
- }
Rust 提供了一套强大的模块(module)系统,可以将代码按层次分成多个逻辑单元(模块),并管理这些模块之间的可见性(公有(public)或私有(private))。
模块是 "项(item)" 的集合,项可以是:函数,结构体,trait,impl
块,甚至其它模块。
//创建一个快捷方式,然后在作用域中只写Asparagus来使用该类型。
use crate::garden::vegetables::Asparagus;
// 将 `deeply::nested::function` 路径绑定到 `other_function`。
use deeply::nested::function as other_function;
模块的路径有两种形式:
crate
开头。self
、super
或当前模块的标识符开头。- fn function() {
- println!("called `function()`");
- }
-
- mod cool {
- pub fn function() {
- println!("called `cool::function()`");
- }
- }
-
- mod my {
- fn function() {
- println!("called `my::function()`");
- }
-
- mod cool {
- pub fn function() {
- println!("called `my::cool::function()`");
- }
- }
-
- pub fn indirect_call() {
- // 让我们从这个作用域中访问所有名为 `function` 的函数!
- print!("called `my::indirect_call()`, that\n> ");
-
- // `self` 关键字表示当前的模块作用域——在这个例子是 `my`。
- // 调用 `self::function()` 和直接调用 `function()` 都得到相同的结果,
- // 因为他们表示相同的函数。
- self::function();
- function();
-
- // 我们也可以使用 `self` 来访问 `my` 内部的另一个模块:
- self::cool::function();
-
- // `super` 关键字表示父作用域(在 `my` 模块外面)。
- super::function();
-
- // 这将在 *crate* 作用域内绑定 `cool::function` 。
- // 在这个例子中,crate 作用域是最外面的作用域。
- {
- use crate::cool::function as root_function;
- root_function();
- }
- }
- }
-
- fn main() {
- my::indirect_call();
- }
绝对路径和相对路径都后跟一个或多个由双冒号(::
)分割的标识符。
mod garden
声明了一个叫做garden
的模块。编译器会在下列路径中寻找模块代码:
mod garden
后方不是一个分号而是一个大括号mod vegetables;
。编译器会在以父模块命名的目录中寻找子模块代码:
mod vegetables
后方不是一个分号而是一个大括号Asparagus
类型可以在crate::garden::vegetables::Asparagus
被找到。模块可以分配到文件/目录的层次结构中。
split.rs 的内容:
- // 此声明将会查找名为 `my.rs` 或 `my/mod.rs` 的文件,并将该文件的内容放到
- // 此作用域中一个名为 `my` 的模块里面。
- mod my;
-
- fn function() {
- println!("called `function()`");
- }
-
- fn main() {
- my::function();
-
- function();
-
- my::indirect_access();
-
- my::nested::function();
- }
my/mod.rs 的内容:
- // 类似地,`mod inaccessible` 和 `mod nested` 将找到 `nested.rs` 和
- // `inaccessible.rs` 文件,并在它们放到各自的模块中。
- mod inaccessible;
- pub mod nested;
-
- pub fn function() {
- println!("called `my::function()`");
- }
-
- fn private_function() {
- println!("called `my::private_function()`");
- }
-
- pub fn indirect_access() {
- print!("called `my::indirect_access()`, that\n> ");
-
- private_function();
- }
my/nested.rs 的内容:
- pub fn function() {
- println!("called `my::nested::function()`");
- }
-
- #[allow(dead_code)]
- fn private_function() {
- println!("called `my::nested::private_function()`");
- }
my/inaccessible.rs 的内容:
- #[allow(dead_code)]
- pub fn public_function() {
- println!("called `my::inaccessible::public_function()`");
- }
编译运行
$ rustc split.rs && ./split
called `my::function()`
called `function()`
called `my::indirect_access()`, that
> called `my::private_function()`
called `my::nested::function()`
rustc
而不是 cargo
来编译some_file.rs
时,some_file.rs
被当作 crate 文件。如果 some_file.rs
里面含有 mod
声明,那么模块文件的内容将在编译之前被插入 crate 文件的相应声明处。换句话说,模块不会单独被编译,只有 crate 才会被编译。crate 可以编译成二进制可执行文件(binary)或库文件(library)。默认情况下,rustc
将从 crate 产生二进制可执行文件。这种行为可以通过 rustc
的选项 --crate-type
重载。创建一个库
- pub fn public_function() {
- println!("called rary's `public_function()`");
- }
-
- fn private_function() {
- println!("called rary's `private_function()`");
- }
-
- pub fn indirect_access() {
- print!("called rary's `indirect_access()`, that\n> ");
-
- private_function();
- }
编译:
$ rustc --crate-type=lib rary.rs
$ ls lib*
library.rlib
默认情况下,库会使用 crate 文件的名字,前面加上 “lib” 前缀,但这个默认名称可以使用 crate_name 属性 覆盖。
使用库,可以使用 rustc
的 --extern
选项
- // extern crate rary; // 在 Rust 2015 版或更早版本需要这个导入语句
-
- fn main() {
- rary::public_function();
-
- // 报错! `private_function` 是私有的
- //rary::private_function();
-
- rary::indirect_access();
- }
# library.rlib 是已编译好的库的路径,这里假设它在同一目录下:
$ rustc executable.rs --extern rary=library.rlib --edition=2018 && ./executable
called rary's `public_function()`
called rary's `indirect_access()`, that
> called rary's `private_function()`
cargo
是官方的 Rust 包管理工具。cargo
还支持更多功能,如基准测试,测试和示例
功能包括:
介绍一些快速入门的基础知识,在 cargo 官方手册中找到详细内容。
创建一个新的 Rust 项目:
# 二进制可执行文件
cargo new foo# 或者库
cargo new --lib foo
main.rs
是新项目的入口源文件——这里没什么新东西。 Cargo.toml
是本项目(foo
)的 cargo
的配置文件。 浏览 Cargo.toml
文件,将看到类似以下的的内容:
package 节点
dependencies 节点是为项目添加 第三方包(模块) 依赖。
cargo
还支持其他类型的依赖。 下面是一个简单的示例:
在项目目录中的任何位置(包括子目录!)执行 cargo build 都可以下载依赖并构建项目。cargo run
是构建和运行
测试。可以将单元测试放在需要测试的模块中,并将集成测试放在源码中 tests/
目录中:
tests 目录下的每个文件都是一个单独的集成测试。cargo 很自然地提供了一种便捷的方法来运行所有测试:cargo test
还可以运行如下测试,其中名称匹配一个模式:cargo test test_foo
需要注意:cargo
可能同时进行多项测试,因此请确保它们不会相互竞争。例如,如果它们都输出到文件,则应该将它们写入不同的文件。
编译之前执行一些先决代码。比如代码生成或者需要编译的一些本地代码。为了解决这个问题,我们构建了 cargo 可以运行的脚本。要向包中添加构建脚本,可以在 Cargo.toml
中指定它,如下所示:
这里 cargo 将在项目目录中优先查找 build.rs
文件。构建脚本只是另一个 Rust 文件,此文件将在编译包中的任何其他内容之前,优先进行编译和调用。 因此,此文件可实现满足 crate 的先决条件。
属性是应用于某些模块、crate 或项的元数据(metadata)。这元数据可以用来:
当属性作用于整个 crate 时,它们的语法为 #![crate_attribute]
,当它们用于模块或项时,语法为 #[item_attribute]
(注意少了感叹号 !
)。
属性可以接受参数,有不同的语法形式:
#[attribute = "value"]
#[attribute(key = "value")]
#[attribute(value)]
属性可以多个值,它们可以分开到多行中:
#[attribute(value, value2)]
#[attribute(value, value2, value3,
value4, value5)]
#[derive(Debug)]
是一个属性(attribute),用于自动生成调试输出所需的 Debug
trait 的实现。通过添加 #[derive(Debug)]
属性,你可以方便地为结构体或枚举类型派生 Debug
trait,从而在调试过程中打印出该类型的值。
- #[derive(Debug)] // 添加 #[derive(Debug)] 属性
- #[allow(dead_code)]
- struct MyStruct {
- field1: i32,
- field2: String,
- }
-
- fn main() {
- let my_struct = MyStruct {
- field1: 42,
- field2: String::from("Hello, World!"),
- };
-
- println!("{:?}", my_struct); // 打印结构体的调试输出
- }
编译器提供了 dead_code
(死代码,无效代码)lint,这会对未使用的函数产生警告。可以用一个属性来禁用这个 lint。
- fn used_function() {}
-
- // `#[allow(dead_code)]` 属性可以禁用 `dead_code` lint
- #[allow(dead_code)]
- fn unused_function() {}
-
- fn noisy_unused_function() {}
- // 改正 ^ 增加一个属性来消除警告
-
- fn main() {
- used_function();
- }
条件编译可能通过两种不同的操作符实现:
cfg
属性:在属性位置中使用 #[cfg(...)]
cfg!
宏:在布尔表达式中使用 cfg!(...)
两种形式使用的参数语法都相同。
- // 这个函数仅当目标系统是 Linux 的时候才会编译
- #[cfg(target_os = "linux")]
- fn are_you_on_linux() {
- println!("You are running linux!")
- }
-
- // 而这个函数仅当目标系统 **不是** Linux 时才会编译
- #[cfg(not(target_os = "linux"))]
- fn are_you_on_linux() {
- println!("You are *not* running linux!")
- }
-
- fn main() {
- are_you_on_linux();
-
- println!("Are you sure?");
- if cfg!(target_os = "linux") {
- println!("Yes. It's definitely linux!");
- } else {
- println!("Yes. It's definitely *not* linux!");
- }
- }
在 Rust 中有多种处理错误的方式,使用场景也不尽相同。总的来说:
panic
主要用于测试,以及处理不可恢复的错误。当执行 panic!宏 时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。Option
类型是为了值是可选的、或者缺少值并不是错误的情况准备的。当处理 Option
时,unwrap
可用于原型开发,也可以用于能够确定 Option
中一定有值 的情形。然而 expect
更有用,因为它允许你指定一条错误信息,以免万一还是出现 了错误。Result
用于当错误有可能发生,且应当由调用者处理。也可以 unwrap
然后 使用 expect
有关错误处理的更多内容,可参考官方文档的错误处理的章节。
最简单的错误处理机制就是 panic
。它会打印一个错误消息,开始回退(unwind)任务且通常会退出程序。在回退栈的同时,运行时将会释放该线程所拥有的所有资源,这是通过调用线程中所有对象的析构函数完成的。
- fn give_princess(gift: &str) {
- // 公主讨厌蛇,所以如果公主表示厌恶的话我们要停止!
- if gift == "snake" { panic!("AAAaaaaa!!!!"); }
-
- println!("I love {}s!!!!!", gift);
- }
-
- fn main() {
- give_princess("teddy bear");
- give_princess("snake");
- }
公主收到蛇这件不合适的礼物时,程序直接 panic。但是,如果公主期待收到礼物,却没收到呢?
可以检查空字符串(""),这就是下面的 Option
标准库(std
)中有个叫做 Option<T>
(option 中文意思是 “选项”)的枚举类型,用于有 “不存在” 的可能性的情况。它表现为以下两个 “option”(选项)中的一个:
Some(T)
:找到一个属于 T
类型的元素None
:找不到相应元素这些选项可以通过 match
显式地处理,或使用 unwrap
隐式地处理。隐式处理要么返回 Some
内部的元素,要么就 panic
。
- // 平民(commoner)们见多识广,收到什么礼物都能应对。
- // 所有礼物都显式地使用 `match` 来处理。
- fn give_commoner(gift: Option<&str>) {
- // 指出每种情况下的做法。
- match gift {
- Some("snake") => println!("Yuck! I'm throwing that snake in a fire."),
- Some(inner) => println!("{}? How nice.", inner),
- None => println!("No gift? Oh well."),
- }
- }
-
- // 养在深闺人未识的公主见到蛇就会 `panic`(恐慌)。
- // 这里所有的礼物都使用 `unwrap` 隐式地处理。
- fn give_princess(gift: Option<&str>) {
- // `unwrap` 在接收到 `None` 时将返回 `panic`。
- let inside = gift.unwrap();
- if inside == "snake" { panic!("AAAaaaaa!!!!"); }
-
- println!("I love {}s!!!!!", inside);
- }
-
- fn main() {
- let food = Some("chicken");
- let snake = Some("snake");
- let void = None;
-
- give_commoner(food);
- give_commoner(snake);
- give_commoner(void);
-
- let bird = Some("robin");
- let nothing = None;
-
- give_princess(bird);
- give_princess(nothing);
- }
unwrap
和 expect
是用于从 Result
或 Option
类型中提取值的方法。unwrap
方法会尝试将一个 Result
或 Option
对象中的值提取出来。如果对象是一个 Ok
或 Some
,则 unwrap
方法返回该值;否则,它会触发 panic 异常并终止程序的执行。expect
用来在触发了 panic 时并打印出提供的错误消息。
- fn func_1() {
- let x: Result<i32, &str> = Ok(42);
- let val = x.unwrap(); // 正常情况下,val 的值为 42
-
- let y: Result<i32, &str> = Err("error message");
- let val = y.unwrap(); // 这里会触发 panic
- println!("测试函数 func_1");
- }
-
- fn func_2() {
- let x: Result<i32, &str> = Ok(42);
- let val = x.expect("提取值失败"); // 正常情况下,val 的值为 42
-
- let y: Result<i32, &str> = Err("error message");
- let val = y.expect("提取值失败"); // 这里会触发 panic,并打印出 "提取值失败"
- println!("测试函数 func_2");
- }
-
- fn main() {
- // func_1();
- func_2();
- }
示例
- use std::fs::File;
- use std::io::ErrorKind;
-
- fn main() {
- let greeting_file_result = File::open("hello.txt");
-
- let greeting_file = match greeting_file_result {
- Ok(file) => file,
- Err(error) => match error.kind() {
- ErrorKind::NotFound => match File::create("hello.txt") {
- Ok(fc) => fc,
- Err(e) => panic!("Problem creating the file: {:?}", e),
- },
- other_error => {
- panic!("Problem opening the file: {:?}", other_error);
- }
- },
- };
- }
自己不处理错误并抛出错误,而是让调用者处理错误。这被称为 传播(propagating)错误。因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
示例:
- #![allow(unused)]
- fn main() {
- use std::fs::File;
- use std::io::{self, Read};
-
- 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),
- }
- }
- }
在Rust中,x?
是一种简洁的错误处理和传播机制,通常用于处理 Result
或 Option
类型的值。可用于函数中的返回类型为 Result
或 Option
的情况。
x?
作用是将被包裹的 Result
或 Option
值进行解包,并根据其结果进行处理。
Ok
或 Some
,则将包裹的值返回;Err
或 None
,则将整个表达式的结果设定为 Err
或 None
,并将错误或空值传播到调用方。可以使用 match
语句来解开 Option
,但使用 ?
运算符通常会更容易。对x?
表达式求值将返回底层值,否则无论函数是否正在执行都将终止且返回 None
。
- use rand::distributions::WeightedError::NoItem;
-
- fn next_birthday(current_age: Option<u8>) -> Option<String> {
- // 如果 `current_age` 是 `None`,这将返回 `None`。
- // 如果 `current_age` 是 `Some`,内部的 `u8` 将赋值给 `next_age`。
- let next_age: u8 = current_age?;
- Some(format!("Next year I will be {}", next_age))
- }
- fn main() {
- let ret_val = next_birthday(None);
- if let Some(s) = ret_val {
- println!("{:?}", s);
- } else {
- println!("None");
- }
- }
对 Result<T, E> 使用 x? 表达式 如下:
- #![allow(unused)]
- fn main() {
- 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)
- }
- }
? 问号 运算符 也可以 链式调用
- #![allow(unused)]
- fn main() {
- use std::fs::File;
- use std::io::{self, Read};
-
- 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)
- }
- }
?
运算符只能被用于返回值与?
作用的值相兼容的函数。因为?
运算符被定义为从函数中提早返回一个值。总结:? 运算符 只能在返回Result
或者其它实现了FromResidual
的类型的函数中使用?
运算符。为了修复这个错误,有两个选择。一个是,如果没有限制的话将函数的返回值改为Result<T, E>
。另一个是使用match
或Result<T, E>
的方法中合适的一个来处理Result<T, E>
。?
也可用于Option<T>
值。如同对Result
使用?
一样,只能在返回Option
的函数中对Option
使用?
。在Option<T>
上调用?
运算符的行为与Result<T, E>
类似:如果值是None
,此时None
会从函数中提前返回。如果值是Some
,Some
中的值作为表达式的返回值同时函数继续。use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}代码打开一个文件,这可能会失败。
?
运算符作用于File::open
返回的Result
值,不过main
函数的返回类型是()
而不是Result
。当编译这段代码会报错。
Box<dyn Error>
类型是一个 trait 对象。目前可以将Box<dyn Error>
理解为 “任何类型的错误”。在返回Box<dyn Error>
错误类型main
函数中对Result
使用?
是允许的,因为它允许任何Err
值提前返回。即便main
函数体从来只会返回std::io::Error
错误类型,通过指定Box<dyn Error>
,这个签名也仍是正确的,甚至当main
函数体中增加更多返回其他错误类型的代码时也是如此。use std::error::Error;
use std::fs::File;fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;Ok(())
}
示例
- use rand::Rng;
- use std::cmp::Ordering;
- use std::io;
-
- fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- loop {
- // --snip--
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- 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;
- }
-
- match guess.cmp(&secret_number) {
- // --snip--
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => {
- println!("You win!");
- break;
- }
- }
- }
- }
Option
有一个内置方法 map()
,这个组合算子可用于 Some -> Some
和 None -> None
这样的简单映射。多个不同的 map()
调用可以串起来,这样更加灵活。
参考:闭包, Option, 和 Option::map()
- #![allow(dead_code)]
-
- #[derive(Debug)] enum Food { Apple, Banana }
-
- #[derive(Debug)] struct Peeled(Food);
- #[derive(Debug)] struct Chopped(Food);
- #[derive(Debug)] struct Cooked(Food);
-
- // 削皮。如果没有食物,就返回 `None`。否则返回削好皮的食物。
- fn peel(food: Option<Food>) -> Option<Peeled> {
- match food {
- Some(food) => Some(Peeled(food)),
- None => None,
- }
- }
-
- // 切食物。如果没有食物,就返回 `None`。否则返回切好的食物。
- fn chop(peeled: Option<Peeled>) -> Option<Chopped> {
- match peeled {
- Some(Peeled(food)) => Some(Chopped(food)),
- None => None,
- }
- }
-
- // 烹饪食物。这里,我们使用 `map()` 来替代 `match` 以处理各种情况。
- fn cook(chopped: Option<Chopped>) -> Option<Cooked> {
- chopped.map(|Chopped(food)| Cooked(food))
- }
-
- // 这个函数会完成削皮切块烹饪一条龙。我们把 `map()` 串起来,以简化代码。
- fn process(food: Option<Food>) -> Option<Cooked> {
- food.map(|f| Peeled(f))
- .map(|Peeled(f)| Chopped(f))
- .map(|Chopped(f)| Cooked(f))
- }
-
- // 在尝试吃食物之前确认食物是否存在是非常重要的!
- fn eat(food: Option<Cooked>) {
- match food {
- Some(food) => println!("Mmm. I love {:?}", food),
- None => println!("Oh no! It wasn't edible."),
- }
- }
-
- fn main() {
- let apple = Some(Food::Apple);
- let cooked_apple = cook(chop(peel(apple)));
- eat(cooked_apple);
- println!("######################################################");
- let banana = Some(Food::Banana);
- // 现在让我们试试看起来更简单的 `process()`。
- let cooked_banana = process(banana);
- eat(cooked_banana);
- }
map()
以链式调用的方式来简化 match
语句。然而,如果以返回类型是 Option<T>
的函数作为 map()
的参数,会导致出现嵌套形式 Option<Option<T>>
。这样多层串联调用就会变得混乱。所以有必要引入 and_then()
,在某些语言中它叫做 flatmap。and_then()
使用被 Option
包裹的值来调用其输入函数并返回结果。 如果 Option
是 None
,那么它返回 None
。
- #![allow(dead_code)]
-
- #[derive(Debug)] enum Food { CordonBleu, Steak, Sushi }
- #[derive(Debug)] enum Day { Monday, Tuesday, Wednesday }
-
- // 我们没有制作寿司所需的原材料(ingredient)(有其他的原材料)。
- fn have_ingredients(food: Food) -> Option<Food> {
- match food {
- Food::Sushi => None,
- _ => Some(food),
- }
- }
-
- // 我们拥有全部食物的食谱,除了法国蓝带猪排(Cordon Bleu)的。
- fn have_recipe(food: Food) -> Option<Food> {
- match food {
- Food::CordonBleu => None,
- _ => Some(food),
- }
- }
-
-
- // 要做一份好菜,我们需要原材料和食谱。
- // 我们可以借助一系列 `match` 来表达这个逻辑:
- fn cookable_v1(food: Food) -> Option<Food> {
- match have_ingredients(food) {
- None => None,
- Some(food) => match have_recipe(food) {
- None => None,
- Some(food) => Some(food),
- },
- }
- }
-
- // 也可以使用 `and_then()` 把上面的逻辑改写得更紧凑:
- fn cookable_v2(food: Food) -> Option<Food> {
- have_ingredients(food).and_then(have_recipe)
- }
-
- fn eat(food: Food, day: Day) {
- match cookable_v2(food) {
- Some(food) => println!("Yay! On {:?} we get to eat {:?}.", day, food),
- None => println!("Oh no. We don't get to eat on {:?}?", day),
- }
- }
-
- fn main() {
- let (cordon_bleu, steak, sushi) = (Food::CordonBleu, Food::Steak, Food::Sushi);
-
- eat(cordon_bleu, Day::Monday);
- eat(steak, Day::Tuesday);
- eat(sushi, Day::Wednesday);
- }
and_then()
使用被 Option
包裹的值来调用其输入函数并返回结果。 如果 Option
是 None
,那么它返回 None
。
?
之前被解释为要么 unwrap
,要么 return Err(err)
,这只是在大多数情况下是正确的。?
实际上是指 unwrap
或 return Err(From::from(err))
。由于 From::from
是不同类型之间的转换工具,也就是说,如果在错误可转换成返回类型地方使用 ?
,它将自动转换成返回类型。
调用 parse
后总是立即将错误从标准库的错误 map
(映射)到装箱错误。
.and_then(|s| s.parse::<i32>()
.map_err(|e| e.into())因为这个操作很简单常见,如果有省略写法就好了。遗憾的是
and_then
不够灵活,所以实现不了这样的写法。不过,我们可以使用?
来代替它。
使用 ?
重写之前的例子。重写后,只要为我们的错误类型实现 From::from
,就可以不再使用 map_err
。
- use std::error;
- use std::fmt;
-
- // 为 `Box<error::Error>` 取别名。
- type Result<T> = std::result::Result<T, Box<dyn error::Error>>;
-
- #[derive(Debug)]
- struct EmptyVec;
-
- impl fmt::Display for EmptyVec {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "invalid first item to double")
- }
- }
-
- impl error::Error for EmptyVec {}
-
- // 这里的结构和之前一样,但是这次没有把所有的 `Result` 和 `Option` 串起来,
- // 而是使用 `?` 立即得到内部值。
- fn double_first(vec: Vec<&str>) -> Result<i32> {
- let first = vec.first().ok_or(EmptyVec)?;
- let parsed = first.parse::<i32>()?;
- Ok(2 * parsed)
- }
-
- fn print(result: Result<i32>) {
- match result {
- Ok(n) => println!("The first doubled is {}", n),
- Err(e) => println!("Error: {}", e),
- }
- }
-
- fn main() {
- let numbers = vec!["42", "93", "18"];
- let empty = vec![];
- let strings = vec!["tofu", "93", "18"];
-
- print(double_first(numbers));
- print(double_first(empty));
- print(double_first(strings));
- }
参考:From::from 和 ?
对 unwrap
的错误处理都在强迫我们一层层地嵌套,然而我们只是想把里面的变量拿出来。?
正是为这种情况准备的。
当找到一个 Err
时,可以采取两种行动:
panic!
,不过我们已经决定要尽可能避免 panic 了。Err
就意味着它已经不能被处理了。? 几乎就等于一个会返回 Err 而不是 panic 的 unwrap。
更多细节请看? 的更多用法。
- use std::num::ParseIntError;
-
- fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
- let first_number = first_number_str.parse::<i32>()?;
- let second_number = second_number_str.parse::<i32>()?;
-
- Ok(first_number * second_number)
- }
-
- fn print(result: Result<i32, ParseIntError>) {
- match result {
- Ok(n) => println!("n is {}", n),
- Err(e) => println!("Error: {}", e),
- }
- }
-
- fn main() {
- print(multiply("10", "2"));
- print(multiply("t", "2"));
- }
在 ?
出现以前,相同的功能是使用 try!
宏完成的。现在推荐使用 ?
运算符,但是在老代码中仍然会看到 try!
。
查阅文档,里面有很多匹配/组合 Result
的方法。
Result<T,E>
可以有两个结果的其中一个:
Ok<T>
:找到 T
元素,并包装操作返回的 value
(value
拥有 T
类型)。Err<E>
:找到 E
元素,E
即表示错误的类型。按照约定,预期结果是 “Ok”,而意外结果是 “Err”。
Result
有很多类似 Option
的方法。例如 unwrap()
,它要么举出元素 T
,要么就 panic
。
- fn multiply(first_number_str: &str, second_number_str: &str) -> i32 {
- // 我们试着用 `unwrap()` 把数字放出来。它会咬我们一口吗?
- let first_number = first_number_str.parse::<i32>().unwrap();
- let second_number = second_number_str.parse::<i32>().unwrap();
- first_number * second_number
- }
-
- fn main() {
- let twenty = multiply("10", "2");
- println!("double is {}", twenty);
-
- let tt = multiply("t", "2");
- println!("double is {}", tt);
- }
在失败的情况下,parse()
产生一个错误,留给 unwrap()
来解包并产生 panic
。另外,panic
会退出我们的程序,并提供一个让人很不爽的错误消息。
为了改善错误消息的质量,我们应该更具体地了解返回类型并考虑显式地处理错误。
为了确定 Err
的类型,我们可以用 parse() 来试验。Rust 已经为 i32 类型使用 FromStr trait 实现了 parse()
。结果表明,这里的 Err
类型被指定为 ParseIntError。
如何确定 Err
的类型,可以用间接的方法。代码如下:
- fn main () {
- let i: () = "t".parse::<i32>();
- }
由于不可能把 Result
类型赋给单元类型变量 i
,编译器会提示我们:
note: expected type `()`
found type `std::result::Result<i32, std::num::ParseIntError>`
这样就知道了 parse<i32>
函数的返回类型详情。
- // use std::num::ParseIntError;
- //
- // // 修改了上一节中的返回类型,现在使用模式匹配而不是 `unwrap()`。
- // fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
- // match first_number_str.parse::<i32>() {
- // Ok(first_number) => {
- // match second_number_str.parse::<i32>() {
- // Ok(second_number) => {
- // Ok(first_number * second_number)
- // },
- // Err(e) => Err(e),
- // }
- // },
- // Err(e) => Err(e),
- // }
- // }
- //
- // fn print(result: Result<i32, ParseIntError>) {
- // match result {
- // Ok(n) => println!("n is {}", n),
- // Err(e) => println!("Error: {}", e),
- // }
- // }
- //
- // fn main() {
- // // 这种情形下仍然会给出正确的答案。
- // let twenty = multiply("10", "2");
- // print(twenty);
- //
- // // 这种情况下就会提供一条更有用的错误信息。
- // let tt = multiply("t", "2");
- // print(tt);
- // }
-
- use std::num::ParseIntError;
-
- // 就像 `Option` 那样,我们可以使用 `map()` 之类的组合算子。
- // 除去写法外,这个函数与上面那个完全一致,它的作用是:
- // 如果值是合法的,计算其乘积,否则返回错误。
- fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
- first_number_str.parse::<i32>().and_then(|first_number| {
- second_number_str.parse::<i32>().map(|second_number| first_number * second_number)
- })
- }
-
- fn print(result: Result<i32, ParseIntError>) {
- match result {
- Ok(n) => println!("n is {}", n),
- Err(e) => println!("Error: {}", e),
- }
- }
-
- fn main() {
- // 这种情况下仍然会给出正确的答案。
- let twenty = multiply("10", "2");
- print(twenty);
-
- // 这种情况下就会提供一条更有用的错误信息。
- let tt = multiply("t", "2");
- print(tt);
- }
- fn main() {
- let strings = vec!["tofu", "93", "18"];
- let numbers: Vec<_> = strings
- .into_iter()
- .map(|s| s.parse::<i32>())
- .collect();
- println!("Results: {:?}", numbers);
- }
使用 filter_map() 忽略失败的项。filter_map 会调用一个函数,过滤掉为 None 的所有结果。
- fn main() {
- let strings = vec!["tofu", "93", "18"];
- let numbers: Vec<_> = strings
- .into_iter()
- .filter_map(|s| s.parse::<i32>().ok())
- .collect();
- println!("Results: {:?}", numbers);
- }
使用 collect() 使整个操作失败。Result 实现了 FromIter,因此结果的向量(Vec<Result<T, E>>)可以被转换成结果包裹着向量(Result<Vec<T>, E>)。一旦找到一个 Result::Err ,遍历就被终止。
- fn main() {
- let strings = vec!["tofu", "93", "18"];
- let numbers: Result<Vec<_>, _> = strings
- .into_iter()
- .map(|s| s.parse::<i32>())
- .collect();
- println!("Results: {:?}", numbers);
- }
同样的技巧可以对 Option
使用。
使用 Partition() 收集所有合法的值与错误
- fn main() {
- let strings = vec!["tofu", "93", "18"];
- let (numbers, errors): (Vec<_>, Vec<_>) = strings
- .into_iter()
- .map(|s| s.parse::<i32>())
- .partition(Result::is_ok);
- println!("Numbers: {:?}", numbers);
- println!("Errors: {:?}", errors);
- }
会发现所有东西还在 Result
中保存着。要取出它们,需要一些模板化的代码。
- fn main() {
- let strings = vec!["tofu", "93", "18"];
- let (numbers, errors): (Vec<_>, Vec<_>) = strings
- .into_iter()
- .map(|s| s.parse::<i32>())
- .partition(Result::is_ok);
- let numbers: Vec<_> = numbers.into_iter().map(Result::unwrap).collect();
- let errors: Vec<_> = errors.into_iter().map(Result::unwrap_err).collect();
- println!("Numbers: {:?}", numbers);
- println!("Errors: {:?}", errors);
- }
另一种处理错误的方式是使用 match
语句和提前返回(early return)的结合。也就是说,如果发生错误,我们可以停止函数的执行然后返回错误。
- use std::num::ParseIntError;
-
- fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
- let first_number = match first_number_str.parse::<i32>() {
- Ok(first_number) => first_number,
- Err(e) => return Err(e),
- };
-
- let second_number = match second_number_str.parse::<i32>() {
- Ok(second_number) => second_number,
- Err(e) => return Err(e),
- };
-
- Ok(first_number * second_number)
- }
-
- fn print(result: Result<i32, ParseIntError>) {
- match result {
- Ok(n) => println!("n is {}", n),
- Err(e) => println!("Error: {}", e),
- }
- }
-
- fn main() {
- print(multiply("10", "2"));
- print(multiply("t", "2"));
- }
有时 Option
需要和 Result
进行交互,或是 Result<T, Error1>
需要和 Result<T, Error2>
进行交互。在这类情况下,我们想要以一种方式来管理不同的错误类型,使得它们可组合且易于交互。
示例:unwrap
的两个实例生成了不同的错误类型。Vec::first
返回一个 Option
,而 parse::<i32>
返回一个 Result<i32, ParseIntError>
:
示例:
- fn double_first(vec: Vec<&str>) -> i32 {
- let first = vec.first().unwrap(); // 生成错误 1
- 2 * first.parse::<i32>().unwrap() // 生成错误 2
- }
-
- fn main() {
- let numbers = vec!["42", "93", "18"];
- let empty = vec![];
- let strings = vec!["tofu", "93", "18"];
-
- println!("The first doubled is {}", double_first(numbers));
-
- println!("The first doubled is {}", double_first(empty));
- // 错误1:输入 vector 为空
-
- println!("The first doubled is {}", double_first(strings));
- // 错误2:此元素不能解析成数字
- }
处理混合错误类型的最基本的手段就是让它们互相包含。
- use std::num::ParseIntError;
-
- fn double_first(vec: Vec<&str>) -> Option<Result<i32, ParseIntError>> {
- vec.first().map(|first| {
- first.parse::<i32>().map(|n| 2 * n)
- })
- }
-
- fn main() {
- let numbers = vec!["42", "93", "18"];
- let empty = vec![];
- let strings = vec!["tofu", "93", "18"];
-
- println!("The first doubled is {:?}", double_first(numbers));
-
- println!("The first doubled is {:?}", double_first(empty));
- // Error 1: the input vector is empty
-
- println!("The first doubled is {:?}", double_first(strings));
- // Error 2: the element doesn't parse to a number
- }
有时候不想再处理错误(比如使用 ? 的时候),但如果 Option
是 None
则继续处理错误。一些组合算子可以让我们轻松地交换 Result
和 Option
。
- use std::num::ParseIntError;
-
- fn double_first(vec: Vec<&str>) -> Result<Option<i32>, ParseIntError> {
- let opt = vec.first().map(|first| {
- first.parse::<i32>().map(|n| 2 * n)
- });
-
- opt.map_or(Ok(None), |r| r.map(Some))
- }
-
- fn main() {
- let numbers = vec!["42", "93", "18"];
- let empty = vec![];
- let strings = vec!["tofu", "93", "18"];
-
- println!("The first doubled is {:?}", double_first(numbers));
- println!("The first doubled is {:?}", double_first(empty));
- println!("The first doubled is {:?}", double_first(strings));
- }
把所有不同的错误都视为一种错误类型会简化代码。
Rust 允许我们定义自己的错误类型。一般来说,一个 “好的” 错误类型应当:
Err(EmptyVec)
Err("Please use a vector with at least one element".to_owned())
Err(BadChar(c, position))
Err("+ cannot be used here".to_owned())
- use std::error;
- use std::fmt;
-
- type Result<T> = std::result::Result<T, DoubleError>;
-
- #[derive(Debug, Clone)]
- // 定义我们的错误类型,这种类型可以根据错误处理的实际情况定制。
- // 我们可以完全自定义错误类型,也可以在类型中完全采用底层的错误实现,
- // 也可以介于二者之间。
- struct DoubleError;
-
- // 错误的生成与它如何显示是完全没关系的。没有必要担心复杂的逻辑会导致混乱的显示。
- //
- // 注意我们没有储存关于错误的任何额外信息,也就是说,如果不修改我们的错误类型定义的话,
- // 就无法指明是哪个字符串解析失败了。
- impl fmt::Display for DoubleError {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "invalid first item to double")
- }
- }
-
- // 为 `DoubleError` 实现 `Error` trait,这样其他错误可以包裹这个错误类型。
- impl error::Error for DoubleError {
- fn source(&self) -> Option<&(dyn error::Error + 'static)> {
- // 泛型错误,没有记录其内部原因。
- None
- }
- }
-
- fn double_first(vec: Vec<&str>) -> Result<i32> {
- vec.first()
- // 把错误换成我们的新类型。
- .ok_or(DoubleError)
- .and_then(|s| {
- s.parse::<i32>()
- // 这里也换成新类型。
- .map_err(|_| DoubleError)
- .map(|i| 2 * i)
- })
- }
-
- fn print(result: Result<i32>) {
- match result {
- Ok(n) => println!("The first doubled is {}", n),
- Err(e) => println!("Error: {}", e),
- }
- }
-
- fn main() {
- let numbers = vec!["42", "93", "18"];
- let empty = vec![];
- let strings = vec!["tofu", "93", "18"];
-
- print(double_first(numbers));
- print(double_first(empty));
- print(double_first(strings));
- }
如果又想写简单的代码,又想保存原始错误信息,一个方法是把它们装箱(Box
)。这样做的坏处就是,被包装的错误类型只能在运行时了解,而不能被静态地判别。
对任何实现了 Error
trait 的类型,标准库的 Box
通过 From 为它们提供了到 Box<Error>
的转换。
- use std::error;
- use std::fmt;
-
- // 为 `Box<error::Error>` 取别名。
- type Result<T> = std::result::Result<T, Box<dyn error::Error>>;
-
- #[derive(Debug, Clone)]
- struct EmptyVec;
-
- impl fmt::Display for EmptyVec {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "invalid first item to double")
- }
- }
-
- impl error::Error for EmptyVec {
- fn description(&self) -> &str {
- "invalid first item to double"
- }
-
- fn cause(&self) -> Option<&dyn error::Error> {
- // 泛型错误。没有记录其内部原因。
- None
- }
- }
-
- fn double_first(vec: Vec<&str>) -> Result<i32> {
- vec.first()
- .ok_or_else(|| EmptyVec.into()) // 装箱
- .and_then(|s| {
- s.parse::<i32>()
- .map_err(|e| e.into()) // 装箱
- .map(|i| 2 * i)
- })
- }
-
- fn print(result: Result<i32>) {
- match result {
- Ok(n) => println!("The first doubled is {}", n),
- Err(e) => println!("Error: {}", e),
- }
- }
-
- fn main() {
- let numbers = vec!["42", "93", "18"];
- let empty = vec![];
- let strings = vec!["tofu", "93", "18"];
-
- print(double_first(numbers));
- print(double_first(empty));
- print(double_first(strings));
- }
参考:动态分发 and Error trait
把错误装箱这种做法也可以改成把它包裹到你自己的错误类型中。
- use std::error;
- use std::num::ParseIntError;
- use std::fmt;
-
- type Result<T> = std::result::Result<T, DoubleError>;
-
- #[derive(Debug)]
- enum DoubleError {
- EmptyVec,
- // 在这个错误类型中,我们采用 `parse` 的错误类型中 `Err` 部分的实现。
- // 若想提供更多信息,则该类型中还需要加入更多数据。
- Parse(ParseIntError),
- }
-
- impl fmt::Display for DoubleError {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match *self {
- DoubleError::EmptyVec =>
- write!(f, "please use a vector with at least one element"),
- // 这是一个封装(wrapper),它采用内部各类型对 `fmt` 的实现。
- DoubleError::Parse(ref e) => e.fmt(f),
- }
- }
- }
-
- impl error::Error for DoubleError {
- fn source(&self) -> Option<&(dyn error::Error + 'static)> {
- match *self {
- DoubleError::EmptyVec => None,
- // 原因采取内部对错误类型的实现。它隐式地转换成了 trait 对象 `&error:Error`。
- // 这可以工作,因为内部的类型已经实现了 `Error` trait。
- DoubleError::Parse(ref e) => Some(e),
- }
- }
- }
-
- // 实现从 `ParseIntError` 到 `DoubleError` 的转换。
- // 在使用 `?` 时,或者一个 `ParseIntError` 需要转换成 `DoubleError` 时,它会被自动调用。
- impl From<ParseIntError> for DoubleError {
- fn from(err: ParseIntError) -> DoubleError {
- DoubleError::Parse(err)
- }
- }
-
- fn double_first(vec: Vec<&str>) -> Result<i32> {
- let first = vec.first().ok_or(DoubleError::EmptyVec)?;
- let parsed = first.parse::<i32>()?;
-
- Ok(2 * parsed)
- }
-
- fn print(result: Result<i32>) {
- match result {
- Ok(n) => println!("The first doubled is {}", n),
- Err(e) => println!("Error: {}", e),
- }
- }
-
- fn main() {
- let numbers = vec!["42", "93", "18"];
- let empty = vec![];
- let strings = vec!["tofu", "93", "18"];
-
- print(double_first(numbers));
- print(double_first(empty));
- print(double_first(strings));
- }
参考:From::from and 枚举类型
泛型的 类型参数 是使用 尖括号和大驼峰命名 的名称:<Aaa, Bbb, ...> 来指定的。泛型类型参数一般用<T>来表示。使用泛型定义函数时,本来在函数签名中指定参数和返回值的类型的地方,会改用泛型来表示:fn foo<T>(arg: T) { ... } 采用这种技术,使得代码适应性更强,从而为函数的调用者提供更多的功能,同时也避免了代码的重复。
- // 一个具体类型 `A`。
- struct A;
-
- // 在定义类型 `Single` 时,第一次使用类型 `A` 之前没有写 `<A>`。
- // 因此,`Single` 是个具体类型,`A` 取上面的定义。
- struct Single(A);
- // ^ 这里是 `Single` 对类型 `A` 的第一次使用。
-
- // 此处 `<T>` 在第一次使用 `T` 前出现,所以 `SingleGen` 是一个泛型类型。
- // 因为 `T` 是泛型的,所以它可以是任何类型,包括在上面定义的具体类型 `A`。
- struct SingleGen<T>(T);
-
- fn main() {
- // `Single` 是具体类型,并且显式地使用类型 `A`。
- let _s = Single(A);
-
- // 创建一个 `SingleGen<char>` 类型的变量 `_char`,并令其值为 `SingleGen('a')`
- // 这里的 `SingleGen` 的类型参数是显式指定的。
- let _char: SingleGen<char> = SingleGen('a');
-
- // `SingleGen` 的类型参数也可以隐式地指定。
- let _t = SingleGen(A); // 使用在上面定义的 `A`。
- let _i32 = SingleGen(6); // 使用 `i32` 类型。
- let _char = SingleGen('a'); // 使用 `char`。
- }
调用函数时,使用显式指定的类型参数会像是这样:fun::<A, B, ...>()
- struct A; // 具体类型 `A`。
- struct S(A); // 具体类型 `S`。
- struct SGen<T>(T); // 泛型类型 `SGen`。
- // 下面全部函数都得到了变量的所有权,并立即使之离开作用域,将变量释放。
- // 定义一个函数 `reg_fn`,接受一个 `S` 类型的参数 `_s`。
- // 因为没有 `<T>` 这样的泛型类型参数,所以这不是泛型函数。
- fn reg_fn(_s: S) {}
-
- // 定义一个函数 `gen_spec_t`,接受一个 `SGen<A>` 类型的参数 `_s`。
- // `SGen<>` 显式地接受了类型参数 `A`,且在 `gen_spec_t` 中,`A` 没有被用作
- // 泛型类型参数,所以函数不是泛型的。
- fn gen_spec_t(_s: SGen<A>) {}
-
- // 定义一个函数 `gen_spec_i32`,接受一个 `SGen<i32>` 类型的参数 `_s`。
- // `SGen<>` 显式地接受了类型参量 `i32`,而 `i32` 是一个具体类型。
- // 由于 `i32` 不是一个泛型类型,所以这个函数也不是泛型的。
- fn gen_spec_i32(_s: SGen<i32>) {}
-
- // 定义一个函数 `generic`,接受一个 `SGen<T>` 类型的参数 `_s`。
- // 因为 `SGen<T>` 之前有 `<T>`,所以这个函数是关于 `T` 的泛型函数。
- fn generic<T>(_s: SGen<T>) {}
-
- fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
- let mut largest = &list[0];
- for item in list {
- 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);
- println!("###########################################");
- // 使用非泛型函数
- reg_fn(S(A)); // 具体类型。
- gen_spec_t(SGen(A)); // 隐式地指定类型参数 `A`。
- gen_spec_i32(SGen(6)); // 隐式地指定类型参数 `i32`。
- // 为 `generic()` 显式地指定类型参数 `char`。
- generic::<char>(SGen('a'));
- // 为 `generic()` 隐式地指定类型参数 `char`。
- generic(SGen('c'));
- }
- struct Point<T> {
- x: T,
- y: T,
- }
-
- fn main() {
- let integer = Point { x: 5, y: 10 };
- let float = Point { x: 1.0, y: 4.0 };
- }
impl
实现泛型 格式- #![allow(unused)]
- fn main() {
- struct S; // 具体类型 `S`
- struct GenericVal<T>(T,); // 泛型类型 `GenericVal`
-
- // GenericVal 的 `impl`,此处我们显式地指定了类型参数:
- impl GenericVal<f32> {} // 指定 `f32` 类型
- impl GenericVal<S> {} // 指定为上面定义的 `S`
-
- // `<T>` 必须在类型之前写出来,以使类型 `T` 代表泛型。
- impl <T> GenericVal<T> {}
- }
- struct Val {
- val: f64
- }
-
- struct GenVal<T>{
- gen_val: T
- }
-
- // Val 的 `impl`
- impl Val {
- fn value(&self) -> &f64 { &self.val }
- }
-
- // GenVal 的 `impl`,指定 `T` 是泛型类型
- impl <T> GenVal<T> {
- fn value(&self) -> &T { &self.gen_val }
- }
-
- fn main() {
- let x = Val { val: 3.0 };
- let y = GenVal { gen_val: 3i32 };
-
- println!("{}, {}", x.value(), y.value());
- }
可以在定义中使用任意多的泛型类型参数,不过太多的话,代码将难以阅读和理解。当你发现代码中需要很多泛型时,这可能表明你的代码需要重构分解成更小的结构。
修改 Point
的定义为拥有两个泛型类型 T
和 U
。其中字段 x
是 T
类型的,而字段 y
是 U
类型的:
- struct Point1<T, U> {
- x: T,
- y: U,
- }
-
- struct Point2<T1, T2> {
- x: T1,
- y: T2,
- }
-
- fn main() {
- let both_integer = Point { x: 5, y: 10 };
- let both_float = Point { x: 1.0, y: 4.0 };
- let integer_and_float = Point { x: 5, y: 4.0 };
- }
- #![allow(unused)]
- fn main() {
- enum Option<T> {
- Some(T),
- None,
- }
- }
- #![allow(unused)]
- fn main() {
- enum Result<T, E> {
- Ok(T),
- Err(E),
- }
- }
- struct Point<T> {
- x: T,
- y: T,
- }
-
- impl<T> Point<T> {
- fn x(&self) -> &T {
- &self.x
- }
- }
-
- fn main() {
- let p = Point { x: 5, y: 10 };
-
- println!("p.x = {}", p.x());
- }
注意必须在 impl
后面声明 T
,这样就可以在 Point<T>
上实现的方法中使用 T
了。通过在 impl
之后声明泛型 T
,Rust 就知道 Point
的尖括号中的类型是泛型而不是具体类型。可以为泛型参数选择一个与结构体定义中声明的泛型参数所不同的名称,不过依照惯例使用了相同的名称。impl
中编写的方法声明了泛型类型可以定位为任何类型的实例,不管最终替换泛型类型的是何具体类型。
定义方法时也可以为泛型指定限制(constraint)。例如,可以选择为 Point<f32>
实例实现方法,而不是为泛型 Point
实例。示例 10-10 展示了一个没有在 impl
之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,f32
:
- struct Point<T> {
- x: T,
- y: T,
- }
-
- impl<T> Point<T> {
- fn x(&self) -> &T {
- &self.x
- }
- }
-
- impl Point<f32> {
- fn distance_from_origin(&self) -> f32 {
- (self.x.powi(2) + self.y.powi(2)).sqrt()
- }
- }
-
- fn main() {
- let p = Point { x: 5, y: 10 };
-
- println!("p.x = {}", p.x());
- }
- struct Point<X1, Y1> {
- x: X1,
- y: Y1,
- }
-
- impl<X1, Y1> Point<X1, Y1> {
- fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
- Point {
- x: self.x,
- y: other.y,
- }
- }
- }
-
- fn main() {
- let p1 = Point { x: 5, y: 10.4 };
- let p2 = Point { x: "Hello", y: 'c' };
-
- let p3 = p1.mixup(p2);
-
- println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
- }
Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。
- #![allow(unused)]
- fn main() {
- let integer = Some(5);
- let float = Some(5.0);
- }
当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 Option<T>
的值并发现有两种 Option<T>
:一个对应 i32
另一个对应 f64
。为此,它会将泛型定义 Option<T>
展开为两个针对 i32
和 f64
的定义,接着将泛型定义替换为这两个具体的定义。
编译器生成的单态化版本的代码看起来像这样
- enum Option_i32 {
- Some(i32),
- None,
- }
-
- enum Option_f64 {
- Some(f64),
- None,
- }
-
- fn main() {
- let integer = Option_i32::Some(5);
- let float = Option_f64::Some(5.0);
- }
trait 类似于其他语言中的常被称为 接口(interfaces)的功能。定义共同拥有的方法。
一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。
- // 定义 trait
- pub trait Summary {
- fn summarize(&self) -> String;
- }
使用 trait
关键字来声明一个 trait,后面是 trait 的名字,在这个例子中是 Summary
。我们也声明 trait
为 pub
以便依赖这个 crate 的 crate 也可以使用这个 trait。大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是 fn summarize(&self) -> String
。
在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 Summary
trait 的类型都拥有与这个签名的定义完全一致的 summarize
方法。
trait 体中可以有多个方法:一行一个方法签名且都以分号结尾。
实现 trait
- pub trait Summary {
- fn summarize(&self) -> String;
- }
-
- pub struct NewsArticle {
- pub headline: String,
- pub location: String,
- pub author: String,
- pub content: String,
- }
-
- // 实现 trail
- 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,
- }
-
- // 实现 trail
- impl Summary for Tweet {
- fn summarize(&self) -> String {
- format!("{}: {}", self.username, self.content)
- }
- }
-
- fn main() {
-
- }
在类型上实现 trait 类似于实现与 trait 无关的方法。区别在于 impl
关键字之后,我们提供需要实现 trait 的名称,接着是 for
和需要实现 trait 的类型的名称。在 impl
块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。
现在库在 NewsArticle
和 Tweet
上实现了Summary
trait,crate 的用户可以像调用常规方法一样调用 NewsArticle
和 Tweet
实例的 trait 方法了。唯一的区别是 trait 必须和类型一起引入作用域以便使用额外的 trait 方法。这是一个二进制 crate 如何利用 aggregator
库 crate 的例子:
- use aggregator::{Summary, Tweet};
-
- fn main() {
- let tweet = Tweet {
- username: String::from("horse_ebooks"),
- content: String::from(
- "of course, as you probably already know, people",
- ),
- reply: false,
- retweet: false,
- };
-
- println!("1 new tweet: {}", tweet.summarize());
- }
注意限制:只有在 trait 或类型至少有一个属于当前 crate 时,我们才能对类型实现该 trait。不能为外部类型实现外部 trait。
这个限制是被称为 相干性(coherence)的程序属性的一部分,或者更具体的说是 孤儿规则(orphan rule),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。
- pub trait Summary {
- 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)
- }
- }
-
- pub fn notify(item: &impl Summary) {
- println!("Breaking news! {}", item.summarize());
- }
该 notify 参数支持任何实现了指定 trait 的类型。在 notify
函数体中,可以调用任何来自 Summary
trait 的方法,比如 summarize
。我们可以传递任何 NewsArticle
或 Tweet
的实例来调用 notify
。任何用其它如 String
或 i32
的类型调用该函数的代码都不能编译,因为它们没有实现 Summary
。
- // 不可复制的类型。
- struct Empty;
- struct Null;
-
- // `T` 的泛型 trait。
- trait DoubleDrop<T> {
- // 定义一个调用者的方法,接受一个额外的参数 `T`,但不对它做任何事。
- fn double_drop(self, _: T);
- }
-
- // 对泛型的调用者类型 `U` 和任何泛型类型 `T` 实现 `DoubleDrop<T>` 。
- impl<T, U> DoubleDrop<T> for U {
- // 此方法获得两个传入参数的所有权,并释放它们。
- fn double_drop(self, _: T) {}
- }
-
- fn main() {
- let empty = Empty;
- let null = Null;
-
- // 释放 `empty` 和 `null`。
- empty.double_drop(null);
-
- //empty;
- //null;
- // ^ 试一试:去掉这两行的注释。
- }
在使用泛型时,类型参数常常必须使用 trait 作为约束(bound)来明确规定类型应实现哪些功能。
示例:用到了 Display
trait 来打印,所以它用 Display
来约束 T
,也就是说 T
必须实现 Display
。
- // 定义一个函数 `printer`,接受一个类型为泛型 `T` 的参数,
- // 其中 `T` 必须实现 `Display` trait。
- fn printer<T: Display>(t: T) {
- println!("{}", t);
- }
泛型参数 与 泛型 约束 ( trait bound ) 声明在一起,位于 尖括号中的冒号后面。
fn func<泛型参数: 泛型约束>(形参: 泛型参数) {
//
}
- 约束 把 泛型类型 限制为符合约束的类型。
struct S<T: Display>(T); // 报错!`Vec<T>` 未实现 `Display`。此次泛型具体化失败。 let s = S(vec![1]);- 约束的另一个作用是泛型的实例可以访问作为约束的 trait 的方法。
// 这个 trait 用来实现打印标记:`{:?}`。 use std::fmt::Debug; trait HasArea { fn area(&self) -> f64; } impl HasArea for Rectangle { fn area(&self) -> f64 { self.length * self.height } } #[derive(Debug)] struct Rectangle { length: f64, height: f64 } #[allow(dead_code)] struct Triangle { length: f64, height: f64 } // 泛型 `T` 必须实现 `Debug` 。只要满足这点,无论什么类型 // 都可以让下面函数正常工作。 fn print_debug<T: Debug>(t: &T) { println!("{:?}", t); } // `T` 必须实现 `HasArea`。任意符合该约束的泛型的实例 // 都可访问 `HasArea` 的 `area` 函数 fn area<T: HasArea>(t: &T) -> f64 { t.area() } fn main() { let rectangle = Rectangle { length: 3.0, height: 4.0 }; let _triangle = Triangle { length: 3.0, height: 4.0 }; print_debug(&rectangle); println!("Area: {}", area(&rectangle)); //print_debug(&_triangle); //println!("Area: {}", area(&_triangle)); // ^ 试一试:取消上述语句的注释。 // | 报错:未实现 `Debug` 或 `HasArea`。 }- 某些情况下也可使用 where 分句来形成约束,这拥有更好的表现力。
impl Trait
很方便,适用于短小的例子。更长的 trait bound 则适用于更复杂的场景。例如,可以获取两个实现了 Summary
的参数。使用 impl Trait
的语法看起来像这样:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
这适用于 item1
和 item2
允许是不同类型的情况(只要它们都实现了 Summary
)。不过如果你希望强制它们都是相同类型呢?这只有在使用 trait bound 时才有可能:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
泛型 T
被指定为 item1
和 item2
的参数限制,如此传递给参数 item1
和 item2
值的具体类型必须一致。
约束的工作机制会产生这样的效果:即使一个 trait
不包含任何功能,你仍然可以用它作为约束。标准库中的 Eq
和 Ord
就是这样的 trait
。
- struct Cardinal;
- struct BlueJay;
- struct Turkey;
-
- trait Red {}
- trait Blue {}
-
- impl Red for Cardinal {}
- impl Blue for BlueJay {}
-
- // 这些函数只对实现了相应的 trait 的类型有效。
- // 事实上这些 trait 内部是空的,但这没有关系。
- fn red<T: Red>(_: &T) -> &'static str { "red" }
- fn blue<T: Blue>(_: &T) -> &'static str { "blue" }
-
- fn main() {
- let cardinal = Cardinal;
- let blue_jay = BlueJay;
- let _turkey = Turkey;
-
- // 由于约束,`red()` 不能作用于 blue_jay (蓝松鸟),反过来也一样。
- println!("A cardinal is {}", red(&cardinal));
- println!("A blue jay is {}", blue(&blue_jay));
- //println!("A turkey is {}", red(&_turkey));
- // ^ 试一试:去掉此行注释。
- }
参考:std::cmp::Eq, std::cmp::Ord, 和 trait
+
连接。,
隔开。- use std::fmt::{Debug, Display};
-
- fn compare_prints<T: Debug + Display>(t: &T) {
- println!("Debug: `{:?}`", t);
- println!("Display: `{}`", t);
- }
-
- fn compare_types<T: Debug, U: Debug>(t: &T, u: &U) {
- println!("t: `{:?}`", t);
- println!("u: `{:?}`", u);
- }
-
- fn main() {
- let string = "words";
- let array = [1, 2, 3];
- let vec = vec![1, 2, 3];
-
- compare_prints(&string);
- //compare_prints(&array);
- // 试一试 ^ 将此行注释去掉。
-
- compare_types(&array, &vec);
- }
使用过多的 trait bound 也有缺点。每个泛型有其自己的 trait bound,所以有多个泛型参数的函数在名称和参数列表之间会有很长的 trait bound 信息,这使得函数签名难以阅读。为此,Rust 有另一个在函数签名之后的 where
从句中指定 trait bound 的语法。所以除了这么写:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
使用 where
从句简化:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
unimplemented!()
}
这个函数签名就显得不那么杂乱,函数名、参数列表和返回值类型都离得很近,看起来跟没有那么多 trait bounds 的函数很像。
- impl <A: TraitB + TraitC, D: TraitE + TraitF> MyTrait<A, D> for YourType {}
-
- // 使用 `where` 从句来表达约束
- impl <A, D> MyTrait<A, D> for YourType where
- A: TraitB + TraitC,
- D: TraitE + TraitF {}
- use std::fmt::Debug;
-
- trait PrintInOption {
- fn print_in_option(self);
- }
-
- // 这里需要一个 `where` 从句,否则就要表达成 `T: Debug`(这样意思就变了),
- // 或者改用另一种间接的方法。
- impl<T> PrintInOption for T where
- Option<T>: Debug {
- // 我们要将 `Option<T>: Debug` 作为约束,因为那是要打印的内容。
- // 否则我们会给出错误的约束。
- fn print_in_option(self) {
- println!("{:?}", Some(self));
- }
- }
-
- fn main() {
- let vec = vec![1, 2, 3];
-
- vec.print_in_option();
- }
也可以在返回值中使用 impl Trait 语法,来返回实现了某个 trait 的类型:
- pub trait Summary {
- 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)
- }
- }
-
- 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,
- }
- }
通过使用 impl Summary
作为返回值类型,我们指定了 returns_summarizable
函数返回某个实现了 Summary
trait 的类型,但是不确定其具体的类型。在这个例子中 returns_summarizable
返回了一个 Tweet
,不过调用方并不知情。
返回一个只是指定了需要实现的 trait 的类型的能力在闭包和迭代器场景十分的有用
使用 trait bound 有条件地实现方法
对任何实现了特定 trait 的类型有条件地实现 trait。对任何满足特定 trait bound 的类型实现 trait 被称为 blanket implementations,它们被广泛的用于 Rust 标准库中。例如,标准库为任何实现了 Display
trait 的类型实现了 ToString
trait。这个 impl
块看起来像这样:
- impl<T: Display> ToString for T {
- // --snip--
- }
trait 和 trait bound 让我们能够使用泛型类型参数来减少重复,而且能够向编译器明确指定泛型类型需要拥有哪些行为。然后编译器可以利用 trait bound 信息检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们调用了一个未定义的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复问题。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了。这样既提升了性能又不必放弃泛型的灵活性。
在 Rust 中,"Newtype" 是一种惯用法,它允许你创建一个新的类型来封装现有类型。通过这种方式,你可以在类型层级上引入更多的安全性和表达力。
一种将类型占位符与 trait 联系起来的 做法,这样 trait 中的方法签名中就可以使用这些占位符类型。trait 的实现会指定在 该实现中那些占位符对应什么具体类型。
- struct Container(i32, i32);
-
- // 这个 trait 检查给定的 2 个项是否储存于容器中
- // 并且能够获得容器的第一个或最后一个值。
- trait Contains<A, B> {
- fn contains(&self, _: &A, _: &B) -> bool; // 显式地要求 `A` 和 `B`
- fn first(&self) -> i32; // 未显式地要求 `A` 或 `B`
- fn last(&self) -> i32; // 未显式地要求 `A` 或 `B`
- }
-
- impl Contains<i32, i32> for Container {
- // 如果存储的数字和给定的相等则为真。
- fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
- (&self.0 == number_1) && (&self.1 == number_2)
- }
-
- // 得到第一个数字。
- fn first(&self) -> i32 { self.0 }
-
- // 得到最后一个数字。
- fn last(&self) -> i32 { self.1 }
- }
-
- // 容器 `C` 就包含了 `A` 和 `B` 类型。鉴于此,必须指出 `A` 和 `B` 显得很麻烦。
- fn difference<A, B, C>(container: &C) -> i32 where
- C: Contains<A, B> {
- container.last() - container.first()
- }
-
- fn main() {
- let number_1 = 3;
- let number_2 = 10;
-
- let container = Container(number_1, number_2);
-
- println!("Does container contain {} and {}: {}",
- &number_1, &number_2,
- container.contains(&number_1, &number_2));
- println!("First number: {}", container.first());
- println!("Last number: {}", container.last());
-
- println!("The difference is: {}", difference(&container));
- }
通过把容器内部的类型放到 trait
中作为输出类型,使用 “关联类型” 增加了代码的可读性。这样的 trait
的定义语法如下:
-
- #![allow(unused)]
- fn main() {
- // `A` 和 `B` 在 trait 里面通过 `type` 关键字来定义。
- // (注意:此处的 `type` 不同于为类型取别名时的 `type`)。
- trait Contains {
- type A;
- type B;
-
- // 这种语法能够泛型地表示这些新类型。
- fn contains(&self, _: &Self::A, _: &Self::B) -> bool;
- }
- }
注意使用了 Contains
trait
的函数就不需要写出 A
或 B
了:
- // 不使用关联类型
- fn difference<A, B, C>(container: &C) -> i32 where
- C: Contains<A, B> { ... }
-
- // 使用关联类型
- fn difference<C: Contains>(container: &C) -> i32 { ... }
使用关联类型来重写上面代码
- struct Container(i32, i32);
-
- // 这个 trait 检查给定的 2 个项是否储存于容器中
- // 并且能够获得容器的第一个或最后一个值。
- trait Contains {
- // 在这里定义可以被方法使用的泛型类型。
- type A;
- type B;
-
- fn contains(&self, _: &Self::A, _: &Self::B) -> bool;
- fn first(&self) -> i32;
- fn last(&self) -> i32;
- }
-
- impl Contains for Container {
- // 指出 `A` 和 `B` 是什么类型。如果 `input`(输入)类型
- // 为 `Container(i32, i32)`,那么 `output`(输出)类型
- // 会被确定为 `i32` 和 `i32`。
- type A = i32;
- type B = i32;
-
- // `&Self::A` 和 `&Self::B` 在这里也是合法的类型。
- fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
- (&self.0 == number_1) && (&self.1 == number_2)
- }
-
- // 得到第一个数字。
- fn first(&self) -> i32 { self.0 }
-
- // 得到最后一个数字。
- fn last(&self) -> i32 { self.1 }
- }
-
- fn difference<C: Contains>(container: &C) -> i32 {
- container.last() - container.first()
- }
-
- fn main() {
- let number_1 = 3;
- let number_2 = 10;
-
- let container = Container(number_1, number_2);
-
- println!("Does container contain {} and {}: {}",
- &number_1, &number_2,
- container.contains(&number_1, &number_2));
- println!("First number: {}", container.first());
- println!("Last number: {}", container.last());
-
- println!("The difference is: {}", difference(&container));
- }
虚类型(phantom type)参数是一种在运行时不出现,而在(且仅在)编译时进行静态检查的类型参数。可以用额外的泛型类型参数指定数据类型,该类型可以充当标记,也可以供编译时类型检查使用。这些额外的参数没有存储值,也没有运行时行为。参考:Derive, 结构体, 和 元组结构体
生命周期是另一类泛型。不同于确保类型有期望的行为,生命周期确保引用如预期一直有效。
Rust 中的每一个引用都有其 生命周期(lifetime),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明它们的关系,这样就能确保运行时实际使用的引用绝对是有效的。
search
返回的数据将与 search
函数中的参数 contents
的数据存在的一样久。这是非常重要的!参数 contents
包含了所有的文本而且我们希望返回匹配的那部分文本,所以 contents
参数需要使用 生命周期语法 来与返回值相关联。其他语言中并不需要你在函数签名中将参数与返回值相关联。 “生命周期与引用有效性” 。函数中的泛型生命周期
生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号('
)开头,其名称通常全是小写,类似于泛型其名称非常短。大多数人使用 'a
作为第一个生命周期注解。生命周期参数注解位于引用的 &
之后,并有一个空格来将引用类型与生命周期注解分隔开。
这里有一些例子:我们有一个没有生命周期参数的 i32
的引用,一个有叫做 'a
的生命周期参数的 i32
的引用,和一个生命周期也是 'a
的 i32
的可变引用:
&i32 // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
单个的生命周期注解本身没有多少意义,因为生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系的。让我们在 longest
函数的上下文中理解生命周期注解如何相互联系。
例如如果函数有一个生命周期 'a
的 i32
的引用的参数 first
。还有另一个同样是生命周期 'a
的 i32
的引用的参数 second
。这两个生命周期注解意味着引用 first
和 second
必须与这泛型生命周期存在得一样久。
函数签名中的生命周期注解
为了在函数签名中使用生命周期注解,需要在函数名和参数列表间的尖括号中声明泛型生命周期(lifetime)参数,就像泛型类型(type)参数一样。
我们希望函数签名表达如下限制:也就是这两个参数和返回的引用存活的一样久。(两个)参数和返回的引用的生命周期是相关的。就像示例 10-21 中在每个引用中都加上了 'a
那样。
- fn main() {
- let string1 = String::from("abcd");
- let string2 = "xyz";
-
- let result = longest(string1.as_str(), string2);
- println!("The longest string is {}", result);
- }
-
- // longest 函数定义指定了签名中所有的引用必须有相同的生命周期 'a
- fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
- if x.len() > y.len() {
- x
- } else {
- y
- }
- }
现在函数签名表明对于某些生命周期 'a
,函数会获取两个参数,它们都是与生命周期 'a
存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 'a
存在的一样长的字符串 slice。它的实际含义是 longest
函数返回的引用的生命周期与函数参数所引用的值的生命周期的较小者一致。这些关系就是我们希望 Rust 分析代码时所使用的。
通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。注意 longest
函数并不需要知道 x
和 y
具体会存在多久,而只需要知道有某个可以被 'a
替代的作用域将会满足这个签名。
当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。生命周期注解成为了函数约定的一部分,非常像签名中的类型。让函数签名包含生命周期约定意味着 Rust 编译器的工作变得更简单了。如果函数注解有误或者调用方法不对,编译器错误可以更准确地指出代码和限制的部分。如果不这么做的话,Rust 编译会对我们期望的生命周期关系做更多的推断,这样编译器可能只能指出离出问题地方很多步之外的代码。
当具体的引用被传递给 longest
时,被 'a
所替代的具体生命周期是 x
的作用域与 y
的作用域相重叠的那一部分。换一种说法就是泛型生命周期 'a
的具体生命周期等同于 x
和 y
的生命周期中较小的那一个。因为我们用相同的生命周期参数 'a
标注了返回的引用值,所以返回的引用值就能保证在 x
和 y
中较短的那个生命周期结束之前保持有效。
让我们看看如何通过传递拥有不同具体生命周期的引用来限制 longest
函数的使用。
- fn main() {
- let string1 = String::from("long string is long");
-
- {
- let string2 = String::from("xyz");
- let result = longest(string1.as_str(), string2.as_str());
- println!("The longest string is {}", result);
- }
- }
-
- fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
- if x.len() > y.len() {
- x
- } else {
- y
- }
- }
从人的角度读上述代码,我们可能会觉得这个代码是正确的。 string1
更长,因此 result
会包含指向 string1
的引用。因为 string1
尚未离开作用域,对于 println!
来说 string1
的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是: longest
函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器代码检查失败,因为它可能会存在无效的引用。
深入理解生命周期
指定生命周期参数的正确方式依赖函数实现的具体功能。例如,如果将 longest
函数的实现修改为总是返回第一个参数而不是最长的字符串 slice,就不需要为参数 y
指定一个生命周期。如下代码将能够编译:
- fn main() {
- let string1 = String::from("abcd");
- let string2 = "efghijklmnopqrstuvwxyz";
-
- let result = longest(string1.as_str(), string2);
- println!("The longest string is {}", result);
- }
-
- fn longest<'a>(x: &'a str, y: &str) -> &'a str {
- x
- }
为参数 x
和返回值指定了生命周期参数 'a
,不过没有为参数 y
指定,因为 y
的生命周期与参数 x
和返回值的生命周期没有任何关系。
总结:生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦它们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。
定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期注解。
- struct ImportantExcerpt<'a> {
- part: &'a str,
- }
-
- fn main() {
- let novel = String::from("Call me Ishmael. Some years ago...");
- let first_sentence = novel.split('.').next().expect("Could not find a '.'");
- let i = ImportantExcerpt {
- part: first_sentence,
- };
- }
这个结构体有唯一一个字段 part
,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。这个注解意味着 ImportantExcerpt
的实例不能比其 part
字段中的引用存在的更久。
这里的 main
函数创建了一个 ImportantExcerpt
的实例,它存放了变量 novel
所拥有的 String
的第一个句子的引用。novel
的数据在 ImportantExcerpt
实例创建之前就存在。另外,直到 ImportantExcerpt
离开作用域之后 novel
都不会离开作用域,所以 ImportantExcerpt
实例中的引用是有效的。
生命周期省略(Lifetime Elision)
每一个引用都有一个生命周期,而且我们需要为那些使用了引用的函数或结构体指定生命周期。
函数或方法的参数的生命周期被称为 输入生命周期(input lifetimes),而返回值的生命周期被称为 输出生命周期(output lifetimes)。
编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 fn
定义,以及 impl
块。
fn foo<'a>(x: &'a i32)
,有两个引用参数的函数就有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
,依此类推。fn foo<'a>(x: &'a i32) -> &'a i32
。&self
或 &mut self
,说明是个对象的方法 (method)(译者注:这里涉及 rust 的面向对象参见 17 章),那么所有输出生命周期参数被赋予 self
的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。- fn first_word(s: &str) -> &str {
- let bytes = s.as_bytes();
-
- for (i, &item) in bytes.iter().enumerate() {
- if item == b' ' {
- return &s[0..i];
- }
- }
-
- &s[..]
- }
-
- fn main() {
- let my_string = String::from("hello world");
-
- // first_word works on slices of `String`s
- let word = first_word(&my_string[..]);
-
- let my_string_literal = "hello world";
-
- // first_word works on slices of string literals
- let word = first_word(&my_string_literal[..]);
-
- // Because string literals *are* string slices already,
- // this works too, without the slice syntax!
- let word = first_word(my_string_literal);
- }
假设我们自己就是编译器。并应用这些规则来计算示例中 first_word
函数签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期:fn first_word(s: &str) -> &str {
接着编译器应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为 'a
,所以现在签名看起来像这样:fn first_word<'a>(s: &'a str) -> &str {
对于第二条规则,因为这里正好只有一个输入生命周期参数所以是适用的。第二条规则表明输入参数的生命周期将被赋予输出生命周期参数,所以现在签名看起来像这样:fn first_word<'a>(s: &'a str) -> &'a str {
现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。
另一个例子:
从没有生命周期参数的
longest
函数开始:fn longest(x: &str, y: &str) -> &str {
再次假设我们自己就是编译器并应用第一条规则:每个引用参数都有其自己的生命周期。这次有两个参数,所以就有两个(不同的)生命周期:fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
再来应用第二条规则,因为函数存在多个输入生命周期,它并不适用于这种情况。再来看第三条规则,它同样也不适用,这是因为没有 self
参数。应用了三个规则之后编译器还没有计算出返回值类型的生命周期。这就是在编译示例 10-20 的代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,仍不能计算出签名中所有引用的生命周期。
因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看那种情况中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期。
方法定义中的生命周期注解
当为带有生命周期的结构体实现方法时,其语法依然类似泛型类型参数的语法。我们在哪里声明和使用生命周期参数,取决于它们是与结构体字段相关还是与方法参数和返回值相关。
(实现方法时)结构体字段的生命周期必须总是在 impl
关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。
impl
块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用示例 10-24 中定义的结构体 ImportantExcerpt
的例子。
首先,这里有一个方法 level
。其唯一的参数是 self
的引用,而且返回值只是一个 i32
,并不引用任何值:
- struct ImportantExcerpt<'a> {
- part: &'a str,
- }
-
- impl<'a> ImportantExcerpt<'a> {
- fn level(&self) -> i32 {
- 3
- }
- }
-
- impl<'a> ImportantExcerpt<'a> {
- fn announce_and_return_part(&self, announcement: &str) -> &str {
- println!("Attention please: {}", announcement);
- self.part
- }
- }
-
- fn main() {
- let novel = String::from("Call me Ishmael. Some years ago...");
- let first_sentence = novel.split('.').next().expect("Could not find a '.'");
- let i = ImportantExcerpt {
- part: first_sentence,
- };
- }
impl
之后和类型名称之后的生命周期参数是必要的,不过因为第一条生命周期规则我们并不必须标注 self
引用的生命周期。
这里是一个适用于第三条生命周期省略规则的例子:
- struct ImportantExcerpt<'a> {
- part: &'a str,
- }
-
- impl<'a> ImportantExcerpt<'a> {
- fn level(&self) -> i32 {
- 3
- }
- }
-
- impl<'a> ImportantExcerpt<'a> {
- fn announce_and_return_part(&self, announcement: &str) -> &str {
- println!("Attention please: {}", announcement);
- self.part
- }
- }
-
- fn main() {
- let novel = String::from("Call me Ishmael. Some years ago...");
- let first_sentence = novel.split('.').next().expect("Could not find a '.'");
- let i = ImportantExcerpt {
- part: first_sentence,
- };
- }
这里有两个输入生命周期,所以 Rust 应用第一条生命周期省略规则并给予 &self
和 announcement
它们各自的生命周期。接着,因为其中一个参数是 &self
,返回值类型被赋予了 &self
的生命周期,这样所有的生命周期都被计算出来了。
静态生命周期
其生命周期能够存活于整个程序期间。所有的字符串字面值都拥有 'static
生命周期,我们也可以选择像下面这样标注出来:
- #![allow(unused)]
- fn main() {
- let s: &'static str = "I have a static lifetime.";
- }
这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是 'static
的。
你可能在错误信息的帮助文本中见过使用 'static
生命周期的建议,不过将引用指定为 'static
之前,思考一下这个引用是否真的在整个程序的生命周期里都有效,以及你是否希望它存在得这么久。大部分情况中,推荐 'static
生命周期的错误信息都是尝试创建一个悬垂引用或者可用的生命周期不匹配的结果。在这种情况下的解决方案是修复这些问题而不是指定一个 'static
的生命周期。
结合泛型类型参数、trait bounds 和生命周期
- fn main() {
- let string1 = String::from("abcd");
- let string2 = "xyz";
-
- let result = longest_with_an_announcement(
- string1.as_str(),
- string2,
- "Today is someone's birthday!",
- );
- println!("The longest string is {}", result);
- }
-
- 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
- }
- }
返回两个字符串 slice 中较长者的 longest
函数,不过带有一个额外的参数 ann
。ann
的类型是泛型 T
,它可以被放入任何实现了 where
从句中指定的 Display
trait 的类型。这个额外的参数会使用 {}
打印,这也就是为什么 Display
trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 'a
和泛型类型参数 T
都位于函数名后的同一尖括号列表中。
泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切发生在编译时所以不会影响运行时效率!
指针 (pointer)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。Rust 中最常见的指针是第四章介绍的 引用(reference)。引用以 &
符号为标志并借用了它们所指向的值。除了引用数据没有任何其他特殊功能,也没有额外开销。
智能指针(smart pointers)是一类数据结构,它们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有;其起源于 C++ 并存在于其他语言中。Rust 标准库中定义了多种不同的智能指针,它们提供了多于引用的额外功能。
为了探索其基本概念,我们来看看一些智能指针的例子,
智能指针通常使用结构体实现。智能指针不同于结构体的地方在于其实现了 Deref
和 Drop
trait。Deref
trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。Drop
trait 允许我们自定义当智能指针离开作用域时运行的代码。
智能指针是一个在 Rust 经常被使用的通用设计模式,这里不会覆盖所有现存的智能指针。很多库都有自己的智能指针而你也可以编写属于你自己的智能指针。这里将会讲到的是来自标准库中最常用的一些:
Box<T>
,用于已知的大小并指向分配在堆上的数据。Rc<T>
,一个引用计数类型,其数据可以有多个所有者。RefCell<T>
和其内部可变性提供了一个可以用于当需要不可变类型但是需要改变其内部值能力的类型,并在运行时而不是编译时检查借用规则。( RefCell<T>
是一个在运行时而不是在编译时执行借用规则的类型)。内部可变性(interior mutability)模式,这是不可变类型暴露出改变其内部值的 API。Ref<T>
和 RefMut<T>
,通过 RefCell<T>
访问。最简单的智能指针是 box,其类型是 Box<T>
。box 允许将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。当box离开作用域时,它的析构函数会被调用,内部的对象会被销毁,堆上分配的内存也会被释放。被box指向的值可以使用 *
运算符进行解引用;这会移除掉一层装箱。除了数据被储存在堆上而不是栈上之外,box 没有性能损失。
- use std::mem;
-
- #[allow(dead_code)]
- #[derive(Debug, Clone, Copy)]
- struct Point {
- x: f64,
- y: f64,
- }
-
- #[allow(dead_code)]
- struct Rectangle {
- p1: Point,
- p2: Point,
- }
-
- fn origin() -> Point {
- Point { x: 0.0, y: 0.0 }
- }
-
- fn boxed_origin() -> Box<Point> {
- // 在堆上分配这个点(point),并返回一个指向它的指针
- Box::new(Point { x: 0.0, y: 0.0 })
- }
-
- fn main() {
- // (所有的类型标注都不是必需的)
- // 栈分配的变量
- let point: Point = origin();
- let rectangle: Rectangle = Rectangle {
- p1: origin(),
- p2: Point { x: 3.0, y: 4.0 }
- };
-
- // 堆分配的 rectangle(矩形)
- let boxed_rectangle: Box<Rectangle> = Box::new(Rectangle {
- p1: origin(),
- p2: origin()
- });
-
- // 函数的输出可以装箱
- let boxed_point: Box<Point> = Box::new(origin());
-
- // 两层装箱
- let box_in_a_box: Box<Box<Point>> = Box::new(boxed_origin());
-
- println!("Point occupies {} bytes in the stack",
- mem::size_of_val(&point));
- println!("Rectangle occupies {} bytes in the stack",
- mem::size_of_val(&rectangle));
-
- // box 的宽度就是指针宽度
- println!("Boxed point occupies {} bytes in the stack",
- mem::size_of_val(&boxed_point));
- println!("Boxed rectangle occupies {} bytes in the stack",
- mem::size_of_val(&boxed_rectangle));
- println!("Boxed box occupies {} bytes in the stack",
- mem::size_of_val(&box_in_a_box));
-
- // 将包含在 `boxed_point` 中的数据复制到 `unboxed_point`
- let unboxed_point: Point = *boxed_point;
- println!("Unboxed point occupies {} bytes in the stack",
- mem::size_of_val(&unboxed_point));
- }
多用于如下场景:
示例:使用 Box<T> 在堆上储存数据
- fn main() {
- let b = Box::new(5);
- println!("b = {}", b);
- }
Box 允许创建递归类型:递归类型(recursive type)的值可以拥有另一个同类型的值作为其自身的一部分。但是这会产生一个问题,因为 Rust 需要在编译时知道类型占用多少空间。递归类型的值嵌套理论上可以无限地进行下去,所以 Rust 不知道递归类型需要多少空间。因为 box 有一个已知的大小,所以通过在循环类型定义中插入 box,就可以创建递归类型了。
示例:cons ("construct function" 的缩写) list 来展示(递归类型)概念。有一个包含列表 1,2,3 的 cons list 的伪代码表示,其每一个列表在一个括号中:(1, (2, (3, Nil))),cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做 Nil
的值且没有下一项。cons list 通过递归调用 cons
函数产生。代表递归的终止条件(base case)的规范名称是 Nil
,它宣布列表的终止。
- use crate::List::{Cons, Nil};
- enum List {
- Cons(i32, List),
- Nil,
- }
- fn main() {
- let list = Cons(1, Cons(2, Cons(3, Nil)));
- }
尝试定义一个递归枚举时得到的错误,这个错误表明这个类型 “有无限的大小”。其原因是 List
的一个成员被定义为是递归的:它直接存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 List
值到底需要多少空间。让我们拆开来看为何会得到这个错误。首先了解一下 Rust 如何决定需要多少空间来存放一个非递归类型。
编译器尝试计算出储存一个 List
枚举需要多少内存,并开始检查 Cons
成员,那么 Cons
需要的空间等于 i32
的大小加上 List
的大小。为了计算 List
需要多少内存,它检查其成员,从 Cons
成员开始。Cons
成员储存了一个 i32
值和一个List
值,这样的计算将无限进行下去。
如图:一个包含无限个 Cons 成员的无限 List
因为 Rust 无法计算出要为定义为递归的类型分配多少空间,所以编译器给出了一个包括了有用建议的错误:
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
|
2 | Cons(i32, Box<List>),
| ++++ +
使用 Box<T> 的 List 定义
- enum List {
- Cons(i32, Box<List>),
- Nil,
- }
-
- use crate::List::{Cons, Nil};
-
- fn main() {
- let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
- }
Cons
成员将会需要一个 i32
的大小加上储存 box 指针数据的空间。Nil
成员不储存值,所以它比 Cons
成员需要更少的空间。现在我们知道了任何 List
值最多需要一个 i32
加上 box 指针数据的大小。通过使用 box,打破了这无限递归的连锁,这样编译器就能够计算出储存 List
值需要的大小了。如图:因为 Cons 存放一个 Box 所以 List 不是无限大小的了
Deref
trait 允许我们重载 解引用运算符(dereference operator)*
常规引用是一个指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。
- fn main() {
- let x = 5;
- let y = &x;
- assert_eq!(5, x);
- assert_eq!(5, *y);
- let number = 100;
- let pointer = &number as *const i32;
- println!("Pointer value: {:?}", pointer);
- }
"数字的引用、数字" 无法进行比较,因为它们是不同的类型。必须使用解引用运算符追踪引用所指向的值。变量 x
存放了一个 i32
值 5
。y
等于 x
的一个引用。可以断言 x
等于 5
。然而,如果希望对 y
的值做出断言,必须使用 *y
来追踪引用所指向的值(也就是 解引用),这样编译器就可以比较实际的值了。一旦解引用了 y
,就可以访问 y
所指向的整型值并可以与 5
做比较。
使用 Box<T> 时,就像使用引用一样。
- fn main() {
- let x = 5;
- let y = Box::new(x);
- assert_eq!(5, x);
- assert_eq!(5, *y); // 底层运行的 *(y.deref())
-
- println!("y 地址: {:p}", y.as_ref());
- }
自定义智能指针,通过实现 Deref trait 将某类型像引用一样处理
为了体会默认情况下智能指针与引用的不同,让我们创建一个类似于标准库提供的 Box<T>
类型的智能指针。接着学习如何增加使用解引用运算符的功能。
从根本上说,Box<T>
被定义为包含一个元素的元组结构体,所以示例 15-8 以相同的方式定义了 MyBox<T>
类型。我们还定义了 new
函数来对应定义于 Box<T>
的 new
函数:
- 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(&self) -> &Self::Target {
- &self.0
- }
- }
- fn main() {
- let x = 5;
- let y = MyBox::new(x);
-
- assert_eq!(5, x);
- assert_eq!(5, *y);
- }
定义了一个结构体 MyBox
并声明了一个泛型参数 T
,因为我们希望其可以存放任何类型的值。MyBox
是一个包含 T
类型元素的元组结构体。MyBox::new
函数获取一个 T
类型的参数并返回一个存放传入值的 MyBox
实例。为了启用 *
运算符的解引用功能,需要实现 Deref
trait。 “为类型实现 trait” 部分所讨论的,为了实现 trait,需要提供 trait 所需的方法实现。Deref
trait,由标准库提供,要求实现名为 deref
的方法,其借用 self
并返回一个内部数据的引用。deref
方法体中写入了 &self.0
,这样 deref
返回了我希望通过 *
运算符访问的值的引用。.0
用来访问元组结构体的第一个元素。
在代码中使用 *
时, *
运算符都被替换成了先调用 deref
方法再接着使用 *
解引用的操作,且只会发生一次,不会对 *
操作符无限递归替换,解引用出上面 i32
类型的值就停止了
Deref 强制转换(deref coercions)将实现了 Deref
trait 的类型的引用转换为另一种类型的引用。例如,Deref 强制转换可以将 &String
转换为 &str
,因为 String
实现了 Deref
trait 因此可以返回 &str
。Deref 强制转换是 Rust 在函数或方法传参上的一种便利操作,并且只能作用于实现了 Deref
trait 的类型。当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时将自动进行。这时会有一系列的 deref
方法被调用,把我们提供的类型转换成了参数所需的类型。
Deref 强制转换的加入使得 Rust 程序员编写函数和方法调用时无需增加过多显式使用 &
和 *
的引用和解引用。
- fn hello(name: &str) {
- println!("Hello, {name}!");
- }
-
- fn main() {
- let m = MyBox::new(String::from("Rust"));
- hello(&m);
- }
如果 Rust 没有实现 Deref 强制转换,为了使用 &MyBox<String>
类型的值调用 hello
,则不得不编写示
- fn main() {
- let m = MyBox::new(String::from("Rust"));
- hello(&(*m)[..]);
- }
(*m)
将 MyBox<String>
解引用为 String
。接着 &
和 [..]
获取了整个 String
的字符串 slice 来匹配 hello
的签名。没有 Deref 强制转换所有这些符号混在一起将更难以读写和理解。Deref 强制转换使得 Rust 自动的帮我们处理这些转换。
Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换:
T: Deref<Target=U>
时从 &T
到 &U
。T: DerefMut<Target=U>
时从 &mut T
到 &mut U
。T: Deref<Target=U>
时从 &mut T
到 &U
。头两个情况除了第二种实现了可变性之外是相同的:第一种情况表明如果有一个 &T
,而 T
实现了返回 U
类型的 Deref
,则可以直接得到 &U
。第二种情况表明对于可变引用也有着相同的行为。
第三个情况有些微妙:Rust 也会将可变引用强转为不可变引用。但是反之是 不可能 的:不可变引用永远也不能强转为可变引用。因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要初始的不可变引用是数据唯一的不可变引用,而借用规则无法保证这一点。因此,Rust 无法假设将不可变引用转换为可变引用是可能的。
指定在值离开作用域时应该执行的代码的方式是实现 Drop
trait。Drop
trait 要求实现一个叫做 drop
的方法,它获取一个 self
的可变引用。Drop
trait 相当于C++的析构函数,离开作用域时自动执行。为了能够看出 Rust 何时调用 drop
,下面示例使用 println!
语句实现 drop
。
- struct CustomSmartPointer {
- data: String,
- }
-
- impl Drop for CustomSmartPointer {
- fn drop(&mut self) {
- println!("清理自定义智能指针的 data `{}`!", self.data);
- }
- }
-
- fn main() {
- let c = CustomSmartPointer {
- data: String::from("my stuff"),
- };
- let d = CustomSmartPointer {
- data: String::from("other stuff"),
- };
- println!("创建自定义智能指针.");
- }
通过 std::mem::drop 提早丢弃值。有时需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行 drop
方法来释放锁以便作用域中的其他代码可以获取锁。
- fn main() {
- let c = CustomSmartPointer {
- data: String::from("some data"),
- };
- println!("CustomSmartPointer created.");
- c.drop();
- println!("CustomSmartPointer dropped before the end of main.");
- }
Rust 不允许显式调用 drop
因为 Rust 仍然会在 main
的结尾对值自动调用 drop
,这会导致一个 double free 错误,因为 Rust 会尝试清理相同的值两次。
- use std::mem::drop;
-
-
- struct CustomSmartPointer {
- data: String,
- }
-
- impl Drop for CustomSmartPointer {
- fn drop(&mut self) {
- println!("Dropping CustomSmartPointer with data `{}`!", self.data);
- }
- }
-
- fn main() {
- let c = CustomSmartPointer {
- data: String::from("some data"),
- };
- println!("CustomSmartPointer created.");
- drop(c);
- println!("CustomSmartPointer dropped before the end of main.");
- }
当需要多个所有权时,可以使用 Rc
(引用计数,Reference Counting 缩写)。Rc
跟踪引用的数量,这相当于包裹在 Rc
值的所有者的数量。Rc<T> 引用计数智能指针,允许在程序的多个部分之间只读地共享数据。
每当克隆一个 Rc
时,Rc
的引用计数就会增加 1,而每当克隆得到的 Rc
退出作用域时,引用计数就会减少 1。当 Rc
的引用计数变为 0 时,这意味着已经没有所有者,Rc
和值两者都将被删除。
克隆 Rc
从不执行深拷贝。克隆只创建另一个指向包裹值的指针,并增加计数。
- use std::rc::Rc;
-
- fn main() {
- let rc_examples = "Rc examples".to_string();
- {
- println!("--- rc_a is created ---");
-
- let rc_a: Rc<String> = Rc::new(rc_examples);
- println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));
-
- {
- println!("--- rc_a is cloned to rc_b ---");
-
- let rc_b: Rc<String> = Rc::clone(&rc_a);
- println!("Reference Count of rc_b: {}", Rc::strong_count(&rc_b));
- println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));
-
- // 如果两者内部的值相等的话,则两个 `Rc` 相等。
- println!("rc_a and rc_b are equal: {}", rc_a.eq(&rc_b));
-
- // 我们可以直接使用值的方法
- println!("Length of the value inside rc_a: {}", rc_a.len());
- println!("Value of rc_b: {}", rc_b);
-
- println!("--- rc_b is dropped out of scope ---");
- }
-
- println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));
-
- println!("--- rc_a is dropped out of scope ---");
- }
-
- // 报错!`rc_examples` 已经移入 `rc_a`。
- // 而且当 `rc_a` 被删时,`rc_examples` 也被一起删除。
- // println!("rc_examples: {}", rc_examples);
- // 试一试 ^ 注释掉此行代码
- }
大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的节点,而这个节点从概念上讲为所有指向它的边所拥有。节点直到没有任何边指向它之前都不应该被清理因此也没有所有者。
为了启用多所有权需要显式地使用 Rust 类型 Rc<T>
,其为 引用计数(reference counting)的缩写。引用计数意味着记录一个值的引用数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。
可以将其想象为客厅中的电视。当一个人进来看电视时,他打开电视。其他人也可以进来看电视。当最后一个人离开房间时,他关掉电视因为它不再被使用了。如果某人在其他人还在看的时候就关掉了电视,正在看电视的人肯定会抓狂的!
注意 Rc<T>
只能用于单线程场景;第十六章并发会涉及到如何在多线程程序中进行引用计数。
使用 Rc<T> 共享数据。创建两个共享第三个列表所有权的列表,如图
两个列表,b
和 c
, 共享第三个列表 a
的所有权。列表 a
包含 5 之后是 10,之后是另两个列表:b
从 3 开始而 c
从 4 开始。b
和 c
会接上包含 5 和 10 的列表 a
。换句话说,这两个列表会尝试共享第一个列表所包含的 5 和 10。
错误代码:
- enum List {
- Cons(i32, Box<List>),
- Nil,
- }
-
- use crate::List::{Cons, Nil};
-
- fn main() {
- let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
- let b = Cons(3, Box::new(a));
- let c = Cons(4, Box::new(a));
- }
Cons
成员拥有其储存的数据,所以当创建 b
列表时,a
被移动进了 b
这样 b
就拥有了 a
。接着当再次尝试使用 a
创建 c
时,这不被允许,因为 a
的所有权已经被移动。
可以改变 Cons
的定义来存放一个引用,不过接着必须指定生命周期参数。通过指定生命周期参数,表明列表中的每一个元素都至少与列表本身存在的一样久。也可以修改 List
的定义为使用 Rc<T>
代替 Box<T>,
现在每一个 Cons
变量都包含一个值和一个指向 List
的 Rc<T>
。当创建 b
时,不同于获取 a
的所有权,这里会克隆 a
所包含的 Rc<List>
,这会将引用计数从 1 增加到 2 并允许 a
和 b
共享 Rc<List>
中数据的所有权。创建 c
时也会克隆 a
,这会将引用计数从 2 增加为 3。每次调用 Rc::clone
,Rc<List>
中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。
- enum List {
- Cons(i32, Rc<List>),
- Nil,
- }
-
- use crate::List::{Cons, Nil};
- use std::rc::Rc;
-
- fn main() {
- let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
- let b = Cons(3, Rc::clone(&a));
- let c = Cons(4, Rc::clone(&a));
- }
克隆 Rc<T> 会增加引用计数
- enum List {
- Cons(i32, Rc<List>),
- Nil,
- }
-
- use crate::List::{Cons, Nil};
- use std::rc::Rc;
-
-
- fn main() {
- let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
- println!("创建a后的引用计数 = {}", Rc::strong_count(&a));
- let b = Cons(3, Rc::clone(&a));
- println!("创建b后的引用计数 = {}", Rc::strong_count(&a));
- {
- let c = Cons(4, Rc::clone(&a));
- println!("创建c后的引用计数 = {}", Rc::strong_count(&a));
- }
- println!("退出\"块作用域\"后的引用计数 = {}", Rc::strong_count(&a));
- }
在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 Rc::strong_count
函数获得。这个函数叫做 strong_count
而不是 count
是因为 Rc<T>
也有 weak_count
;在 “避免引用循环:将 Rc<T> 变为 Weak<T>” 部分会讲解 weak_count
的用途。
选择 Box<T>
,Rc<T>
或 RefCell<T>
的理由:
Rc<T>
允许相同数据有多个所有者;Box<T>
和 RefCell<T>
有单一所有者。Box<T>
允许在编译时执行不可变或可变借用检查;Rc<T>
仅允许在编译时执行不可变借用检查;RefCell<T>
允许在运行时执行不可变或可变借用检查。RefCell<T>
允许在运行时执行可变借用检查,所以在即便 RefCell<T>
自身是不可变的情况下也可以修改其内部的值。当线程之间所有权需要共享时,可以使用Arc
(共享引用计数,Atomic Reference Counted 缩写)可以使用。这个结构通过 Clone
实现可以为内存堆中的值的位置创建一个引用指针,同时增加引用计数器。由于它在线程之间共享所有权,因此当指向某个值的最后一个引用指针退出作用域时,该变量将被删除。
- use std::sync::Arc;
- use std::thread;
-
- fn main() {
- // 这个变量声明用来指定其值的地方。
- let apple = Arc::new("the same apple");
-
- for _ in 0..10 {
- // 这里没有数值说明,因为它是一个指向内存堆中引用的指针。
- let apple = Arc::clone(&apple);
-
- thread::spawn(move || {
- // 由于使用了Arc,线程可以使用分配在 `Arc` 变量指针位置的值来生成。
- println!("{:?}", apple);
- });
- }
- }
-
在不可变值内部改变值就是 内部可变性 模式。
结合 Rc<T> 和 RefCell<T> 来拥有多个可变数据所有者。RefCell<T>
的一个常见用法是与 Rc<T>
结合。 Rc<T>
允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 RefCell<T>
的 Rc<T>
的话,就可以得到有多个所有者 并且 可以修改的值了!
示例:cons list 的例子中使用 Rc<T>
使得多个列表共享另一个列表的所有权。因为 Rc<T>
只存放不可变值,所以一旦创建了这些列表值后就不能修改。现在加入 RefCell<T>
来获得修改列表中值的能力。就可以修改所有列表中的值了:
- #[derive(Debug)]
- enum List {
- Cons(Rc<RefCell<i32>>, Rc<List>),
- Nil,
- }
-
- use crate::List::{Cons, Nil};
- use std::cell::RefCell;
- use std::rc::Rc;
-
- fn main() {
- let value = Rc::new(RefCell::new(5));
-
- let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
-
- let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
- let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
-
- *value.borrow_mut() += 10;
-
- println!("a after = {:?}", a);
- println!("b after = {:?}", b);
- println!("c after = {:?}", c);
- }
创建了一个 Rc<RefCell<i32>>
实例并储存在变量 value
中以便之后直接访问。接着在 a
中用包含 value
的 Cons
成员创建了一个 List
。需要克隆 value
以便 a
和 value
都能拥有其内部值 5
的所有权,而不是将所有权从 value
移动到 a
或者让 a
借用 value
。
我们将列表 a
封装进了 Rc<T>
这样当创建列表 b
和 c
时,它们都可以引用 a
,正如示例 15-18 一样。
一旦创建了列表 a
、b
和 c
,我们将 value
的值加 10。为此对 value
调用了 borrow_mut
,这里使用了第五章讨论的自动解引用功能(“-> 运算符到哪去了?” 部分)来解引用 Rc<T>
以获取其内部的 RefCell<T>
值。borrow_mut
方法返回 RefMut<T>
智能指针,可以对其使用解引用运算符并修改其内部值。
当我们打印出 a
、b
和 c
时,可以看到它们都拥有修改后的值 15 而不是 5:
通过使用 RefCell<T>
,我们可以拥有一个表面上不可变的 List
,不过可以使用 RefCell<T>
中提供内部可变性的方法来在需要时修改数据。RefCell<T>
的运行时借用规则检查也确实保护我们免于出现数据竞争——有时为了数据结构的灵活性而付出一些性能是值得的。注意 RefCell<T>
不能用于多线程代码!Mutex<T>
是一个线程安全版本的 RefCell<T>
,
在 a
中创建了一个列表,一个指向 a
中列表的 b
列表,接着修改 a
中的列表指向 b
中的列表,这会创建一个引用循环。在这个过程的多个位置有 println!
语句展示引用计数。
- use crate::List::{Cons, Nil};
- use std::cell::RefCell;
- use std::rc::Rc;
-
- #[derive(Debug)]
- enum List {
- Cons(i32, RefCell<Rc<List>>),
- Nil,
- }
-
- impl List {
- fn tail(&self) -> Option<&RefCell<Rc<List>>> {
- match self {
- Cons(_, item) => Some(item),
- Nil => None,
- }
- }
- }
-
- fn main() {
- let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
-
- println!("a initial rc count = {}", Rc::strong_count(&a));
- println!("a next item = {:?}", a.tail());
-
- let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
-
- println!("a rc count after b creation = {}", Rc::strong_count(&a));
- println!("b initial rc count = {}", Rc::strong_count(&b));
- println!("b next item = {:?}", b.tail());
-
- if let Some(link) = a.tail() {
- *link.borrow_mut() = Rc::clone(&b);
- }
-
- println!("b rc count after changing a = {}", Rc::strong_count(&b));
- println!("a rc count after changing a = {}", Rc::strong_count(&a));
-
- // Uncomment the next line to see that we have a cycle;
- // it will overflow the stack
- // println!("a next item = {:?}", a.tail());
- }
在变量 a
中创建了一个 Rc<List>
实例来存放初值为 5, Nil
的 List
值。接着在变量 b
中创建了存放包含值 10 和指向列表 a
的 List
的另一个 Rc<List>
实例。
最后,修改 a
使其指向 b
而不是 Nil
,这就创建了一个循环。为此需要使用 tail
方法获取 a
中 RefCell<Rc<List>>
的引用,并放入变量 link
中。接着使用 RefCell<Rc<List>>
的 borrow_mut
方法将其值从存放 Nil
的 Rc<List>
修改为 b
中的 Rc<List>
。
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
可以看到将列表 a
修改为指向 b
之后, a
和 b
中的 Rc<List>
实例的引用计数都是 2。在 main
的结尾,Rust 丢弃 b
,这会使 b
Rc<List>
实例的引用计数从 2 减为 1。然而,b
Rc<List>
不能被回收,因为其引用计数是 1 而不是 0。接下来 Rust 会丢弃 a
将 a
Rc<List>
实例的引用计数从 2 减为 1。这个实例也不能被回收,因为 b
Rc<List>
实例依然引用它,所以其引用计数是 1。这些列表的内存将永远保持未被回收的状态。如图所示的引用循环:
如果取消最后 println!
的注释并运行程序,Rust 会尝试打印出 a
指向 b
指向 a
这样的循环直到栈溢出。
避免引用循环:将 Rc<T> 变为 Weak<T>。调用 Rc::clone
会增加 Rc<T>
实例的 strong_count
,和只在其 strong_count
为 0 时才会被清理的 Rc<T>
实例。你也可以通过调用 Rc::downgrade
并传递 Rc<T>
实例的引用来创建其值的 弱引用(weak reference)。强引用代表如何共享 Rc<T>
实例的所有权。弱引用并不属于所有权关系,当 Rc<T>
实例被清理时其计数没有影响。它们不会造成引用循环,因为任何涉及弱引用的循环会在其相关的值的强引用计数为 0 时被打断。
调用 Rc::downgrade
时会得到 Weak<T>
类型的智能指针。不同于将 Rc<T>
实例的 strong_count
加 1,调用 Rc::downgrade
会将 weak_count
加 1。Rc<T>
类型使用 weak_count
来记录其存在多少个 Weak<T>
引用,类似于 strong_count
。其区别在于 weak_count
无需计数为 0 就能使 Rc<T>
实例被清理。
强引用代表如何共享 Rc<T>
实例的所有权,但弱引用并不属于所有权关系。它们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断。
创建树形数据结构:带有子节点的 Node
use std::cell::RefCell;
use std::rc::Rc;#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
希望 Node
能够拥有其子节点,同时也希望能将所有权共享给变量,以便可以直接访问树中的每一个 Node
,为此 Vec<T>
的项的类型被定义为 Rc<Node>
。我们还希望能修改其他节点的子节点,所以 children
中 Vec<Rc<Node>>
被放进了 RefCell<T>
。
接下来,使用此结构体定义来创建一个叫做 leaf
的带有值 3 且没有子节点的 Node
实例,和另一个带有值 5 并以 leaf
作为子节点的实例 branch
- use std::cell::RefCell;
- use std::rc::{Rc, Weak};
-
- #[derive(Debug)]
- struct Node {
- value: i32,
- parent: RefCell<Weak<Node>>,
- children: RefCell<Vec<Rc<Node>>>,
- }
-
-
- fn main() {
- let leaf = Rc::new(Node {
- value: 3,
- parent: RefCell::new(Weak::new()),
- children: RefCell::new(vec![]),
- });
-
- println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
-
- let branch = Rc::new(Node {
- value: 5,
- parent: RefCell::new(Weak::new()),
- children: RefCell::new(vec![Rc::clone(&leaf)]),
- });
-
- *leaf.parent.borrow_mut() = Rc::downgrade(&branch);
-
- println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
- }
一个节点就能够引用其父节点,但不拥有其父节点。
可视化 strong_count 和 weak_count 的改变
- use std::cell::RefCell;
- use std::rc::{Rc, Weak};
-
- #[derive(Debug)]
- struct Node {
- value: i32,
- parent: RefCell<Weak<Node>>,
- children: RefCell<Vec<Rc<Node>>>,
- }
-
-
- fn main() {
- let leaf = Rc::new(Node {
- value: 3,
- parent: RefCell::new(Weak::new()),
- children: RefCell::new(vec![]),
- });
-
- println!(
- "leaf strong = {}, weak = {}",
- Rc::strong_count(&leaf),
- Rc::weak_count(&leaf),
- );
-
- {
- let branch = Rc::new(Node {
- value: 5,
- parent: RefCell::new(Weak::new()),
- children: RefCell::new(vec![Rc::clone(&leaf)]),
- });
-
- *leaf.parent.borrow_mut() = Rc::downgrade(&branch);
-
- println!(
- "branch strong = {}, weak = {}",
- Rc::strong_count(&branch),
- Rc::weak_count(&branch),
- );
-
- println!(
- "leaf strong = {}, weak = {}",
- Rc::strong_count(&leaf),
- Rc::weak_count(&leaf),
- );
- }
-
- println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
- println!(
- "leaf strong = {}, weak = {}",
- Rc::strong_count(&leaf),
- Rc::weak_count(&leaf),
- );
- }
一旦创建了 leaf
,其 Rc<Node>
的强引用计数为 1,弱引用计数为 0。在内部作用域中创建了 branch
并与 leaf
相关联,此时 branch
中 Rc<Node>
的强引用计数为 1,弱引用计数为 1(因为 leaf.parent
通过 Weak<Node>
指向 branch
)。这里 leaf
的强引用计数为 2,因为现在 branch
的 branch.children
中储存了 leaf
的 Rc<Node>
的拷贝,不过弱引用计数仍然为 0。
当内部作用域结束时,branch
离开作用域,Rc<Node>
的强引用计数减少为 0,所以其 Node
被丢弃。来自 leaf.parent
的弱引用计数 1 与 Node
是否被丢弃无关,所以并没有产生任何内存泄漏!
如果在内部作用域结束后尝试访问 leaf
的父节点,会再次得到 None
。在程序的结尾,leaf
中 Rc<Node>
的强引用计数为 1,弱引用计数为 0,因为现在 leaf
又是 Rc<Node>
唯一的引用了。
所有这些管理计数和值的逻辑都内建于 Rc<T>
和 Weak<T>
以及它们的 Drop
trait 实现中。通过在 Node
定义中指定从子节点到父节点的关系为一个Weak<T>
引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。
安全且高效地处理并发编程是 Rust 的另一个主要目标。
这两个概念随着计算机越来越多的利用多处理器的优势而显得愈发重要。
注意:出于简洁的考虑,将很多问题归类为 并发,而不是更准确的区分 并发和(或)并行。如果这是一本专注于并发和/或并行的书,我们肯定会更加精确的。对于本章,当我们谈到 并发 时,请自行脑内替换为 并发和(或)并行。
涉及到的内容:
Sync
和 Send
trait,将 Rust 的并发保证扩展到用户定义的以及标准库提供的类型中。在大部分现代操作系统中,已执行程序的代码在一个 进程(process)中运行,操作系统则会负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。这些运行这些独立部分的功能被称为 线程(threads)。例如,web 服务器可以有多个线程以便可以同时响应多个请求。
将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这会导致诸如此类的问题:
为了创建一个新线程,需要调用 thread::spawn
函数并传递一个闭包,并在其中包含希望在新线程运行的代码。
- use std::thread;
- use std::time::Duration;
-
- fn main() {
- thread::spawn(|| {
- for i in 1..10 {
- println!("hi number {} from the spawned thread!", i);
- thread::sleep(Duration::from_millis(1));
- }
- });
-
- for i in 1..5 {
- println!("hi number {} from the main thread!", i);
- thread::sleep(Duration::from_millis(1));
- }
- }
注意当 Rust 程序的主线程结束时,新线程也会结束,而不管其是否执行完毕。
thread::sleep
调用强制线程停止执行一小段时间,这会允许其他不同的线程运行。这些线程可能会轮流运行,不过并不保证如此:这依赖操作系统如何调度线程。在这里,主线程首先打印,即便新创建线程的打印语句位于程序的开头,甚至即便我们告诉新建的线程打印直到 i
等于 9,它在主线程结束之前也只打印到了 5。
- use std::thread;
-
- static NTHREADS: i32 = 10;
-
- // 这是主(`main`)线程
- fn main() {
- // 提供一个 vector 来存放所创建的子线程(children)。
- let mut children = vec![];
-
- for i in 0..NTHREADS {
- // 启动(spin up)另一个线程
- children.push(thread::spawn(move || {
- println!("this is thread number {}", i)
- }));
- }
-
- for child in children {
- // 等待线程结束。返回一个结果。
- let _ = child.join();
- }
- }
把它们分成几块,放入不同的线程。每个线程会把自己那一块数字的每一位加起来,之后我们再把每个线程提供的结果再加起来。
- use std::thread;
-
- // 这是 `main` 线程
- fn main() {
-
- // 这是我们要处理的数据。
- // 我们会通过线程实现 map-reduce 算法,从而计算每一位的和
- // 每个用空白符隔开的块都会分配给单独的线程来处理
- //
- // 试一试:插入空格,看看输出会怎样变化!
- let data = "86967897737416471853297327050364959
- 11861322575564723963297542624962850
- 70856234701860851907960690014725639
- 38397966707106094172783238747669219
- 52380795257888236525459303330302837
- 58495327135744041048897885734297812
- 69920216438980873548808413720956532
- 16278424637452589860345374828574668";
-
- // 创建一个向量,用于储存将要创建的子线程
- let mut children = vec![];
-
- /*************************************************************************
- * "Map" 阶段
- *
- * 把数据分段,并进行初始化处理
- ************************************************************************/
-
- // 把数据分段,每段将会单独计算
- // 每段都是完整数据的一个引用(&str)
- let chunked_data = data.split_whitespace();
-
- // 对分段的数据进行迭代。
- // .enumerate() 会把当前的迭代计数与被迭代的元素以元组 (index, element)
- // 的形式返回。接着立即使用 “解构赋值” 将该元组解构成两个变量,
- // `i` 和 `data_segment`。
- for (i, data_segment) in chunked_data.enumerate() {
- println!("data segment {} is \"{}\"", i, data_segment);
-
- // 用单独的线程处理每一段数据
- //
- // spawn() 返回新线程的句柄(handle),我们必须拥有句柄,
- // 才能获取线程的返回值。
- //
- // 'move || -> u32' 语法表示该闭包:
- // * 没有参数('||')
- // * 会获取所捕获变量的所有权('move')
- // * 返回无符号 32 位整数('-> u32')
- //
- // Rust 可以根据闭包的内容推断出 '-> u32',所以我们可以不写它。
- //
- // 试一试:删除 'move',看看会发生什么
- children.push(thread::spawn(move || -> u32 {
- // 计算该段的每一位的和:
- let result = data_segment
- // 对该段中的字符进行迭代..
- .chars()
- // ..把字符转成数字..
- .map(|c| c.to_digit(10).expect("should be a digit"))
- // ..对返回的数字类型的迭代器求和
- .sum();
-
- // println! 会锁住标准输出,这样各线程打印的内容不会交错在一起
- println!("processed segment {}, result={}", i, result);
-
- // 不需要 “return”,因为 Rust 是一种 “表达式语言”,每个代码块中
- // 最后求值的表达式就是代码块的值。
- result
-
- }));
- }
-
-
- /*************************************************************************
- * "Reduce" 阶段
- *
- * 收集中间结果,得出最终结果
- ************************************************************************/
-
- // 把每个线程产生的中间结果收入一个新的向量中
- let mut intermediate_sums = vec![];
- for child in children {
- // 收集每个子线程的返回值
- let intermediate_sum = child.join().unwrap();
- intermediate_sums.push(intermediate_sum);
- }
-
- // 把所有中间结果加起来,得到最终结果
- //
- // 我们用 “涡轮鱼” 写法 ::<> 来为 sum() 提供类型提示。
- //
- // 试一试:不使用涡轮鱼写法,而是显式地指定 intermediate_sums 的类型
- let final_result = intermediate_sums.iter().sum::<u32>();
-
- println!("Final sum result: {}", final_result);
- }
-
-
由于主线程结束,上面示例代码大部分时候不光会提早结束新建线程,因为无法保证线程运行的顺序,我们甚至不能实际保证新建线程会被执行!可以通过将 thread::spawn
的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。thread::spawn
的返回值类型是 JoinHandle
。JoinHandle
是一个拥有所有权的值,当对其调用 join
方法时,它会等待其线程结束。将 join
调用放在了主线程的 for
循环之后
- use std::thread;
- use std::time::Duration;
-
- fn main() {
- let handle = thread::spawn(|| {
- for i in 1..10 {
- println!("hi number {} from the spawned thread!", i);
- thread::sleep(Duration::from_millis(1));
- }
- });
-
- for i in 1..5 {
- println!("hi number {} from the main thread!", i);
- thread::sleep(Duration::from_millis(1));
- }
-
- handle.join().unwrap();
- }
调用 handle 的 join
会阻塞当前线程直到 handle 所代表的线程结束。阻塞(Blocking)线程意味着阻止该线程执行工作或退出。上面代码中,两个线程仍然会交替执行,不过主线程会由于 handle.join()
调用会等待直到新建线程执行完毕。
move
关键字经常用于传递给 thread::spawn
的闭包,因为闭包会获取从环境中取得的值的所有权,因此会将这些值的所有权从一个线程传送到另一个线程。在 “闭包会捕获其环境” 部分讨论了闭包上下文中的 move
。现在专注于 move
和 thread::spawn
之间的交互。
使用 move
关键字强制闭包获取其使用的环境值的所有权。这个技巧在创建新线程将值的所有权从一个线程移动到另一个线程时最为实用。
闭包之前增加 move
关键字,我们强制闭包获取其使用的值的所有权,
- use std::thread;
-
- fn main() {
- let v = vec![1, 2, 3];
-
- let handle = thread::spawn(move || {
- println!("Here's a vector: {:?}", v);
- });
-
- handle.join().unwrap();
- }
通过 消息传递(message passing)确保安全并发。这个思想来源于 Go 编程语言文档中 的口号:“不要通过共享内存来通讯;而是通过通讯来共享内存。”(“Do not communicate by sharing memory; instead, share memory by communicating.”)。Rust 为线程之间的通信提供了异步的通道(channel
)。通道允许两个端点之间信息的单向流动:Sender
(发送端) 和 Receiver
(接收端)。信道都是单所有权,因为一旦将一个值传送到信道中,将无法再使用这个值
- use std::sync::mpsc::{Sender, Receiver};
- use std::sync::mpsc;
- use std::thread;
-
- static NTHREADS: i32 = 3;
-
- fn main() {
- // 通道有两个端点:`Sender<T>` 和 `Receiver<T>`,其中 `T` 是要发送
- // 的消息的类型(类型标注是可选的)
- let (tx, rx): (Sender<i32>, Receiver<i32>) = mpsc::channel();
-
- for id in 0..NTHREADS {
- // sender 端可被复制
- let thread_tx = tx.clone();
-
- // 每个线程都将通过通道来发送它的 id
- thread::spawn(move || {
- // 被创建的线程取得 `thread_tx` 的所有权
- // 每个线程都把消息放在通道的消息队列中
- thread_tx.send(id).unwrap();
-
- // 发送是一个非阻塞(non-blocking)操作,线程将在发送完消息后
- // 会立即继续进行
- println!("thread {} finished", id);
- });
- }
-
- // 所有消息都在此处被收集
- let mut ids = Vec::with_capacity(NTHREADS as usize);
- for _ in 0..NTHREADS {
- // `recv` 方法从通道中拿到一个消息
- // 若无可用消息的话,`recv` 将阻止当前线程
- ids.push(rx.recv());
- }
-
- // 显示消息被发送的次序
- println!("{:?}", ids);
- }
示例:在一个线程生成值向信道发送,而在另一个线程会接收值并打印出来。一旦你熟悉了这项技术,你就可以将信道用于任何相互通信的任何线程,例如一个聊天系统,或利用很多线程进行分布式计算并将部分计算结果发送给一个线程进行聚合。
- use std::sync::mpsc;
- use std::thread;
-
- fn main() {
- let (tx, rx) = mpsc::channel();
-
- thread::spawn(move || {
- let val = String::from("hi");
- tx.send(val).unwrap();
- });
-
- let received = rx.recv().unwrap();
- println!("Got: {}", received);
- }
使用 thread::spawn
来创建一个新线程并使用 move
将 tx
移动到闭包中这样新建线程就拥有 tx
了。新建线程需要拥有信道的发送端以便能向信道发送消息。信道的发送端有一个 send
方法用来获取需要放入信道的值。send
方法返回一个 Result<T, E>
类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 unwrap
产生 panic。不过对于一个真实程序,需要合理地处理它
信道的接收者有两个有用的方法:recv
和 try_recv
。这里,我们使用了 recv
,它是 receive 的缩写。这个方法会阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值,recv
会在一个 Result<T, E>
中返回它。当信道发送端关闭,recv
会返回一个错误表明不会再有新的值到来了。
try_recv
不会阻塞,相反它立刻返回一个 Result<T, E>
:Ok
值包含可用的信息,而 Err
值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用 try_recv
很有用:可以编写一个循环来频繁调用 try_recv
,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。
出于简单的考虑,这个例子使用了 recv
;主线程中除了等待消息之外没有任何其他工作,所以阻塞主线程是合适的。
信道与所有权转移。
所有权规则在消息传递中扮演了重要角色,其有助于我们编写安全的并发代码。也可以防止并发编程中的错误。
send
函数获取其参数的所有权并移动这个值归接收者所有。这可以防止在发送后再次意外地使用这个值;所有权系统检查一切是否合乎规则。示例:信道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的信道中发送完
val
值 之后 再使用它。
- use std::sync::mpsc;
- use std::thread;
-
- fn main() {
- let (tx, rx) = mpsc::channel();
-
- thread::spawn(move || {
- let val = String::from("hi");
- tx.send(val).unwrap();
- println!("val is {}", val);
- });
-
- let received = rx.recv().unwrap();
- println!("Got: {}", received);
- }
发送多个值并观察接收者的等待
- use std::sync::mpsc;
- use std::thread;
- use std::time::Duration;
-
- fn main() {
- let (tx, rx) = mpsc::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::from_secs(1));
- }
- });
-
- for received in rx {
- println!("Got: {}", received);
- }
- }
这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历它们,单独的发送每一个字符串并通过一个 Duration
值调用 thread::sleep
函数来暂停一秒。
在主线程中,不再显式调用 recv
函数:而是将 rx
当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当信道被关闭时,迭代器也将结束。
通过克隆发送者来创建多个生产者
- use std::sync::mpsc;
- use std::thread;
- use std::time::Duration;
-
- fn main() {
- // --snip--
-
- let (tx, rx) = mpsc::channel();
-
- let tx1 = tx.clone();
- thread::spawn(move || {
- let vals = vec![
- String::from("hi"),
- String::from("from"),
- String::from("the"),
- String::from("thread"),
- ];
-
- for val in vals {
- tx1.send(val).unwrap();
- thread::sleep(Duration::from_secs(1));
- }
- });
-
- thread::spawn(move || {
- let vals = vec![
- String::from("more"),
- String::from("messages"),
- String::from("for"),
- String::from("you"),
- ];
-
- for val in vals {
- tx.send(val).unwrap();
- thread::sleep(Duration::from_secs(1));
- }
- });
-
- for received in rx {
- println!("Got: {}", received);
- }
-
- // --snip--
- }
在创建新线程之前,我们对发送者调用了 clone
方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的信道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向信道的接收端发送不同的消息。
共享状态 实现并发
虽然消息传递是一个很好的处理并发的方式,但并不是唯一一个。另一种方式是让多个线程拥有相同的共享数据。再一次思考一下 Go 编程语言文档中口号的这一部分:“不要通过共享内存来通讯”(“do not communicate by sharing memory.”):
在某种程度上,任何编程语言中的信道都类似于单所有权,因为一旦将一个值传送到信道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。介绍智能指针时,如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。Rust 的类型系统和所有权规则极大的协助了正确地管理这些所有权。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。
互斥器(mutex)是 mutual exclusion 的缩写,也就是说,任意时刻,其只允许一个线程访问某些数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的 锁(lock)来表明其希望访问数据。锁是一个作为互斥器一部分的数据结构,它记录谁有数据的排他访问权。因此,我们描述互斥器为通过锁系统 保护(guarding)其数据。
作为一个现实中互斥器的例子,想象一下在某个会议的一次小组座谈会中,只有一个麦克风。如果一位成员要发言,他必须请求或表示希望使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一位希望讲话的成员。如果一位成员结束发言后忘记将麦克风交还,其他人将无法发言。如果对共享麦克风的管理出现了问题,座谈会将无法如期进行!
正确的管理互斥器异常复杂,这也是许多人之所以热衷于信道的原因。然而,在 Rust 中,得益于类型系统和所有权,不会在锁和解锁上出错。
多线程和多所有权
- use std::rc::Rc;
- use std::sync::Mutex;
- use std::thread;
-
- fn main() {
- let counter = Rc::new(Mutex::new(0));
- let mut handles = vec![];
-
- for _ in 0..10 {
- let counter = Rc::clone(&counter);
- let handle = thread::spawn(move || {
- let mut num = counter.lock().unwrap();
-
- *num += 1;
- });
- handles.push(handle);
- }
-
- for handle in handles {
- handle.join().unwrap();
- }
-
- println!("Result: {}", *counter.lock().unwrap());
- }
直接报错,因为 Rc<T>
并不能安全的在线程间共享。当 Rc<T>
管理引用计数时,它必须在每一个 clone
调用时增加计数,并在每一个克隆被丢弃时减少计数。Rc<T>
并没有使用任何并发原语,来确保改变计数的操作不会被其他线程打断。在计数出错时可能会导致诡异的 bug,比如可能会造成内存泄漏,或在使用结束之前就丢弃一个值。我们所需要的是一个完全类似 Rc<T>
,又以一种线程安全的方式改变引用计数的类型。
原子引用计数 Arc<T>。Arc<T>
正是 这么一个类似 Rc<T>
并可以安全的用于并发环境的类型。字母 “a” 代表 原子性(atomic),所以这是一个 原子引用计数(atomically reference counted)类型。原子性是另一类这里还未涉及到的并发原语:请查看标准库中 std::sync::atomic 的文档来获取更多细节。目前我们只需要知道原子类就像基本类型一样可以安全的在线程间共享。
- use std::sync::{Arc, Mutex};
- use std::thread;
-
- fn main() {
- let counter = Arc::new(Mutex::new(0));
- let mut handles = vec![];
-
- for _ in 0..10 {
- let counter = Arc::clone(&counter);
- let handle = thread::spawn(move || {
- let mut num = counter.lock().unwrap();
-
- *num += 1;
- });
- handles.push(handle);
- }
-
- for handle in handles {
- handle.join().unwrap();
- }
-
- println!("Result: {}", *counter.lock().unwrap());
- }
使用 Sync 和 Send trait 的可扩展并发
Rust 的并发模型中一个有趣的方面是:语言本身对并发知之 甚少。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的并发功能。
然而有两个并发概念是内嵌于语言中的:
std::marker
中的Sync
和Send
trait。
因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。它们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。
process::Output
结构体表示已结束的子进程(child process)的输出,而 process::Command
结构体是一个进程创建者(process builder)。
- use std::process::Command;
-
- fn main() {
- let output = Command::new("rustc")
- .arg("--version")
- .output().unwrap_or_else(|e| {
- panic!("failed to execute process: {}", e)
- });
-
- if output.status.success() {
- let s = String::from_utf8_lossy(&output.stdout);
-
- print!("rustc succeeded and stdout was:\n{}", s);
- } else {
- let s = String::from_utf8_lossy(&output.stderr);
-
- print!("rustc failed and stderr was:\n{}", s);
- }
- }
std::Child
结构体代表了一个正在运行的子进程,它暴露了 stdin
(标准输入),stdout
(标准输出)和 stderr
(标准错误)句柄,从而可以通过管道与所代表的进程交互。
- use std::io::prelude::*;
- use std::process::{Command, Stdio};
-
- static PANGRAM: &'static str =
- "the quick brown fox jumped over the lazy dog\n";
-
- fn main() {
- // 启动 `wc` 命令
- let process = match Command::new("wc")
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .spawn() {
- Err(why) => panic!("couldn't spawn wc: {:?}", why),
- Ok(process) => process,
- };
-
- // 将字符串写入 `wc` 的 `stdin`。
- //
- // `stdin` 拥有 `Option<ChildStdin>` 类型,不过我们已经知道这个实例不为空值,
- // 因而可以直接 `unwrap 它。
- match process.stdin.unwrap().write_all(PANGRAM.as_bytes()) {
- Err(why) => panic!("couldn't write to wc stdin: {:?}", why),
- Ok(_) => println!("sent pangram to wc"),
- }
-
- // 因为 `stdin` 在上面调用后就不再存活,所以它被 `drop` 了,管道也被关闭。
- //
- // 这点非常重要,因为否则 `wc` 就不会开始处理我们刚刚发送的输入。
-
- // `stdout` 字段也拥有 `Option<ChildStdout>` 类型,所以必需解包。
- let mut s = String::new();
- match process.stdout.unwrap().read_to_string(&mut s) {
- Err(why) => panic!("couldn't read wc stdout: {:?}", why),
- Ok(_) => print!("wc responded with:\n{}", s),
- }
- }
等待一个 process::Child
完成,就必须调用 Child::wait
,这会返回一个 process::ExitStatus
。
- use std::process::Command;
-
- fn main() {
- let mut child = Command::new("sleep").arg("5").spawn().unwrap();
- let _result = child.wait().unwrap();
-
- println!("reached end of main");
- }
std::io::fs 模块包含几个处理文件系统的函数。
- use std::fs;
- use std::fs::{File, OpenOptions};
- use std::io;
- use std::io::prelude::*;
- use std::os::unix;
- use std::path::Path;
-
- // `% cat path` 的简单实现
- fn cat(path: &Path) -> io::Result<String> {
- let mut f = File::open(path)?;
- let mut s = String::new();
- match f.read_to_string(&mut s) {
- Ok(_) => Ok(s),
- Err(e) => Err(e),
- }
- }
-
- // `% echo s > path` 的简单实现
- fn echo(s: &str, path: &Path) -> io::Result<()> {
- let mut f = File::create(path)?;
-
- f.write_all(s.as_bytes())
- }
-
- // `% touch path` 的简单实现(忽略已存在的文件)
- fn touch(path: &Path) -> io::Result<()> {
- match OpenOptions::new().create(true).write(true).open(path) {
- Ok(_) => Ok(()),
- Err(e) => Err(e),
- }
- }
-
- fn main() {
- println!("`mkdir a`");
- // 创建一个目录,返回 `io::Result<()>`
- match fs::create_dir("a") {
- Err(why) => println!("! {:?}", why.kind()),
- Ok(_) => {},
- }
-
- println!("`echo hello > a/b.txt`");
- // 前面的匹配可以用 `unwrap_or_else` 方法简化
- echo("hello", &Path::new("a/b.txt")).unwrap_or_else(|why| {
- println!("! {:?}", why.kind());
- });
-
- println!("`mkdir -p a/c/d`");
- // 递归地创建一个目录,返回 `io::Result<()>`
- fs::create_dir_all("a/c/d").unwrap_or_else(|why| {
- println!("! {:?}", why.kind());
- });
-
- println!("`touch a/c/e.txt`");
- touch(&Path::new("a/c/e.txt")).unwrap_or_else(|why| {
- println!("! {:?}", why.kind());
- });
-
- println!("`ln -s ../b.txt a/c/b.txt`");
- // 创建一个符号链接,返回 `io::Resutl<()>`
- if cfg!(target_family = "unix") {
- unix::fs::symlink("../b.txt", "a/c/b.txt").unwrap_or_else(|why| {
- println!("! {:?}", why.kind());
- });
- }
-
- println!("`cat a/c/b.txt`");
- match cat(&Path::new("a/c/b.txt")) {
- Err(why) => println!("! {:?}", why.kind()),
- Ok(s) => println!("> {}", s),
- }
-
- println!("`ls a`");
- // 读取目录的内容,返回 `io::Result<Vec<Path>>`
- match fs::read_dir("a") {
- Err(why) => println!("! {:?}", why.kind()),
- Ok(paths) => for path in paths {
- println!("> {:?}", path.unwrap().path());
- },
- }
-
- println!("`rm a/c/e.txt`");
- // 删除一个文件,返回 `io::Result<()>`
- fs::remove_file("a/c/e.txt").unwrap_or_else(|why| {
- println!("! {:?}", why.kind());
- });
-
- println!("`rmdir a/c/d`");
- // 移除一个空目录,返回 `io::Result<()>`
- fs::remove_dir("a/c/d").unwrap_or_else(|why| {
- println!("! {:?}", why.kind());
- });
- }
另一种定义 cat
函数的方式是使用 ?
标记:
- fn cat(path: &Path) -> io::Result<String> {
- let mut f = File::open(path)?;
- let mut s = String::new();
- f.read_to_string(&mut s)?;
- Ok(s)
- }
Path
结构体代表了底层文件系统的文件路径。Path
分为两种:posix::Path
,针对类 UNIX 系统;以及 windows::Path
,针对 Windows。prelude 会选择并输出符合平台类型的 Path
种类。
prelude 是 Rust 自动地在每个程序中导入的一些通用的东西,这样我们就不必每写 一个程序就手动导入一番。
Path
可从 OsStr
类型创建,并且它提供数种方法,用于获取路径指向的文件/目录的信息。
注意 Path
在内部并不是用 UTF-8 字符串表示的,而是存储为若干字节(Vec<u8>
)的 vector。因此,将 Path
转化成 &str
并非零开销的(free),且可能失败(因此它返回一个 Option
)。
- use std::path::Path;
-
- fn main() {
- // 从 `&'static str` 创建一个 `Path`
- let path = Path::new(".");
-
- // `display` 方法返回一个可显示(showable)的结构体
- let display = path.display();
-
- // `join` 使用操作系统特定的分隔符来合并路径到一个字节容器,并返回新的路径
- let new_path = path.join("a").join("b");
-
- // 将路径转换成一个字符串切片
- match new_path.to_str() {
- None => panic!("new path is not a valid UTF-8 sequence"),
- Some(s) => println!("new path is {}", s),
- }
- }
File
结构体表示一个被打开的文件(它包裹了一个文件描述符),并赋予了对所表示的文件的读写能力。由于在进行文件 I/O(输入/输出)操作时可能出现各种错误,因此 File
的所有方法都返回 io::Result<T>
类型,它是 Result<T, io::Error>
的别名。
open
静态方法能够以只读模式(read-only mode)打开一个文件。
File
拥有资源,即文件描述符(file descriptor),它会在自身被 drop
时关闭文件。
- use std::fs::File;
- use std::io::prelude::*;
- use std::path::Path;
-
- fn main() {
- // 创建指向所需的文件的路径
- let path = Path::new("hello.txt");
- let display = path.display();
-
- // 以只读方式打开路径,返回 `io::Result<File>`
- let mut file = match File::open(&path) {
- // `io::Error` 的 `description` 方法返回一个描述错误的字符串。
- Err(why) => panic!("couldn't open {}: {:?}", display, why),
- Ok(file) => file,
- };
-
- // 读取文件内容到一个字符串,返回 `io::Result<usize>`
- let mut s = String::new();
- match file.read_to_string(&mut s) {
- Err(why) => panic!("couldn't read {}: {:?}", display, why),
- Ok(_) => print!("{} contains:\n{}", display, s),
- }
-
- // `file` 离开作用域,并且 `hello.txt` 文件将被关闭。
- }
create
静态方法以只写模式(write-only mode)打开一个文件。若文件已经存在,则旧内容将被销毁。否则,将创建一个新文件。
- static LOREM_IPSUM: &'static str =
- "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
- tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
- quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
- consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
- cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
- proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
- ";
-
- use std::io::prelude::*;
- use std::fs::File;
- use std::path::Path;
-
- fn main() {
- let path = Path::new("out/lorem_ipsum.txt");
- let display = path.display();
-
- // 以只写模式打开文件,返回 `io::Result<File>`
- let mut file = match File::create(&path) {
- Err(why) => panic!("couldn't create {}: {:?}", display, why),
- Ok(file) => file,
- };
-
- // 将 `LOREM_IPSUM` 字符串写进 `file`,返回 `io::Result<()>`
- match file.write_all(LOREM_IPSUM.as_bytes()) {
- Err(why) => {
- panic!("couldn't write to {}: {:?}", display, why)
- },
- Ok(_) => println!("successfully wrote to {}", display),
- }
- }
方法 lines()
在文件的行上返回一个迭代器。File::open
需要一个泛型 AsRef<Path>
。这正是 read_lines()
期望的输入。
- use std::fs::File;
- use std::io::{self, BufRead};
- use std::path::Path;
-
- fn main() {
- // 在生成输出之前,文件主机必须存在于当前路径中
- if let Ok(lines) = read_lines("./hosts") {
- // 使用迭代器,返回一个(可选)字符串
- for line in lines {
- if let Ok(ip) = line {
- println!("{}", ip);
- }
- }
- }
- }
-
- // 输出包裹在 Result 中以允许匹配错误,
- // 将迭代器返回给文件行的读取器(Reader)。
- fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
- where P: AsRef<Path>, {
- let file = File::open(filename)?;
- Ok(io::BufReader::new(file).lines())
- }
面向对象编程语言所共享的一些特性往往是 对象、且对象有 "封装、继承、多态"
在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。
impl
块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被 称为 "对象",但是它们提供了与对象相同的功能。pub
关键字可以决定 模块、类型、函数和方法是公有的,没有pub时默认都是私有的。那么 Rust 满足 封装 这个要求。在代码中不同的部分使用 pub
与否可以封装其实现细节。示例:在结构体上实现 add
、remove
和 average
方法,并通过 pub 暴露给外部进行调用
- impl AveragedCollection {
- pub fn add(&mut self, value: i32) {
- self.list.push(value);
- self.update_average();
- }
-
- pub fn remove(&mut self) -> Option<i32> {
- let result = self.list.pop();
- match result {
- Some(value) => {
- self.update_average();
- Some(value)
- }
- None => None,
- }
- }
-
- pub fn average(&self) -> f64 {
- self.average
- }
-
- fn update_average(&mut self) {
- let total: i32 = self.list.iter().sum();
- self.average = total as f64 / self.list.len() as f64;
- }
- }
总结
Rust 刻意不将结构体与枚举称为 "对象" ,以便与其他语言中的对象相区别。在结构体或枚举中,结构体字段中的数据和 impl
块中的行为是分开的,"trait 对象" 将数据和行为两者相结合,但是不能向 "trait 对象" 增加数据。"trait 对象" 具体的作用是允许对通用行为进行抽象。
必须带上类型标注,才能实现多态调用。
- struct Sheep { naked: bool, name: &'static str }
-
- trait Animal {
- // 静态方法签名;`Self` 表示实现者类型(implementor type)。
- fn new(name: &'static str) -> Self;
-
- // 实例方法签名;这些方法将返回一个字符串。
- fn name(&self) -> &'static str;
- fn noise(&self) -> &'static str;
-
- // trait 可以提供默认的方法定义。
- fn talk(&self) {
- println!("{} says {}", self.name(), self.noise());
- }
- }
-
- impl Sheep {
- fn is_naked(&self) -> bool {
- self.naked
- }
-
- fn shear(&mut self) {
- if self.is_naked() {
- // 实现者可以使用它的 trait 方法。
- println!("{} is already naked...", self.name());
- } else {
- println!("{} gets a haircut!", self.name);
-
- self.naked = true;
- }
- }
- }
-
- // 对 `Sheep` 实现 `Animal` trait。
- impl Animal for Sheep {
- // `Self` 是实现者类型:`Sheep`。
- fn new(name: &'static str) -> Sheep {
- Sheep { name: name, naked: false }
- }
-
- fn name(&self) -> &'static str {
- self.name
- }
-
- fn noise(&self) -> &'static str {
- if self.is_naked() {
- "baaaaah?"
- } else {
- "baaaaah!"
- }
- }
-
- // 默认 trait 方法可以重载。
- fn talk(&self) {
- // 例如我们可以增加一些安静的沉思。
- println!("{} pauses briefly... {}", self.name, self.noise());
- }
- }
-
- fn main() {
- // 这种情况需要类型标注。
- let mut dolly: Sheep = Animal::new("Dolly");
- // 试一试 ^ 移除类型标注。
-
- dolly.talk();
- dolly.shear();
- dolly.talk();
- }
示例 2:
- // 带有生命周期标注的结构体。
- #[derive(Debug)]
- struct Borrowed<'a> {
- x: &'a i32,
- }
-
- // 给 impl 标注生命周期。
- impl<'a> Default for Borrowed<'a> {
- fn default() -> Self {
- Self {
- x: &10,
- }
- }
- }
-
- fn main() {
- let b: Borrowed = Default::default(); // 必须带上类型标注,才能实现多态调用。
- println!("b is {:?}", b);
- }
示例 3:
- use std::fmt::Debug;
-
- trait PrintInOption {
- fn print_in_option(self);
- }
-
- // 这里需要一个 `where` 从句,否则就要表达成 `T: Debug`(这样意思就变了),
- // 或者改用另一种间接的方法。
- impl<T> PrintInOption for T where
- Option<T>: Debug {
- // 我们要将 `Option<T>: Debug` 作为约束,因为那是要打印的内容。
- // 否则我们会给出错误的约束。
- fn print_in_option(self) {
- println!("{:?}", Some(self));
- }
- }
-
- fn main() {
- let vec = vec![1, 2, 3];
-
- vec.print_in_option();
- }
派生本指江河的源头产生出支流,引申为从一个主要事物的发展中分化出来。
继承和派生从两个不同的角度来说明的,举例如下:
- B 是 A 的子类,则可以说 B 继承 A,是 A 的派生类
- 如果C又是B的子类,就不能说C继承A,而只能说C是A的派生类。其实就是隔代的问题
参考:derive
通过 #[derive]
属性,编译器能够提供某些 trait 的基本实现。如果需要更复杂的行为,这些 trait 也可以手动实现。
下面是可以自动派生的 trait:
&T
创建副本 T
。&T
计算哈希值(hash)。{:?}
formatter 来格式化一个值。- // `Centimeters`,可以比较的元组结构体
- #[derive(PartialEq, PartialOrd)]
- struct Centimeters(f64);
-
- // `Inches`,可以打印的元组结构体
- #[derive(Debug)]
- struct Inches(i32);
-
- impl Inches {
- fn to_centimeters(&self) -> Centimeters {
- let &Inches(inches) = self;
- Centimeters(inches as f64 * 2.54)
- }
- }
-
- // `Seconds`,不带附加属性的元组结构体
- struct Seconds(i32);
-
- fn main() {
- let _one_second = Seconds(1);
- // 报错:`Seconds` 不能打印;它没有实现 `Debug` trait
- //println!("One second looks like: {:?}", _one_second);
- // 试一试 ^ 取消此行注释
-
- // 报错:`Seconds`不能比较;它没有实现 `PartialEq` trait
- //let _this_is_true = (_one_second == _one_second);
- // 试一试 ^ 取消此行注释
-
- let foot = Inches(12);
- println!("One foot equals {:?}", foot);
- let meter = Centimeters(100.0);
- let cmp =
- if foot.to_centimeters() < meter {
- "smaller"
- } else {
- "bigger"
- };
- println!("One foot is {} than one meter.", cmp);
- }
dyn
是一个关键字,用于声明动态分发(dynamic dispatch)的 trait 对象。即当使用 trait 对象实现多态时,必须使用dyn修饰。
Rust 编译器需要知道每个函数的返回类型需要多少空间。这意味着所有函数都必须返回一个具体类型。与其他语言不同,如果你有个像 Animal
那样的的 trait,则不能编写返回 Animal
的函数,因为其不同的实现将需要不同的内存量。
但是,有一个简单的解决方法。相比于直接返回一个 trait 对象,我们的函数返回一个包含一些 Animal
的 Box
。box
只是对堆中某些内存的引用。因为引用的大小是静态已知的,并且编译器可以保证引用指向已分配的堆 Animal
,所以我们可以从函数中返回 trait!
每当在堆上分配内存时,Rust 都会尝试尽可能明确。因此,如果你的函数以这种方式返回指向堆的 trait 指针,则需要使用 dyn
关键字编写返回类型,例如 Box<dyn Animal>
。
示例:
- struct Sheep {}
- struct Cow {}
-
- trait Animal {
- // 实例方法签名
- fn noise(&self) -> &'static str;
- }
-
- // 实现 `Sheep` 的 `Animal` trait。
- impl Animal for Sheep {
- fn noise(&self) -> &'static str {
- "baaaaah!"
- }
- }
-
- // 实现 `Cow` 的 `Animal` trait。
- impl Animal for Cow {
- fn noise(&self) -> &'static str {
- "moooooo!"
- }
- }
-
- // 返回一些实现 Animal 的结构体,但是在编译时我们不知道哪个结构体。
- fn random_animal(random_number: f64) -> Box<dyn Animal> {
- if random_number < 0.5 {
- Box::new(Sheep {})
- } else {
- Box::new(Cow {})
- }
- }
-
- fn main() {
- let random_number = 0.234;
- let animal = random_animal(random_number);
- println!("You've randomly chosen an animal, and it says {}", animal.noise());
- }
-
示例:使用 dyn
关键字可以创建一个指向实现了特定 trait 的类型的 trait 对象。
"父类" 的说法是来自C++,这里是为了更好的理解。其实更像Java中的 "父接口"
动态分发会阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。
- // 相当于定义接口
- pub trait Draw {
- // 接口中的方法列表,
- fn draw(&self); // 声明方法(没有方法体的实现)
- }
- //####################################################################
- pub struct Button {
- // 定义 button, 通过 pub 暴露给外部
- pub width: u32,
- pub height: u32,
- pub label: String,
- }
- // button "继承" 接口
- impl Draw for Button {
- // 实现接口的方法
- fn draw(&self) {
- // code to actually draw a button
- }
- }
-
- //####################################################################
- struct SelectBox {
- width: u32,
- height: u32,
- options: Vec<String>,
- }
-
- impl Draw for SelectBox {
- fn draw(&self) {
- // code to actually draw a select box
- }
- }
- //####################################################################
- //一个 Screen 结构体的定义,它带有一个字段 components,
- // 其包含实现了 Draw trait 的 trait 对象的 vector
- pub struct Screen {
- pub components: Vec<Box<dyn Draw>>,
- }
- impl Screen {
- // 在 Screen 结构体上,定义一个 run 方法,
- // 该方法会对其 components 上的每一个组件调用 draw 方法
- pub fn run(&self) {
- for component in self.components.iter() {
- component.draw();
- }
- }
- }
- //####################################################################
- fn main() {
- let screen = Screen {
- components: vec![
- Box::new(SelectBox {
- width: 75,
- height: 10,
- options: vec![
- String::from("Yes"),
- String::from("Maybe"),
- String::from("No"),
- ],
- }),
- Box::new(Button {
- width: 50,
- height: 10,
- label: String::from("OK"),
- }),
- ],
- };
- screen.run();
- }
在 Rust 中,很多运算符可以通过 trait 来重载。也就是说,这些运算符可以根据它们的输入参数来完成不同的任务。这之所以可行,是因为运算符就是方法调用的语法糖。例如,a + b
中的 +
运算符会调用 add
方法(也就是 a.add(b)
)。这个 add
方法是 Add
trait 的一部分。因此,+
运算符可以被任何 Add
trait 的实现者使用。
会重载运算符的 trait
(比如 Add
这种)可以在这里查看。
- use std::ops;
-
- struct Foo;
- struct Bar;
-
- #[derive(Debug)]
- struct FooBar;
-
- #[derive(Debug)]
- struct BarFoo;
-
- // `std::ops::Add` trait 用来指明 `+` 的功能,这里我们实现 `Add<Bar>`,它是用于
- // 把对象和 `Bar` 类型的右操作数(RHS)加起来的 `trait`。
- // 下面的代码块实现了 `Foo + Bar = FooBar` 这样的运算。
- impl ops::Add<Bar> for Foo {
- type Output = FooBar;
-
- fn add(self, _rhs: Bar) -> FooBar {
- println!("> Foo.add(Bar) was called");
-
- FooBar
- }
- }
-
- // 通过颠倒类型,我们实现了不服从交换律的加法。
- // 这里我们实现 `Add<Foo>`,它是用于把对象和 `Foo` 类型的右操作数加起来的 trait。
- // 下面的代码块实现了 `Bar + Foo = BarFoo` 这样的运算。
- impl ops::Add<Foo> for Bar {
- type Output = BarFoo;
-
- fn add(self, _rhs: Foo) -> BarFoo {
- println!("> Bar.add(Foo) was called");
-
- BarFoo
- }
- }
-
- fn main() {
- println!("Foo + Bar = {:?}", Foo + Bar);
- println!("Bar + Foo = {:?}", Bar + Foo);
- }
Drop trait 只有一个方法:drop
,当对象离开作用域时会自动调用该方法。Drop
trait 的主要作用是释放实现者的实例拥有的资源。Box
,Vec
,String
,File
,以及 Process
都实现了 Drop
trait 来释放资源的类型。Drop
trait 也可以为任何自定义数据类型手动实现。
示例:给 drop
函数增加了打印到控制台的功能
- struct Droppable {
- name: &'static str,
- }
-
- // 这个简单的 `drop` 实现添加了打印到控制台的功能。
- impl Drop for Droppable {
- fn drop(&mut self) {
- println!("> Dropping {}", self.name);
- }
- }
-
- fn main() {
- let _a = Droppable { name: "a" };
- // 代码块 A
- {
- let _b = Droppable { name: "b" };
- // 代码块 B
- {
- let _c = Droppable { name: "c" };
- let _d = Droppable { name: "d" };
- println!("Exiting block B");
- }
- println!("Just exited block B");
- println!("Exiting block A");
- }
- println!("Just exited block A");
- // 变量可以手动使用 `drop` 函数来销毁。
- drop(_a);
- // 试一试 ^ 将此行注释掉。
- println!("end of the main function");
- // `_a` *不会*在这里再次销毁,因为它已经被(手动)销毁。
- }
状态模式(state pattern)是一个面向对象设计模式。该模式的关键在于定义一系列值的内含状态。这些状态体现为一系列的 状态对象,同时值的行为随着其内部状态而改变。
take() 方法获取并清空某个对象的可变状态。这种用法通常与 Option<T>
类型一起使用。Option<T>
是一个泛型枚举类型,表示一个可能存在或可能不存在的值。它有两个变体:Some(T)
表示存在一个值 T
,None
表示不存在值。当我们需要获取某个对象的状态,并在同一时间将其重置为初始状态时,可以使用 self.state.take()
。take()
方法从 Option<T>
中获取值并将其替换为 None
,以确保状态被取走后不再存在。
- struct MyStruct {
- state: Option<String>,
- }
-
- impl MyStruct {
- fn take_state(&mut self) -> Option<String> {
- self.state.take()
- }
- }
-
- fn main() {
- let mut my_struct = MyStruct {
- state: Some("Hello".to_string()),
- };
-
- let state = my_struct.take_state();
- println!("{:?}", state); // 输出 Some("Hello")
-
- let empty_state = my_struct.take_state();
- println!("{:?}", empty_state); // 输出 None
- }
示例:编写一个博客发布结构体的例子,它拥有一个包含其状态的字段,这是一个有着 "draft"、"review" 或 "published" 的状态对象
- //###############################################################
- trait State {
- fn request_review(self: Box<Self>) -> Box<dyn State>;
- fn approve(self: Box<Self>) -> Box<dyn State>;
- fn content<'a>(&self, post: &'a Post) -> &'a str {
- ""
- }
- }
- //###############################################################
- struct Draft {}
- impl State for Draft {
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- Box::new(PendingReview {})
- }
- fn approve(self: Box<Self>) -> Box<dyn State> {
- self
- }
- }
- //###############################################################
- struct PendingReview {}
- impl State for PendingReview {
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- self
- }
- fn approve(self: Box<Self>) -> Box<dyn State> {
- Box::new(Published {})
- }
- }
- //###############################################################
- struct Published {}
- impl State for Published {
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- self
- }
- fn approve(self: Box<Self>) -> Box<dyn State> {
- self
- }
- fn content<'a>(&self, post: &'a Post) -> &'a str {
- &post.content
- }
- }
- //###############################################################
- pub struct Post {
- state: Option<Box<dyn State>>,
- content: String,
- }
- impl Post {
- pub fn new() -> Post {
- Post {
- state: Some(Box::new(Draft {})),
- content: String::new(),
- }
- }
- pub fn add_text(&mut self, text: &str) {
- self.content.push_str(text);
- }
- pub fn content(&self) -> &str {
- self.state.as_ref().unwrap().content(self)
- }
- pub fn request_review(&mut self) {
- if let Some(s) = self.state.take() {
- self.state = Some(s.request_review())
- }
- }
- pub fn approve(&mut self) {
- if let Some(s) = self.state.take() {
- self.state = Some(s.approve())
- }
- }
- }
- //###############################################################
- fn main() {
- let mut post = Post::new();
-
- post.add_text("I ate a salad for lunch today");
- assert_eq!("", post.content());
-
- post.request_review();
- assert_eq!("", post.content());
-
- post.approve();
- assert_eq!("I ate a salad for lunch today", post.content());
- }
见识了 trait 对象是一个 Rust 中获取部分面向对象功能的方法。动态分发可以通过牺牲少量运行时性能来为你的代码提供一些灵活性。这些灵活性可以用来实现有助于代码可维护性的面向对象模式。Rust 也有像所有权这样不同于面向对象语言的功能。面向对象模式并不总是利用 Rust 优势的最好方式,但也是可用的选项。
Iterator
trait 用来对集合(collection)类型(比如数组)实现迭代器。
这个 trait 只需定义一个返回 next
(下一个)元素的方法,这可手动在 impl
代码块中定义,或者自动定义(比如在数组或区间中)。
为方便起见,for
结构会使用 .into_iter() 方法将一些集合类型转换为迭代器。
下面例子展示了如何使用 Iterator
trait 的方法,更多可用的方法可以看这里。
- struct Fibonacci {
- curr: u32,
- next: u32,
- }
-
- // 为 `Fibonacci`(斐波那契)实现 `Iterator`。
- // `Iterator` trait 只需定义一个能返回 `next`(下一个)元素的方法。
- impl Iterator for Fibonacci {
- type Item = u32;
-
- // 我们在这里使用 `.curr` 和 `.next` 来定义数列(sequence)。
- // 返回类型为 `Option<T>`:
- // * 当 `Iterator` 结束时,返回 `None`。
- // * 其他情况,返回被 `Some` 包裹(wrap)的下一个值。
- fn next(&mut self) -> Option<u32> {
- let new_next = self.curr + self.next;
-
- self.curr = self.next;
- self.next = new_next;
-
- // 既然斐波那契数列不存在终点,那么 `Iterator` 将不可能
- // 返回 `None`,而总是返回 `Some`。
- Some(self.curr)
- }
- }
-
- // 返回一个斐波那契数列生成器
- fn fibonacci() -> Fibonacci {
- Fibonacci { curr: 1, next: 1 }
- }
-
- fn main() {
- // `0..3` 是一个 `Iterator`,会产生:0、1 和 2。
- let mut sequence = 0..3;
-
- println!("Four consecutive `next` calls on 0..3");
- println!("> {:?}", sequence.next());
- println!("> {:?}", sequence.next());
- println!("> {:?}", sequence.next());
- println!("> {:?}", sequence.next());
-
- // `for` 遍历 `Iterator` 直到返回 `None`,
- // 并且每个 `Some` 值都被解包(unwrap),然后绑定给一个变量(这里是 `i`)。
- println!("Iterate through 0..3 using `for`");
- for i in 0..3 {
- println!("> {}", i);
- }
-
- // `take(n)` 方法提取 `Iterator` 的前 `n` 项。
- println!("The first four terms of the Fibonacci sequence are: ");
- for i in fibonacci().take(4) {
- println!("> {}", i);
- }
-
- // `skip(n)` 方法移除前 `n` 项,从而缩短了 `Iterator` 。
- println!("The next four terms of the Fibonacci sequence are: ");
- for i in fibonacci().skip(4).take(4) {
- println!("> {}", i);
- }
-
- let array = [1u32, 3, 3, 7];
-
- // `iter` 方法对数组/slice 产生一个 `Iterator`。
- println!("Iterate the following array {:?}", &array);
- for i in array.iter() {
- println!("> {}", i);
- }
- }
如果函数返回实现了 MyTrait
的类型,可以将其返回类型编写为 -> impl MyTrait
。这可以大大简化你的类型签名!
- use std::iter;
- use std::vec::IntoIter;
-
- // 该函数组合了两个 `Vec <i32>` 并在其上返回一个迭代器。
- // 看看它的返回类型多么复杂!
- fn combine_vecs_explicit_return_type(
- v: Vec<i32>,
- u: Vec<i32>,
- ) -> iter::Cycle<iter::Chain<IntoIter<i32>, IntoIter<i32>>> {
- v.into_iter().chain(u.into_iter()).cycle()
- }
-
- // 这是完全相同的函数,但其返回类型使用 `impl Trait`。
- // 看看它多么简单!
- fn combine_vecs(
- v: Vec<i32>,
- u: Vec<i32>,
- ) -> impl Iterator<Item=i32> {
- v.into_iter().chain(u.into_iter()).cycle()
- }
-
- fn main() {
- let v1 = vec![1, 2, 3];
- let v2 = vec![4, 5];
- let mut v3 = combine_vecs(v1, v2);
- assert_eq!(Some(1), v3.next());
- assert_eq!(Some(2), v3.next());
- assert_eq!(Some(3), v3.next());
- assert_eq!(Some(4), v3.next());
- assert_eq!(Some(5), v3.next());
- println!("all done");
- }
更重要的是,某些 Rust 类型无法写出。例如,每个闭包都有自己未命名的具体类型。在使用 impl Trait
语法之前,必须在堆上进行分配才能返回闭包。但是现在你可以像下面这样静态地完成所有操作:
- // 返回一个将输入和 `y` 相加的函数
- fn make_adder_function(y: i32) -> impl Fn(i32) -> i32 {
- let closure = move |x: i32| { x + y };
- closure
- }
-
- fn main() {
- let plus_one = make_adder_function(1);
- assert_eq!(plus_one(2), 3);
- }
还可以使用 impl Trait
返回使用 map
或 filter
闭包的迭代器!这使得使用 map
和 filter
更容易。因为闭包类型没有名称,所以如果函数返回带闭包的迭代器,则无法写出显式的返回类型。但是有了 impl Trait
,你就可以轻松地做到这一点:
- fn double_positives<'a>(numbers: &'a Vec<i32>) -> impl Iterator<Item = i32> + 'a {
- numbers
- .iter()
- .filter(|x| x > &&0)
- .map(|x| x * 2)
- }
当处理资源时,默认的行为是在赋值或函数调用的同时将它们转移。但是我们有时候也需要把资源复制一份。Clone trait 正好帮助我们完成这任务。可以使用由 Clone
trait 定义的 .clone()
方法。
- // 不含资源的单元结构体
- #[derive(Debug, Clone, Copy)]
- struct Nil;
-
- // 一个包含资源的结构体,它实现了 `Clone` trait
- #[derive(Clone, Debug)]
- struct Pair(Box<i32>, Box<i32>);
-
- fn main() {
- // 实例化 `Nil`
- let nil = Nil;
- // 复制 `Nil`,没有资源用于移动(move)
- let copied_nil = nil;
-
- // 两个 `Nil` 都可以独立使用
- println!("original: {:?}", nil);
- println!("copy: {:?}", copied_nil);
-
- // 实例化 `Pair`
- let pair = Pair(Box::new(1), Box::new(2));
- println!("original: {:?}", pair);
-
- // 将 `pair` 绑定到 `moved_pair`,移动(move)了资源
- let moved_pair = pair;
- println!("copy: {:?}", moved_pair);
-
- // 报错!`pair` 已失去了它的资源。
- //println!("original: {:?}", pair);
- // 试一试 ^ 取消此行注释。
-
- // 将 `moved_pair`(包括其资源)克隆到 `cloned_pair`。
- let cloned_pair = moved_pair.clone();
- // 使用 std::mem::drop 来销毁原始的 pair。
- drop(moved_pair);
-
- // 报错!`moved_pair` 已被销毁。
- //println!("copy: {:?}", moved_pair);
- // 试一试 ^ 将此行注释掉。
-
- // 由 .clone() 得来的结果仍然可用!
- println!("clone: {:?}", cloned_pair);
- }
Rust 没有“继承”,但是可以将一个 trait 定义为另一个 trait 的超集(即父 trait)。例如:
-
- trait Person {
- fn name(&self) -> String;
- }
-
- // Person 是 Student 的父 trait。
- // 实现 Student 需要你也 impl 了 Person。
- trait Student: Person {
- fn university(&self) -> String;
- }
-
- trait Programmer {
- fn fav_language(&self) -> String;
- }
-
- // CompSciStudent (computer science student,计算机科学的学生) 是 Programmer 和 Student 两者的子类。
- // 实现 CompSciStudent 需要你同时 impl 了两个父 trait。
- trait CompSciStudent: Programmer + Student {
- fn git_username(&self) -> String;
- }
-
- struct TempTest;
-
-
- impl Programmer for TempTest {
- fn fav_language(&self) -> String {
- String::from("C++/Python/Rust").clone()
- }
- }
-
- impl Student for TempTest {
- fn university(&self) -> String {
- String::from("宇宙社会大学").clone()
- }
- }
-
- impl Person for TempTest {
- fn name(&self) -> String {
- String::from("无名氏").clone()
- }
- }
-
- impl CompSciStudent for TempTest {
- fn git_username(&self) -> String {
- String::from("无名").clone()
- }
- }
-
- fn comp_sci_student_greeting(student: &dyn CompSciStudent) -> String {
- format!(
- "姓名:{}, 学校:{}, 编程语言:{}, github名:{}",
- student.name(),
- student.university(),
- student.fav_language(),
- student.git_username()
- )
- }
-
- fn main() {
- let temp = Box::new(TempTest);
- println!("{}", comp_sci_student_greeting(&*temp));
-
- let temp = TempTest;
- println!("{}", comp_sci_student_greeting(&temp));
- }
一个类型可以实现许多不同的 trait。如果两个 trait 都需要相同的名称怎么办?例如,许多 trait 可能拥有名为 get()
的方法。他们甚至可能有不同的返回类型!
有个好消息:由于每个 trait 实现都有自己的 impl
块,因此很清楚您要实现哪个 trait 的 get
方法。
何时需要调用这些方法呢?为了消除它们之间的歧义,我们必须使用完全限定语法(Fully Qualified Syntax)。
- trait UsernameWidget {
- // 从这个 widget 中获取选定的用户名
- fn get(&self) -> String;
- }
-
- trait AgeWidget {
- // 从这个 widget 中获取选定的年龄
- fn get(&self) -> u8;
- }
-
- // 同时具有 UsernameWidget 和 AgeWidget 的表单
- struct Form {
- username: String,
- age: u8,
- }
-
- impl UsernameWidget for Form {
- fn get(&self) -> String {
- self.username.clone()
- }
- }
-
- impl AgeWidget for Form {
- fn get(&self) -> u8 {
- self.age
- }
- }
-
- fn main() {
- let form = Form{
- username: "rustacean".to_owned(),
- age: 28,
- };
-
- // 如果取消注释此行,则会收到一条错误消息,提示 “multiple `get` found”(找到了多个`get`)。
- // 因为毕竟有多个名为 `get` 的方法。
- // println!("{}", form.get());
-
- let username = <Form as UsernameWidget>::get(&form);
- assert_eq!("rustacean".to_owned(), username);
- let age = <Form as AgeWidget>::get(&form);
- assert_eq!(28, age);
- }
Rust 提供了一个强大的宏系统,可进行元编程(metaprogramming)。宏看起来和函数很像,只不过名称末尾有一个感叹号 !
。宏并不产生函数调用,而是展开成源码,并和程序的其余部分一起被编译。Rust 又有一点和 C 以及其他语言都不同,那就是 Rust 的宏会展开为抽象语法树(AST,abstract syntax tree),而不是像字符串预处理那样直接替换成代码,这样就不会产生无法预料的优先权错误。
宏 (Macro) 指的是 Rust 中一系列的功能:使用 macro_rules!
的 声明(Declarative)宏。但是为什么已经有了函数还需要宏呢?宏和函数的区别:从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 元编程(metaprogramming)。宏可以在编译器翻译代码前展开,宏定义要比函数定义更复杂,在一个文件里调用宏 之前 必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。
三种 过程(Procedural)宏:
#[derive]
宏在结构体和枚举上指定通过 derive
属性添加的代码宏是通过 macro_rules!
宏来创建的。
- // 这是一个简单的宏,名为 `say_hello`。
- macro_rules! say_hello {
- // `()` 表示此宏不接受任何参数。
- () => (
- // 此宏将会展开成这个代码块里面的内容。
- println!("Hello!");
- )
- }
-
- fn main() {
- // 这个调用将会展开成 `println("Hello");`!
- say_hello!()
- }
宏的参数使用一个美元符号 $
作为前缀,并使用一个指示符(designator)来注明类型:
全部指示符:
block
expr
用于表达式ident
用于变量名或函数名item
literal
用于字面常量pat
(模式 pattern)path
stmt
(语句 statement)tt
(标记树 token tree)ty
(类型 type)vis
(可见性描述符)完整列表详见 Rust Reference。
- macro_rules! create_function {
- // 此宏接受一个 `ident` 指示符表示的参数,并创建一个名为 `$func_name` 的函数。
- // `ident` 指示符用于变量名或函数名
- ($func_name:ident) => (
- fn $func_name() {
- // `stringify!` 宏把 `ident` 转换成字符串。
- println!("You called {:?}()",
- stringify!($func_name))
- }
- )
- }
-
- // 借助上述宏来创建名为 `foo` 和 `bar` 的函数。
- create_function!(foo);
- create_function!(bar);
-
- macro_rules! print_result {
- // 此宏接受一个 `expr` 类型的表达式,并将它作为字符串,连同其结果一起
- // 打印出来。
- // `expr` 指示符表示表达式。
- ($expression:expr) => (
- // `stringify!` 把表达式*原样*转换成一个字符串。
- println!("{:?} = {:?}",
- stringify!($expression),
- $expression)
- )
- }
-
- fn main() {
- foo();
- bar();
-
- print_result!(1u32 + 1);
-
- // 回想一下,代码块也是表达式!
- print_result!({
- let x = 1u32;
-
- x * x + 2 * x - 1
- });
- }
示例:查看 vec!
宏定义来探索如何使用 macro_rules!
结构。
- let v: Vec<u32> = vec![1, 2, 3];
-
- // 一个 vec! 宏定义的简化版本
- #[macro_export]
- macro_rules! vec {
- ( $( $x:expr ),* ) => {
- {
- let mut temp_vec = Vec::new();
- $(
- temp_vec.push($x);
- )*
- temp_vec
- }
- };
- }
注意:标准库中实际定义的 vec!
包括预分配适当量的内存的代码。这部分为代码优化,为了让示例简化,此处并没有包含在内。
首先,一对括号包含了整个模式。我们使用美元符号($
)在宏系统中声明一个变量来包含匹配该模式的 Rust 代码。美元符号明确表明这是一个宏变量而不是普通 Rust 变量。之后是一对括号,其捕获了符合括号内模式的值用以在替代代码中使用。$()
内则是 $x:expr
,其匹配 Rust 的任意表达式,并将该表达式命名为 $x
。$()
之后的逗号说明一个可有可无的逗号分隔符可以出现在 $()
所匹配的代码之后。紧随逗号之后的 *
说明该模式匹配零个或更多个 *
之前的任何模式。当以 vec![1, 2, 3];
调用宏时,$x
模式与三个表达式 1
、2
和 3
进行了三次匹配。
在线文档或其他资源,如 “The Little Book of Rust Macros” 来更多地了解如何写宏,
宏可以重载,从而接受不同的参数组合。在这方面,macro_rules!
的作用类似于匹配(match)代码块:
- // 根据你调用它的方式,`test!` 将以不同的方式来比较 `$left` 和 `$right`。
- macro_rules! test {
- // 参数不需要使用逗号隔开。
- // 参数可以任意组合!
- ($left:expr; and $right:expr) => (
- println!("{:?} and {:?} is {:?}",
- stringify!($left),
- stringify!($right),
- $left && $right)
- );
- // ^ 每个分支都必须以分号结束。
- ($left:expr; or $right:expr) => (
- println!("{:?} or {:?} is {:?}",
- stringify!($left),
- stringify!($right),
- $left || $right)
- );
- }
-
- fn main() {
- test!(1i32 + 1 == 2i32; and 2i32 * 2 == 4i32);
- test!(true; or false);
- }
宏在参数列表中可以使用 +
来表示一个参数可能出现一次或多次,使用 *
来表示该参数可能出现零次或多次。
例子中,把模式这样: $(...),+
包围起来,就可以匹配一个或多个用逗号隔开的表达式。另外注意到,宏定义的最后一个分支可以不用分号作为结束。
- // `min!` 将求出任意数量的参数的最小值。
- macro_rules! find_min {
- // 基本情形:
- ($x:expr) => ($x);
- // `$x` 后面跟着至少一个 `$y,`
- ($x:expr, $($y:expr),+) => (
- // 对 `$x` 后面的 `$y` 们调用 `find_min!`
- std::cmp::min($x, find_min!($($y),+))
- )
- }
-
- fn main() {
- println!("{}", find_min!(1u32));
- println!("{}", find_min!(1u32 + 2 , 2u32));
- println!("{}", find_min!(5u32, 2u32 * 3, 4u32));
- }
通过提取函数或测试集的公共部分,宏可以让你写出 DRY 的代码(DRY 是 Don't Repeat Yourself 的缩写,意思为 “不要写重复代码”)。
示例:对 Vec<T>
实现并测试了关于 +=
、*=
和 -=
等运算符。
- use std::ops::{Add, Mul, Sub};
-
- macro_rules! assert_equal_len {
- // `tt`(token tree,标记树)指示符表示运算符和标记。
- ($a:ident, $b: ident, $func:ident, $op:tt) => (
- assert!($a.len() == $b.len(),
- "{:?}: dimension mismatch: {:?} {:?} {:?}",
- stringify!($func),
- ($a.len(),),
- stringify!($op),
- ($b.len(),));
- )
- }
-
- macro_rules! op {
- ($func:ident, $bound:ident, $op:tt, $method:ident) => (
- fn $func<T: $bound<T, Output=T> + Copy>(xs: &mut Vec<T>, ys: &Vec<T>) {
- assert_equal_len!(xs, ys, $func, $op);
-
- for (x, y) in xs.iter_mut().zip(ys.iter()) {
- *x = $bound::$method(*x, *y);
- // *x = x.$method(*y);
- }
- }
- )
- }
-
- // 实现 `add_assign`、`mul_assign` 和 `sub_assign` 等函数。
- op!(add_assign, Add, +=, add);
- op!(mul_assign, Mul, *=, mul);
- op!(sub_assign, Sub, -=, sub);
-
- mod test {
- use std::iter;
- macro_rules! test {
- ($func: ident, $x:expr, $y:expr, $z:expr) => {
- #[test]
- fn $func() {
- for size in 0usize..10 {
- let mut x: Vec<_> = iter::repeat($x).take(size).collect();
- let y: Vec<_> = iter::repeat($y).take(size).collect();
- let z: Vec<_> = iter::repeat($z).take(size).collect();
-
- super::$func(&mut x, &y);
-
- assert_eq!(x, z);
- }
- }
- }
- }
-
- // 测试 `add_assign`、`mul_assign` 和 `sub_assign`
- test!(add_assign, 1u32, 2u32, 3u32);
- test!(mul_assign, 2u32, 3u32, 6u32);
- test!(sub_assign, 3u32, 2u32, 1u32);
- }
-
$ rustc --test dry.rs && ./dry
running 3 tests
test test::mul_assign ... ok
test test::add_assign ... ok
test test::sub_assign ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
DSL 是 Rust 的宏中集成的微型 “语言”。这种语言是完全合法的,因为宏系统会把它转换成普通的 Rust 语法树,它只不过看起来像是另一种语言而已。这就允许你为一些特定功能创造一套简洁直观的语法(当然是有限制的)。
示例:想要定义一套小的计算器 API,可以传给它表达式,它会把结果打印到控制台上。
- macro_rules! calculate {
- (eval $e:expr) => {{
- {
- let val: usize = $e; // 强制类型为整型
- println!("{} = {}", stringify!{$e}, val);
- }
- }};
- }
-
- fn main() {
- calculate! {
- eval 1 + 2 // 看到了吧,`eval` 可并不是 Rust 的关键字!
- }
-
- calculate! {
- eval (1 + 2) * (3 / 4)
- }
- }
输出:
1 + 2 = 3
(1 + 2) * (3 / 4) = 0
这个例子非常简单,但是已经有很多利用宏开发的复杂接口了,比如 lazy_static 和 clap。
可变参数接口可以接受任意数目的参数。比如说 println
就可以,其参数的数目是由格式化字符串指定的。把之前的 calculate!
宏改写成可变参数接口:
- macro_rules! calculate {
- // 单个 `eval` 的模式
- (eval $e:expr) => {{
- {
- let val: usize = $e; // Force types to be integers
- println!("{} = {}", stringify!{$e}, val);
- }
- }};
-
- // 递归地拆解多重的 `eval`
- (eval $e:expr, $(eval $es:expr),+) => {{
- calculate! { eval $e }
- calculate! { $(eval $es),+ }
- }};
- }
-
- fn main() {
- calculate! { // 妈妈快看,可变参数的 `calculate!`!
- eval 1 + 2,
- eval 3 + 4,
- eval (2 * 3) + 1
- }
- }
输出:
1 + 2 = 3
3 + 4 = 7
(2 * 3) + 1 = 7
第二种形式的宏被称为 过程宏(procedural macros),因为它们更像函数(一种过程类型)。过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。有三种类型的过程宏(自定义派生(derive),类属性和类函数),不过它们的工作方式都类似。
- use proc_macro;
-
- #[some_attribute]
- pub fn some_name(input: TokenStream) -> TokenStream {
- }
定义过程宏的函数接收一个 TokenStream 作为输入并生成 TokenStream 作为输出。TokenStream
是定义于proc_macro
crate 里代表一系列 token 的类型,Rust 默认携带了proc_macro
crate。这就是宏的核心:宏所处理的源代码组成了输入 TokenStream
,宏生成的代码是输出 TokenStream
。函数上还有一个属性;这个属性指明了我们创建的过程宏的类型。在同一 crate 中可以有多种的过程宏。
提供一个过程式宏以便用户可以使用 #[derive(HelloMacro)]
注解它们的类型来得到 hello_macro
函数的默认实现。该默认实现会打印 Hello, Macro! My name is TypeName!
,其中 TypeName
为定义了 trait 的类型名。
类属性宏与自定义派生宏相似,不同的是 derive
属性生成代码,它们(类属性宏)能让你创建新的属性。它们也更为灵活;derive
只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 route
的属性用于注解 web 应用程序框架(web application framework)的函数:
#[route(GET, "/")]
fn index() {
类函数(Function-like)宏的定义看起来像函数调用的宏。类似于 macro_rules!
,它们比函数更灵活;例如,可以接受未知数量的参数。然而 macro_rules!
宏只能使用之前 “使用 macro_rules! 的声明宏用于通用元编程” 介绍的类匹配的语法定义。类函数宏获取 TokenStream
参数,其定义使用 Rust 代码操纵 TokenStream
,就像另两种过程宏一样。一个类函数宏例子是可以像这样被调用的 sql!
宏:
let sql = sql!(SELECT * FROM posts WHERE id=1);
这个宏会解析其中的 SQL 语句并检查其是否是句法正确的,这是比 macro_rules!
可以做到的更为复杂的处理。sql!
宏应该被定义为如此:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
这类似于自定义派生宏的签名:获取括号中的 token,并返回希望生成的代码。
命令行参数可使用 std::env::args
进行接收,这将返回一个迭代器,该迭代器会对每个参数举出一个字符串。
- use std::env;
-
- fn main() {
- let args: Vec<String> = env::args().collect();
-
- // 第一个参数是调用本程序的路径
- println!("My path is {}.", args[0]);
-
- // 其余的参数是被传递给程序的命令行参数。
- // 请这样调用程序:
- // $ ./args arg1 arg2
- println!("I got {:?} arguments: {:?}.", args.len() - 1, &args[1..]);
- }
可以用模式匹配来解析简单的参数:
- use std::env;
-
- fn increase(number: i32) {
- println!("{}", number + 1);
- }
-
- fn decrease(number: i32) {
- println!("{}", number - 1);
- }
-
- fn help() {
- println!("usage:
- match_args <string>
- Check whether given string is the answer.
- match_args {{increase|decrease}} <integer>
- Increase or decrease given integer by one.");
- }
-
- fn main() {
- let args: Vec<String> = env::args().collect();
-
- match args.len() {
- // 没有传入参数
- 1 => {
- println!("My name is 'match_args'. Try passing some arguments!");
- },
- // 一个传入参数
- 2 => {
- match args[1].parse() {
- Ok(42) => println!("This is the answer!"),
- _ => println!("This is not the answer."),
- }
- },
- // 传入一条命令和一个参数
- 3 => {
- let cmd = &args[1];
- let num = &args[2];
- // 解析数字
- let number: i32 = match num.parse() {
- Ok(n) => {
- n
- },
- Err(_) => {
- println!("error: second argument not an integer");
- help();
- return;
- },
- };
- // 解析命令
- match &cmd[..] {
- "increase" => increase(number),
- "decrease" => decrease(number),
- _ => {
- println!("error: invalid command");
- help();
- },
- }
- },
- // 所有其他情况
- _ => {
- // 显示帮助信息
- help();
- }
- }
- }
$ ./match_args Rust
This is not the answer.
$ ./match_args 42
This is the answer!
$ ./match_args do something
error: second argument not an integer
usage:
match_args <string>
Check whether given string is the answer.
match_args {increase|decrease} <integer>
Increase or decrease given integer by one.
$ ./match_args do 42
error: invalid command
usage:
match_args <string>
Check whether given string is the answer.
match_args {increase|decrease} <integer>
Increase or decrease given integer by one.
$ ./match_args increase 42
43
测试有三种风格:
Rust 也支持在测试中指定额外的依赖:
大多数单元测试都会被放到一个叫 tests
的、带有 #[cfg(test)]
属性的模块中,测试函数要加上 #[test]
属性。
当测试函数中有什么东西 panic 了,测试就失败。有一些这方面的辅助宏:
assert!(expression)
- 如果表达式的值是 false
则 panic。assert_eq!(left, right)
和 assert_ne!(left, right)
- 检验左右两边是否 相等/不等。- pub fn add(a: i32, b: i32) -> i32 {
- a + b
- }
-
- // 这个加法函数写得很差,本例中我们会使它失败。
- #[allow(dead_code)]
- fn bad_add(a: i32, b: i32) -> i32 {
- a - b
- }
-
- #[cfg(test)]
- mod tests {
- // 注意这个惯用法:在 tests 模块中,从外部作用域导入所有名字。
- use super::*;
-
- #[test]
- fn test_add() {
- assert_eq!(add(1, 2), 3);
- }
-
- #[test]
- fn test_bad_add() {
- // 这个断言会导致测试失败。注意私有的函数也可以被测试!
- assert_eq!(bad_add(1, 2), 3);
- }
- }
可以使用 cargo test
来运行测试。
测试 panic
一些函数应当在特定条件下 panic。为测试这种行为,请使用 #[should_panic]
属性。这个属性接受可选参数 expected =
以指定 panic 时的消息。如果你的函数能以多种方式 panic,这个属性就保证了你在测试的确实是所指定的 panic。
- pub fn divide_non_zero_result(a: u32, b: u32) -> u32 {
- if b == 0 {
- panic!("Divide-by-zero error");
- } else if a < b {
- panic!("Divide result is zero");
- }
- a / b
- }
-
- #[cfg(test)]
- mod tests {
- use super::*;
-
- #[test]
- fn test_divide() {
- assert_eq!(divide_non_zero_result(10, 2), 5);
- }
-
- #[test]
- #[should_panic]
- fn test_any_panic() {
- divide_non_zero_result(1, 0);
- }
-
- #[test]
- #[should_panic(expected = "Divide result is zero")]
- fn test_specific_panic() {
- divide_non_zero_result(1, 10);
- }
- }
运行特定的测试。要运行特定的测试,只要把测试名称传给 cargo test
命令就可以了。
cargo test test_any_panic
cargo test panic
忽略测试。可以把属性 #[ignore] 赋予测试以排除某些测试,或者使用 cargo test -- --ignored 命令来运行它们。
-
- #![allow(unused)]
- fn main() {
- pub fn add(a: i32, b: i32) -> i32 {
- a + b
- }
-
- #[cfg(test)]
- mod tests {
- use super::*;
-
- #[test]
- fn test_add() {
- assert_eq!(add(2, 2), 4);
- }
-
- #[test]
- fn test_add_hundred() {
- assert_eq!(add(100, 2), 102);
- assert_eq!(add(2, 100), 102);
- }
-
- #[test]
- #[ignore]
- fn ignored_test() {
- assert_eq!(add(0, 0), 0);
- }
- }
- }
cargo test
cargo test -- --ignored
单元测试一次仅能单独测试一个模块,这种测试是小规模的,并且能测试私有代码;集成测试是 crate 外部的测试,并且仅使用 crate 的公共接口,就像其他使用该 crate 的程序那样。集成测试的目的是检验你的库的各部分是否能够正确地协同工作。
cargo 在与 src
同级别的 tests
目录寻找集成测试。
文件 src/lib.rs
:
- // 在一个叫做 'adder' 的 crate 中定义此函数。
- pub fn add(a: i32, b: i32) -> i32 {
- a + b
- }
包含测试的文件:tests/integration_test.rs
:
-
- #[test]
- fn test_add() {
- assert_eq!(adder::add(3, 2), 5);
- }
使用 cargo test
命令:
tests
目录中的每一个 Rust 源文件都被编译成一个单独的 crate。在集成测试中要想共享代码,一种方式是创建具有公用函数的模块,然后在测试中导入并使用它。
文件 tests/common.rs
:
- pub fn setup() {
- // 一些配置代码,比如创建文件/目录,开启服务器等等。
- }
包含测试的文件:tests/integration_test.rs
- // 导入共用模块。
- mod common;
-
- #[test]
- fn test_add() {
- // 使用共用模块。
- common::setup();
- assert_eq!(adder::add(3, 2), 5);
- }
有时仅在测试中才需要一些依赖(比如基准测试相关的)。这种依赖要写在 Cargo.toml
的 [dev-dependencies]
部分。这些依赖不会传播给其他依赖于这个包的包。
比如说使用 pretty_assertions
,这是扩展了标准的 assert!
宏的一个 crate。
# 这里省略了标准的 crate 数据
[dev-dependencies]
pretty_assertions = "1"
文件 src/lib.rs
:
- pub fn add(a: i32, b: i32) -> i32 {
- a + b
- }
-
- #[cfg(test)]
- mod tests {
- use super::*;
- use pretty_assertions::assert_eq; // 仅用于测试, 不能在非测试代码中使用
-
- #[test]
- fn test_add() {
- assert_eq!(add(2, 3), 5);
- }
- }
涉及如下内容:
不安全代码块主要用于四件事情:
上面所有论过的代码都有 Rust 在编译时会强制执行的内存安全保证。然而,Rust 还隐藏有第二种语言,它不会强制执行这类内存安全保证:这被称为 不安全 Rust(unsafe Rust)。它与常规 Rust 代码无异,但是会提供额外的超能力。尽管代码可能没问题,但如果 Rust 编译器没有足够的信息可以确定,它将拒绝代码。
不安全 Rust 之所以存在,是因为静态分析本质上是保守的。当编译器尝试确定一段代码是否支持某个保证时,拒绝一些合法的程序比接受无效的程序要好一些。这必然意味着有时代码 可能 是合法的,但如果 Rust 编译器没有足够的信息来确定,它将拒绝该代码。在这种情况下,可以使用不安全代码告诉编译器,“相信我,我知道我在干什么。” 不过千万注意,使用不安全 Rust 风险自担:如果不安全代码出错了,比如解引用空指针,可能会导致不安全的内存使用。
另一个 Rust 存在不安全一面的原因是:底层计算机硬件固有的不安全性。如果 Rust 不允许进行不安全操作,那么有些任务则根本完成不了。Rust 需要能够进行像直接与操作系统交互,甚至于编写你自己的操作系统这样的底层系统编程!这也是 Rust 语言的目标之一。
通过 unsafe
关键字来切换到不安全 Rust,接着可以开启一个新的存放不安全代码的块。这里有五类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作,它们称之为 “不安全的超能力。(unsafe superpowers)” 这些超能力是:
union
的字段unsafe
并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,它仍会被检查。unsafe
关键字只是提供了那五个不会被编译器检查内存安全的功能。你仍然能在不安全块中获得某种程度的安全。
再者,unsafe
不意味着块中的代码就一定是危险的或者必然导致内存安全问题:其意图在于作为程序员你将会确保 unsafe
块中的代码以有效的方式访问内存。
人是会犯错误的,错误总会发生,不过通过要求这五类操作必须位于标记为 unsafe
的块中,就能够知道任何与内存安全相关的错误必定位于 unsafe
块内。保持 unsafe
块尽可能小,如此当之后调查内存 bug 时就会感谢你自己了。
为了尽可能隔离不安全代码,将不安全代码封装进一个安全的抽象并提供安全 API 是一个好主意,当我们学习不安全函数和方法时会讨论到。标准库的一部分被实现为在被评审过的不安全代码之上的安全抽象。这个技术防止了 unsafe
泄露到所有你或者用户希望使用由 unsafe
代码实现的功能的地方,因为使用其安全抽象是安全的。
裸指针与引用和智能指针的区别在于
从引用同时创建不可变和可变裸指针。
- fn main() {
- let mut int_val = 5;
-
- let r1 = &int_val as *const i32;
- let r2 = &mut int_val as *mut i32;
-
- println!("{:?}", r1);
- println!("{:?}", r2);
-
- unsafe { *r2 = 100; }
- println!("{:?}", r2);
-
- unsafe {
- println!("{:?}", *r1);
- println!("{:?}", *r2);
- }
- }
原始指针(raw pointer,裸指针)*
和引用 &T
有类似的功能,但引用总是安全的,因为借用检查器保证了它指向一个有效的数据。解引用一个裸指针只能通过不安全代码块执行。
- fn main() {
- let raw_p: *const u32 = &10;
-
- unsafe {
- assert!(*raw_p == 10);
- }
- }
第二类可以在不安全块中进行的操作是调用不安全函数。不安全函数和方法与常规函数方法十分类似,除了其开头有一个额外的 unsafe
。在此上下文中,关键字unsafe
表示该函数具有调用时需要满足的要求,而 Rust 不会保证满足这些要求。通过在 unsafe
块中调用不安全函数,表明我们已经阅读过此函数的文档并对其是否满足函数自身的契约负责。不安全函数体也是有效的 unsafe
块,所以在不安全函数中进行另一个不安全操作时无需新增额外的 unsafe
块。
unsafe fn dangerous() {}
unsafe {
dangerous();
}
一些函数可以声明为不安全的(unsafe
),这意味着在使用它时保证正确性不再是编译器的责任,而是程序员的。一个例子就是 std::slice::from_raw_parts,向它传入指向第一个元素的指针和长度参数,它会创建一个切片。
- use std::slice;
-
- fn main() {
- let some_vector = vec![1, 2, 3, 4];
-
- let pointer = some_vector.as_ptr();
- let length = some_vector.len();
-
- unsafe {
- let my_slice: &[u32] = slice::from_raw_parts(pointer, length);
-
- assert_eq!(some_vector.as_slice(), my_slice);
- }
- }
slice::from_raw_parts 假设传入的指针指向有效的内存,且被指向的内存具有正确的数据类型,我们必须满足这一假设,否则程序的行为是未定义的(undefined),于是我们就不能预测会发生些什么了。
仅仅因为函数包含不安全代码并不意味着整个函数都需要标记为不安全的。事实上,将不安全代码封装进安全函数是一个常见的抽象。
- fn main() {
- let mut v = vec![1, 2, 3, 4, 5, 6];
- let r = &mut v[..];
- let (a, b) = r.split_at_mut(3);
- assert_eq!(a, &mut [1, 2, 3]);
- assert_eq!(b, &mut [4, 5, 6]);
- }
示例:
- use std::slice;
- fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
- let len = values.len();
- let ptr = values.as_mut_ptr();
- assert!(mid <= len);
- unsafe {
- (
- slice::from_raw_parts_mut(ptr, mid),
- slice::from_raw_parts_mut(ptr.add(mid), len - mid),
- )
- }
- }
-
- fn main() {
- let address = 0x01234usize;
- let r = address as *mut i32;
- let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
- }
“Slice 类型” 部分,slice 是一个指向一些数据的指针,并带有该 slice 的长度。可以使用 len
方法获取 slice 的长度,使用 as_mut_ptr
方法访问 slice 的裸指针。
有时你的 Rust 代码可能需要与其他语言编写的代码交互。为此 Rust 有一个关键字,extern
,有助于创建和使用 外部函数接口(Foreign Function Interface,FFI)。外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。
示例:如何集成 C 标准库中的 abs
函数。
- extern "C" {
- fn abs(input: i32) -> i32;
- }
-
- fn main() {
- unsafe {
- println!("Absolute value of -3 according to C: {}", abs(-3));
- }
- }
Rust 提供了到 C 语言库的外部语言函数接口(Foreign Function Interface,FFI)。外部语言函数必须在一个 extern
代码块中声明,且该代码块要带有一个包含库名称的 #[link]
属性。
- use std::fmt;
-
- // 这个 extern 代码块链接到 libm 库
- #[link(name = "m")]
- extern {
- // 这个外部函数用于计算单精度复数的平方根
- fn csqrtf(z: Complex) -> Complex;
-
- // 这个用来计算单精度复数的复变余弦
- fn ccosf(z: Complex) -> Complex;
- }
-
- // 由于调用其他语言的函数被认为是不安全的,我们通常会给它们写一层安全的封装
- fn cos(z: Complex) -> Complex {
- unsafe { ccosf(z) }
- }
-
- fn main() {
- // z = -1 + 0i
- let z = Complex { re: -1., im: 0. };
-
- // 调用外部语言函数是不安全操作
- let z_sqrt = unsafe { csqrtf(z) };
-
- println!("the square root of {:?} is {:?}", z, z_sqrt);
-
- // 调用不安全操作的安全的 API 封装
- println!("cos({:?}) = {:?}", z, cos(z));
- }
-
- // 单精度复数的最简实现
- #[repr(C)]
- #[derive(Clone, Copy)]
- struct Complex {
- re: f32,
- im: f32,
- }
-
- impl fmt::Debug for Complex {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- if self.im < 0. {
- write!(f, "{}-{}i", self.re, -self.im)
- } else {
- write!(f, "{}+{}i", self.re, self.im)
- }
- }
- }
Rust 避免讨论 全局变量(global variables),Rust 确实支持,不过这对于 Rust 的所有权规则来说是有问题的。如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争。
全局变量在 Rust 中被称为 静态(static)变量。
- static HELLO_WORLD: &str = "Hello, world!";
-
- fn main() {
- println!("name is: {}", HELLO_WORLD);
- }
静态(static
)变量类似于 “变量和常量的区别” 部分讨论的常量。通常静态变量的名称采用 SCREAMING_SNAKE_CASE
写法。静态变量只能储存拥有 'static
生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。访问不可变静态变量是安全的。
- static mut COUNTER: u32 = 0;
-
- fn add_to_count(inc: u32) {
- unsafe {
- COUNTER += inc;
- }
- }
-
- fn main() {
- add_to_count(3);
-
- unsafe {
- // 读取或修改一个可变静态变量是不安全的
- println!("COUNTER: {}", COUNTER);
- }
- }
注意:拥有可以全局访问的可变数据,难以保证不存在数据竞争,这就是为何 Rust 认为可变静态变量是不安全的。任何可能的情况,请优先使用第十六章讨论的并发技术和线程安全智能指针,这样编译器就能检测不同线程间的数据访问是否是安全的。
unsafe
的另一个操作用例是实现不安全 trait。当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的。可以在 trait
之前增加 unsafe
关键字将 trait 声明为 unsafe
,同时 trait 的实现也必须标记为 unsafe
,
- unsafe trait Foo {
- // methods go here
- }
-
- unsafe impl Foo for i32 {
- // method implementations go here
- }
-
- fn main() {}
关联类型(associated types)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。trait 的实现者会针对特定的实现在这个占位符类型指定相应的具体类型。如此可以定义一个使用多种类型的 trait,直到实现此 trait 时都无需知道这些类型具体是什么。
个带有关联类型的 trait 的例子是标准库提供的 Iterator
trait。它有一个叫做 Item
的关联类型来替代遍历的值的类型。Iterator
trait 的定义如示例
- pub trait Iterator {
- // Iterator trait 的定义中带有关联类型 Item
- type Item;
- fn next(&mut self) -> Option<Self::Item>;
- }
-
- struct Counter{}
-
- impl Iterator for Counter {
- // 实现中指定了 Item 的类型为 u32:
- type Item = u32;
-
- fn next(&mut self) -> Option<Self::Item> {
- // --snip--
- Some(5)
- }
- }
- fn main() {
- let mut temp = Counter{};
- match temp.next() {
- Some(val) => println!("{val}"),
- _ => println!("返回值为空")
- };
- }
这个语法类似于泛型。
- pub trait Iterator<T> {
- fn next(&mut self) -> Option<T>;
- }
当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。如果默认类型就足够的话,这消除了为具体类型实现 trait 的需要。为泛型类型指定默认类型的语法是在声明泛型类型时使用:<PlaceholderType=ConcreteType>
。
这种情况的一个非常好的例子是使用 运算符重载(Operator overloading),这是指在特定情况下自定义运算符(比如 +
)行为的操作。
- use std::ops::Add;
-
- #[derive(Debug, Copy, Clone, PartialEq)]
- struct Point {
- x: i32,
- y: i32,
- }
-
- impl Add for Point {
- type Output = Point;
-
- fn add(self, other: Point) -> Point {
- Point {
- x: self.x + other.x,
- y: self.y + other.y,
- }
- }
- }
-
- fn main() {
- assert_eq!(
- Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
- Point { x: 3, y: 3 }
- );
- }
add
方法将两个 Point
实例的 x
值和 y
值分别相加来创建一个新的 Point
。Add
trait 有一个叫做 Output
的关联类型,它用来决定 add
方法的返回值类型。
这里默认泛型类型位于 Add
trait 中。这里是其定义:
- trait Add<Rhs=Self> {
- type Output;
-
- fn add(self, rhs: Rhs) -> Self::Output;
- }
带有一个方法和一个关联类型的 trait。尖括号中的 Rhs=Self
:这个语法叫做 默认类型参数(default type parameters)。Rhs
是一个泛型类型参数(“right hand side” 的缩写),它用于定义 add
方法中的 rhs
参数。如果实现 Add
trait 时不指定 Rhs
的具体类型,Rhs
的类型将是默认的 Self
类型,也就是在其上实现 Add
的类型。
完全限定语法定义为:<Type as Trait>::function(receiver_if_method, next_arg, ...);
Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法,也不能阻止为同一类型同时实现这两个 trait。甚至直接在类型上实现开始已经有的同名方法也是可能的!
不过,当调用这些同名方法时,需要告诉 Rust 我们希望使用哪一个。
- trait Pilot {
- fn fly(&self);
- }
-
- trait Wizard {
- fn fly(&self);
- }
-
- struct Human;
-
- impl Pilot for Human {
- fn fly(&self) {
- println!("This is your captain speaking.");
- }
- }
-
- impl Wizard for Human {
- fn fly(&self) {
- println!("Up!");
- }
- }
-
- impl Human {
- fn fly(&self) {
- println!("*waving arms furiously*");
- }
- }
- fn main() {
- let p1 = Human{};
- /*当调用 Human 实例的 fly 时,编译器默认调用直接实现在类型上的方法*/
- p1.fly();
-
- let p2 = Human;
- Human::fly(&p2);
-
- let p3 = Human;
- Pilot::fly(&p3);
- Wizard::fly(&p3);
- p3.fly();
- }
示例:
- trait Animal {
- fn baby_name() -> String;
- }
-
- struct Dog;
-
- impl Dog {
- fn baby_name() -> String {
- String::from("11111")
- }
- }
-
- impl Animal for Dog {
- fn baby_name() -> String {
- String::from("22222")
- }
- }
-
- fn main() {
- println!("A baby dog is called a {}", Dog::baby_name());
- println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
- }
可能会需要编写一个依赖另一个 trait 的 trait 定义:对于一个实现了第一个 trait 的类型,你希望要求这个类型也实现了第二个 trait。如此就可使 trait 定义使用第二个 trait 的关联项。这个所需的 trait 是我们实现的 trait 的 父(超)trait(supertrait)。
- use std::fmt;
-
- trait OutlinePrint: fmt::Display {
- fn outline_print(&self) {
- let output = self.to_string();
- let len = output.len();
- println!("{}", "*".repeat(len + 4));
- println!("*{}*", " ".repeat(len + 2));
- println!("* {} *", output);
- println!("*{}*", " ".repeat(len + 2));
- println!("{}", "*".repeat(len + 4));
- }
- }
- struct Point {
- x: i32,
- y: i32,
- }
- impl OutlinePrint for Point {}
- impl fmt::Display for Point {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "({}, {})", self.x, self.y)
- }
- }
-
- fn main() {
- let p = Point{x:1, y:2};
- p.outline_print()
- }
-
实现 OutlinePrint trait,它要求来自 Display 的功能。因为指定了 OutlinePrint
需要 Display
trait,则可以在 outline_print
中使用 to_string
,其会为任何实现 Display
的类型自动实现。如果不在 trait 名后增加 : Display
并尝试在 outline_print
中使用 to_string
,则会得到一个错误说在当前作用域中没有找到用于 &Self
类型的方法 to_string
。
“为类型实现 trait” 部分,提到了孤儿规则(orphan rule),它说明只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait。一个绕开这个限制的方法是使用 newtype 模式(newtype pattern),它涉及到在一个元组结构体( “用没有命名字段的元组结构体来创建不同的类型” 介绍了元组结构体)中创建一个新类型。这个元组结构体带有一个字段作为希望实现 trait 的类型的简单封装。接着这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait。Newtype 是一个源自 (U.C.0079,逃) Haskell 编程语言的概念。使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。
例如,如果想要在 Vec<T>
上实现 Display
,而孤儿规则阻止我们直接这么做,因为 Display
trait 和 Vec<T>
都定义于我们的 crate 之外。可以创建一个包含 Vec<T>
实例的 Wrapper
结构体,接着可以如列表 19-23 那样在 Wrapper
上实现 Display
并使用 Vec<T>
的值:
- use std::fmt;
-
- struct Wrapper(Vec<String>);
-
- impl fmt::Display for Wrapper {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "[{}]", self.0.join(", "))
- }
- }
-
- fn main() {
- let w = Wrapper(vec![String::from("hello"), String::from("world")]);
- println!("w = {}", w);
- }
Display
的实现使用 self.0
来访问其内部的 Vec<T>
,因为 Wrapper
是元组结构体而 Vec<T>
是结构体总位于索引 0 的项。接着就可以使用 Wrapper
中 Display
的功能了。
此方法的缺点是,因为 Wrapper
是一个新类型,它没有定义于其值之上的方法;必须直接在 Wrapper
上实现 Vec<T>
的所有方法,这样就可以代理到self.0
上 —— 这就允许我们完全像 Vec<T>
那样对待 Wrapper
。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 Deref
trait(第十五章 “通过 Deref trait 将智能指针当作常规引用处理” 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则必须只自行实现所需的方法。
使用 type
关键字来给予现有类型另一个名字。例如,可以像这样创建 i32
的别名 Kilometers
:
type Kilometers = i32;
类型别名的主要用途是减少重复。例如,可能会有这样很长的类型:Box<dyn Fn() + Send + 'static>
在函数签名或类型注解中每次都书写这个类型将是枯燥且易于出错的。示例代码的项目:
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
// --snip--
}fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
// --snip--
}
示例:
- use std::fmt;
- use std::io::Error;
-
- pub trait Write_1 {
- fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
- fn flush(&mut self) -> Result<(), Error>;
-
- fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
- fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
- }
-
- type Result<T> = std::result::Result<T, std::io::Error>;
- pub trait Write_2 {
- fn write(&mut self, buf: &[u8]) -> Result<usize>;
- fn flush(&mut self) -> Result<()>;
-
- fn write_all(&mut self, buf: &[u8]) -> Result<()>;
- fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
- }
向函数传递常规函数。fn
被称为 函数指针(function pointer)。通过函数指针允许我们使用函数作为另一个函数的参数。
指定参数为函数指针的语法类似于闭包
- fn add_one(x: i32) -> i32 {
- x + 1
- }
-
- fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
- f(arg) + f(arg)
- }
-
- fn main() {
- let answer = do_twice(add_one, 5);
-
- println!("The answer is: {}", answer);
- }
示例 :
- fn main() {
- let list_of_numbers = vec![1, 2, 3];
- let list_of_strings: Vec<String> =
- list_of_numbers.iter().map(|i| i.to_string()).collect();
- println!("{:?}", list_of_strings);
-
- let list_of_numbers = vec![1, 2, 3];
- let list_of_strings: Vec<String> =
- list_of_numbers.iter().map(ToString::to_string).collect();
- println!("{:?}", list_of_strings);
- }
闭包表现为 trait,这意味着不能直接返回闭包。因为它们没有一个可返回的具体类型;例如不允许使用函数指针 fn
作为返回值类型。
这段代码尝试直接返回闭包,它并不能编译:
- fn returns_closure() -> dyn Fn(i32) -> i32 {
- |x| x + 1
- }
错误又一次指向了 Sized
trait!Rust 并不知道需要多少空间来储存闭包。不过我们在上一部分见过这种情况的解决办法:可以使用 trait 对象:
- fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
- Box::new(|x| x + 1)
- }
这段代码正好可以编译。关于 trait 对象的更多内容 “为使用不同类型的值而设计的 trait 对象” 。
:https://kaisery.github.io/trpl-zh-cn/ch20-00-final-project-a-web-server.html
实现一个返回 “hello” 的 web server,它在浏览器中看起来就如图
内容
这里使用的方法并不是使用 Rust 构建 web server 最好的方法。crates.io 上有很多可用于生产环境的 crate,它们提供了更为完整的 web server 和线程池实现。本章的目的在于学习,而不是走捷径。新建一个项目:cargo new hello
- use std::net::TcpListener;
-
- fn main() {
- let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
-
- for stream in listener.incoming() {
- let stream = stream.unwrap();
-
- println!("Connection established!");
- }
- }
- use std::{
- io::{prelude::*, BufReader},
- net::{TcpListener, TcpStream},
- };
-
- fn main() {
- let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
-
- for stream in listener.incoming() {
- let stream = stream.unwrap();
-
- handle_connection(stream);
- }
- }
-
- fn handle_connection(mut stream: TcpStream) {
- let buf_reader = BufReader::new(&mut stream);
- let http_request: Vec<_> = buf_reader
- .lines()
- .map(|result| result.unwrap())
- .take_while(|line| !line.is_empty())
- .collect();
-
- let response = "HTTP/1.1 200 OK\r\n\r\n";
-
- stream.write_all(response.as_bytes()).unwrap();
- }
hello.html
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <title>Hello!</title>
- </head>
- <body>
- <h1>Hello!</h1>
- <p>Hi from Rust</p>
- </body>
- </html>
404.html
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <title>Hello!</title>
- </head>
- <body>
- <h1>Oops!</h1>
- <p>Sorry, I don't know what you're asking for.</p>
- </body>
- </html>
- use std::{
- fs,
- io::{prelude::*, BufReader},
- net::{TcpListener, TcpStream},
- };
-
- fn main() {
- let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
- for stream in listener.incoming() {
- let stream = stream.unwrap();
- handle_connection(stream);
- }
- }
-
- fn handle_connection(mut stream: TcpStream) {
- let buf_reader = BufReader::new(&mut stream);
- let request_line = buf_reader.lines().next().unwrap().unwrap();
-
- let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
- ("HTTP/1.1 200 OK", "hello.html")
- } else {
- ("HTTP/1.1 404 NOT FOUND", "404.html")
- };
-
- let contents = fs::read_to_string(filename).unwrap();
- let length = contents.len();
-
- let response =
- format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
-
- stream.write_all(response.as_bytes()).unwrap();
- }
线程池(thread pool)是一组预先分配的等待或准备处理任务的线程。当程序收到一个新任务,线程池中的一个线程会被分配任务,这个线程会离开并处理任务。其余的线程则可用于处理在第一个线程处理任务的同时处理其他接收到的任务。当第一个线程处理完任务时,它会返回空闲线程池中等待处理新任务。线程池允许我们并发处理连接,增加 server 的吞吐量。
为每一个请求分配线程
- fn main() {
- let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
-
- for stream in listener.incoming() {
- let stream = stream.unwrap();
-
- thread::spawn(|| {
- handle_connection(stream);
- });
- }
- }
创建有限数量的线程
- fn main() {
- let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
- let pool = ThreadPool::new(4);
-
- for stream in listener.incoming() {
- let stream = stream.unwrap();
-
- pool.execute(|| {
- handle_connection(stream);
- });
- }
- }
为 ThreadPool
实现 Drop
trait 对线程池中的每一个线程调用 join
,这样这些线程将会执行完它们的请求。接着会为 ThreadPool
实现一个告诉线程它们应该停止接收新请求并结束的方式。
:https://course.rs/about-book.html
1. 寻找牛刀,以便小试:https://course.rs/first-try/intro.html
2. Rust 基础入门:https://course.rs/basic/intro.html
3. 入门实战:文件搜索工具:https://course.rs/basic-practice/intro.html
4. Rust 高级进阶:https://course.rs/advance/intro.html
5. 进阶实战1: 实现一个 web 服务器:https://course.rs/advance-practice1/intro.html
6. 进阶实战2: 实现一个简单 Redis:https://course.rs/advance-practice/intro.html
7. Rust 难点攻关:https://course.rs/difficulties/intro.html
8. 自动化测试:https://course.rs/test/intro.html
9. Cargo 使用指南:https://course.rs/cargo/intro.html
10. 企业落地实践:https://course.rs/usecases/intro.html
11. 日志和监控:https://course.rs/logs/intro.html
12. Rust 最佳实践:https://course.rs/practice/intro.html
13. 手把手带你实现链表:https://course.rs/too-many-lists/intro.html
14. 征服编译错误:https://course.rs/compiler/intro.html
15. Rust 性能优化 todo:https://course.rs/profiling/intro.html
16. Appendix:https://course.rs/appendix/keywords.html
英文文档:https://doc.rust-lang.org/rust-by-example/
中文文档:https://rustwiki.org/zh-CN/rust-by-example/
查看更多 Rust 官方文档中英文双语教程,包括双语版《Rust 程序设计语言》(出版书名为《Rust 权威指南》), Rust 标准库中文版。
《通过例子学 Rust》(Rust By Example 中文版)翻译自 Rust By Example,中文版最后更新时间:2022-1-26。查看此书的 Github 翻译项目和源码。
Rust学习笔记-异步编程(async/await/Future):https://zhuanlan.zhihu.com/p/611587154
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。