Rust

  • 没有传统意义上的接口.
  • 变量所有权和生命周期导致提取式重构的实现成本显著增大.
  • 变量所有权和生命周期导致大量时间被花在与编译器对抗上.
  • 不得不访问共享状态时(常见于多线程服务器), 事实上仍需要使用Mutex(互斥锁).
  • VSCode的编辑器支持很差, 几乎一定要用IntelliJ IDEA.
  • 编译时的内存用量过高.
  • 类似于ECMAScript, rust从2015年开始每三年发布一个版本, 在发展过程中会淘汰掉旧模式.
    问题在于大多数文档/博客编写者并没有意识到需要区分版本,
    这种模糊造成学习成本徒增, 不理解rust发展史的新人会因此走弯路.
  • 不支持字符串枚举, 在解析JSON时会比较麻烦.
  • 不支持类TypeScript的联合类型(union types), 类型会受到严格限制, 导致需要针对结构相似的类型编写重复的代码.
    泛型并不能完全解决这个问题.
    目前可以通过auto_enums宏来曲线救国: https://github.com/taiki-e/auto_enums
  • 缺乏"异常"概念.
    基于Option和Result的链式错误处理非常糟糕:
    • 创建了一堆很难仅凭名字区分用途的链式方法.
    • 不同分支的后续代码被穿插在一起, 造成混乱.
    • 链式方法经常需要使用闭包, 然而闭包会引入rust的所有权机制, 招来更大的麻烦.
    • 代码很丑陋.
  • 有大量本该成为语法的功能是由社区实现的, 遍地都是宏, 学习曲线非常陡峭.
  • 没有C-like for循环, 这在实现一些算法时相当致命, 将被迫使用while循环来替代.

https://github.com/casey/just

https://github.com/sagiegurari/cargo-make

https://github.com/google/evcxr

下载量最大的HTTP客户端库, 基于hyper, 支持WASM.

纯Rust的阻塞式HTTP客户端库.

HTTP多后端客户端库, 可用于WASM.

https://github.com/sagebind/isahc/

使用libcurl的HTTP客户端库.

官方书籍: https://rust-lang.github.io/async-book/

Rust中的Future类型由标准库实现, 是惰性的.

Rust中的async/.await由编译器实现, 是零开销的.
被async修饰的函数或块返回 =impl Future<Output = T>=.
被async修饰的函数或块在运行时可以通过 Future.await 让出线程, 而不是阻塞线程.

async fn do_foo_and_bar() {
foo().await;
bar().await;
}

最流行的异步运行时, 尽管异步库没有一个统一的答案, 但Tokio似乎已经发展成了一种事实标准.

Tokio被Deno项目使用.

#[tokio::main]
async fn main() {
...
}

一个社区异步库, 提供std的异步版本.

一个社区异步库, 提供了各种有利于Future使用的函数和宏,
例如 futures::join! (名称来自于Fork-Join模型, 功能相当于JavaScript里的 Promise.all).

async fn do_foo_and_bar_at_same_time {
let future1 = foo();
let future2 = bar();
join!(future1, future2);
}

Rust中的编译单元, 可以生成为库(lib, dll)或可执行文件.

crate之间不能出现循环引用.

cargo是Rust的命令行包管理器.

cargo new hello_world --bin 创建可执行文件项目
cargo new hello_world --lib 创建库项目

cargo build 编译(实际进行编译使用的是rustc)
RUSTFLAGS="-C target-feature=-crt-static" cargo build 用于musl工具链的编译
cargo run 编译执行
cargo clean 清除编译结果
cargo doc 生成文档
cargo test 单元测试
cargo test -- --nocapture 保留输出(println)的单元测试
cargo bench 性能测试
cargo update 更新依赖项的锁定文件
cargo install 安装可执行程序
cargo uninstall 删除可执行程序

由社区开发的cargo子命令.
cargo add 添加依赖项
cargo rm 删除依赖项
cargo upgrade 升级依赖项, 会修改Cargo.toml文件

Rust的registry索引原始设计十分病态:
它是一个构建在Git仓库上的平面文件数据库, 实际的数据文件是NDJSON格式.
这项莫明其妙的设计导致:

  • registry服务不仅需要操作Git仓库, 还需要向外暴露Git仓库.
    这为实施替代品增添了很多不必要的麻烦, 并且引入了很多决策问题.
  • cargo为了下载软件包, 需要克隆Git仓库, 而不是简单查询服务器获得结果.

RFC2789引入了基于HTTP静态文件服务器的registry来解决registry早期设计的缺陷,
但相关实现(https://github.com/rust-lang/cargo/pull/8890)还没有就位,
并且与身份验证相关的RFC3139还没有合并.

https://github.com/rust-lang/cargo/wiki/Third-party-registries

Rust中的模块系统.

Rust中的每一个源文件都是模块, 模块名与文件名相同.
在模块外部使用这些模块时, 需要通过 mod 导入这些模块, 然后才能 use.

在源文件里可以继续用 mod 定义子模块.
模块默认是私有, 对其他文件不可见, 通过 pub 修饰符将它们变成公有.

以下内容于Rust 2018淘汰:

  • extern crate
  • #[macro_use]

Rust会自动识别 main.rs, lib.rs, mod.rs 文件, 因此不需要也不应该显式通过 mod 导入它们.

相当于Node.js的index.js, 通常用来作为模块的索引入口.rs= 因此不需要.

在Rust 2018里, 允许用户在模块的上一级文件夹中使用与模块同名的rs文件替代mod.rs,
如此设计是因为mod.rs文件对文本编辑器不友好(多个mod.rs文件难以区分).
人们普遍认为Rust 2015的目录结构更加"内聚", 所以不建议使用Rust 2018新增的目录结构.

use crate::
use self::, self相当于路径里的 ., 指的是当前的模块(但由于每个文件都是模块, self其实就是指向该文件模块本身).
use super::, super相当于路径里的 ...

尽管名称有一定的迷惑性, 但它的功能是使用模块的同时将模块内容再次导出.

单行写法:

[dependencies]
rand = { git = "https://github.com/rust-lang-nursery/rand", branch = "next" }

可读性更好的多行写法:

[dependencies.rand]
git = "https://github.com/rust-lang-nursery/rand"
branch = "next"

cargo访问私有Git仓库时通过ssh-agent进行认证, 因此必须启用ssh-agent:

# 启动ssh-agent, 并执行ssh-agent的返回值:
# SSH_AUTH_SOCK=/tmp/ssh-xxxxx/agent.xxxxxx
# 作用是将sock地址赋值给环境变量SSH_AUTH_SOCK, 以供其他程序访问.
eval `ssh-agent`
# 加载本地的ssh凭证
ssh-add

由于缺乏流行的私有registry实现, 且一些改进提案仍在推进,
目前只推荐使用基于私有Git仓库的私有包方案.
为了能在CI/CD里正确拉取项目代码, 需要在对应的代码仓库添加只读的部署公钥.

[dependencies]
crate-name = { path = "../../library/crate-name" }

一个包含最常见的type和trait的模块, 会由Rust编译器自动导入.

Rust可以用type关键字给类型起别名, 以增强代码的可读性.

type Age = u32;
// 支持泛型
type Double<T> = (T, Vec<T>);

Rust允许在一个代码块中重复声明变量, 后声明的变量会遮蔽掉先声明的变量.

这同样能用来改变变量的修饰符:

let mut v = Vec::new();
let v = v;
// 反之亦然
let v = Vec::new();
let mut v = v;

变量遮蔽不会导致被遮蔽变量的生命周期提前结束.

Rust中的变量在默认情况下是不可变的.
如果需要让其可变, 必须加上 mut 修饰符.

Rust中的下划线用于忽略变量绑定.

绑定到下划线上的内存会立即调用析构函数(被销毁).

用static关键字声明的变量是静态变量(生命周期直到程序结束的一种变量).
static也是Rust中声明全局变量的唯一方法.

static GLOBAL: i32 = 0;

用const关键字声明常量.

const GLOBAL: i32 = 0;

const声明的常量很可能会被编译器优化为内联值.

Rust里极少发生隐式类型转换, 不同类型的转换通常需要用 as 显式完成.

隐式类型转换通常只发生在智能指针上:

  • &String 会转换为 &str
  • &Vec<i32> 会转换为 &[i32]
  • &Box<Chessboard> 会转换为 &Chessboard.
  • true
  • false

char表示任何一个unicode字符值, 占用4个字节的内存空间.

let love = 'c';
let c1 = '\n';
let c2 = '\x7f';
let c3 = '\u{7FFF}';

可以用u8表示单个ASCII字符, 只占用1个字节.

let x: u8 = 1;
let y: u8 = b'A'; // 字节
let s: &[u8; 5] = b"hello"; // 字节字符串

Rust的整数类型由类型定义其占用的内存空间, 而不是像C类语言那样由编译器定义类型的内存空间.

类型 有符号 无符号
8 bits i8 u8
16 bits i16 u16
32 bits i32 u32
64 bits i64 u64
128 bits i128 u128
Pointer size isize usize

pointer size会根据平台改变实际的类型,
在32位平台上, usize相当于u32, 在64位上, usize相当于u64.
标准库和类型推导都倾向于使用pointer size而不是具体的类型.

let var1: i32 = 32;
let var2: i32 = 0xFF;
let var3: i32 = 0o55;
let var4: i32 = 0b1001;
let var5 = 0x_1234_ABCD; // 任意添加_以增强可读性
let var6 = 123usize; // 指定字面量类型
let var7 = 32; // 默认为i32类型
9_i32.pow(3); // 直接在字面量上调用函数

整数会自带溢出检查(降低性能), 溢出时引发panic.

溢出时会自动舍弃高位.

Rust使用IEEE 754-2008标准的浮点数, 分为 f32 和 f64 两种.

let f1 = 123.0f64;
let f2 = 0.1f64;
let f3 = 0.1f32;
let f4 = 12E+99_f64;
let f5: f64 = 2;

根据标准, 浮点数分为以下类型:

  • Nan 不是数字 std::f32::NAN
  • Infinite 无穷大 std::f32::INFINITY
  • Zero 零
  • Subnormal
  • Normal 正常浮点数
类型 含义
Box<T> 指向类型T的, 独占所有权的, 有权释放内存的指针.
&T 指向类型T的借用指针, 或称引用. 无权释放内存, 无权写数据.
&mut T &T加上mut修饰符的版本, 有权写数据.
*const T 指向类型T的只读裸指针, 没有生命周期信息, 无权写数据.
*mut T 指向类型T的可读写裸指针, 没有声明周期信息, 有权写数据.
Rc<T> 指向类型T的引用计数指针, 共享所有权, 线程不安全.
Arc<T> 指向类型T的原子引用计数指针, 共享所有权, 线程安全.
Cow<'a, T> 写时复制(clone-on-write)指针.
let a = (1i32, false);
let b = ("a", (1i32, 2i32));
let c = (0,); // 只有一个元素的元组
let d: () = (); // 空元组(unit, 单元类型), 不占用内存空间
// 通过模式匹配访问元组成员
let p = (1i32, 2i32);
let (a, b) = p;
// 通过索引访问元组成员
let x = p.0
let y = p.1

结构体有时会提供一个new函数作为该结构体的工厂函数.
结构体有时会提供一个default函数返回该结构体的默认值.
Rust不支持继承/混入结构体.

struct Point {
x: i32,
y: 32, // 最后的逗号可以省略, 但出于习惯和版本控制目的, 建议使用
}
fn main() {
let p = Point { x: 0, y: 0 };
println!("Point is at {} {}", p.x, p.y);
// 简写
let x = 10;
let y = 20;
let p = Point { x, y };
// 类似JavaScript的spread语法
let p = Point { ..p };
let Point { x: px, y: py } = p;
let Point { x, y } = p;
}
// 空结构体
struct Foo1;
struct Foo2();
struct Foo3 {}
struct Color(i32, i32, i32);
struct Color {
0: i32,
1: i32,
2: i32,
}
// 用来创造独立的类型
fn main() {
struct Inches(i32);
fn f(value: Inches) {}
let v: i32 = 0;
f(v); // 编译不通过, 因为Inches和i32不是一个类型, 且Rust不允许隐式类型转换
}

Rust中的枚举类型是一种带有类型注记(变体, variant)的代数数据类型(ADT), 而不是其他编程语言里常见的常量枚举.
和Haskell一样, variant可以作为函数使用.

enum Number {
Int(i32),
Float(f32),
}
fn read_num(num: &Number) {
// 枚举用于模式匹配
match num {
&Number::Int(value) => println!("Integer {}", value),
&Number::Float(value) => println!("Float {}", value),
}
}
fn main() {
let n: Number = Number::Int(10);
read_num(&n);
}

枚举可以用来表示JSON数据类型.

enum Json {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec<Json>),
Object(Box<HashMap<String, Json>>)
}
enum BinaryTree<T> {
Empty,
NonEmpty(Box<TreeNode<T>>)
}
struct TreeNode<T> {
element: T,
left: BinaryTree<T>,
right: BinaryTree<T>
}

Rust标准库中的Result和Option就是enum类型.

enum Result<T, E> {
Ok(T),
Err(E)
}
enum Option<T> {
None,
Some(T),
}

Rust数组的元素个数是在编译阶段决定的, 因此是固定长度.

在数组上可以使用切片的方法.

除非使用引用类型, 否则Rust中的数组赋值默认是clone.

let xs: [i32; 5] = [1, 2, 3, 4, 5];
let xs: [i32; 500] = [0; 500]; // 批量初始化
let xs: [i32; 0] = []; // 空数组
// 多维数组
let xs: [[i32; 2]; 3] = [[0, 0], [0, 0], [0, 0]];

Rust中的数组索引访问会带有运行时的边界检查, 索引访问的性能不如C/C++.
使用迭代器访问时则不会有边界检查.

for (index, value) in v.iter().enumerate() {
...
}

在数组上调用borrow(借用元素的引用)方法, 可以生成切片, 用 & 作为语法糖.
切片是一种没有所有权的数据视图(指针), 可以视作是数组的取只读引用.
切片是胖指针(fat pointer), 同时包含指向的地址和数据的长度.

let r = 1..10; // Range<i32>, [1, 10)
let r = 1i32..10;
// 等同于
let r = Range { start: 1, end: 10 };
// 用于数组切片
let arr: [i32; 5] = [1, 2, 3, 4, 5];
&arr[..]
&arr[2..];
&arr[..2];
// 左闭右闭区间
start..=end // std::ops::RangeInclusive
// 右闭区间(左边在有符号时为负无穷, 无符号时为0)
..=end // std::ops::RangeToInclusive

Vec的元素数量超过其缓冲区容量时, 会创建一个比原来大一倍的新缓冲区.

Rust里的字符串切片视图, 是一个指向内存的定长指针, 同时也是字面量字符串的类型.
加上 mut 时可变, 但不能修改其长度.
它不是char类型(Unicode)的数组, 而是u8类型的数组,
当需要将它视作高级编程语言的字符串时, 需要将其转为 Vec<char>.

在结构体中使用 &str 时, 需要显式声明其生命周期参数.
这会让编码变得非常痛苦, 因此一些人倾向于在结构体里使用 String.

let s: &str = "Hello"
// 由于字符串使用了utf-8变长编码, 字符所处的内存位置不能直接得到.
// Rust里按索引取字符需要这样写:
s.chars().nth(n);
// 原始字符串
// r之后可以添加任意数量的井号(#), 字符串末端需要以相同数量的井号结束, 从而允许在原始字符串里输入双引号(").
let path = r"C:\Program Files\Rust";
// 字符串连接
let new_str = &str + &str; // 反直觉的是, 这在Rust里会报错.
// 解决方案: 创建一个String作为新字符串的容器
let mut new_string = String::new();
new_string.push_str(s);
new_string.push_str(s);
let new_str = new_string.as_str(); // 转换为&str

String 可以扩容, 其内部实现类似于 Vec<u8> 类型.

let mut s: String = String::from("Hello");
s.push(' ');
s.push_str("World.");
s.as_str(); // 转换为&str类型

String 的切片和数组切片行为类似, 本质上是在相应内存创建了一个数据更少的新视图.

  • String 是一种类似于 Vec 的动态字符串类型, 位于堆, 可以从中得到切片视图 &str.
  • &str 是一个指向内存中数据的指针/切片视图(C语言的 char*).
    指向的地方可以是 string.as_str() (指向堆),
    可以是在创建时就知道其长度的字面量 &str (指向栈),
    也可以是 &'static str (指向程序的静态存储, 静态存储在程序运行时加载进内存).

if语句后的大括号是强制的.

if n < 0 {
...
} else if n > 0 {
...
} else {
...
}
// 模式匹配解构赋值
if let Some(x) = optVal {
...
}

if本身是一种表达式, 相当于三元运算符.

let x: i32 = if condition { 1 } else { 2 };

match是Rust的模式匹配功能, 编译器会强制要求程序员处理所有match情况.
Rust的match表达式能够支持非常复杂的模式匹配.

enum Direction {
East, West, South, North
}
fn print(x: Direction) {
match x {
Direction::East => { ... }
Direction::West => { ... }
Direction::South => { ... }
Direction::North => { ... }
}
}
match x {
i if i > 5 => ...,
i if i <= 5 => ...,
_ => unreachable!(), // 由于编译器不懂得数学运算, 因此需要手动提示编译器
}
match x {
'0' ... '9' => read_number(),
'a' ... 'z' => read_word(),
' ' | '\t' | '\n' => read_whitespace(),
_ => skip()
}
match x {
ref r => ..., // 用ref关键字表示匹配的是引用而不是值, 从而避免所有权转移
}
enum OptionalInt {
Value(i32),
Missing,
}
let x = OptionalInt::Value(5);
match x {
// 通过if表达式强化变体的条件匹配
OptionalInt::Value(i) if i > 5 => ...,
// `..`表示不在乎内容是什么
OptionalInt::Value(..) => ...,
OptionalInt::Missing => ...,
}

一种语法糖, 直接将匹配到的右值绑定到左值代表的变量上.

// 原始代码
match self.get_selection() {
Shape::Rect(top_left, bottom_right) =>
optimized_paint(&Shape::Rect(top_left, bottom_right)),
_ => // ...
}
// 用@模式简写
match self.get_selection() {
rect @ Shape::Rect(..) =>
optimized_paint(&rect),
_ => // ...
}

与其他语言不同, 语句块在Rust里是一个较为常用的功能:
由于Rust独特的变量所有权机制, 有时候需要使用语句块来控制变量的生存周期.

let x: i32 = {
println("Hello");
1 + 2
}

无限循环, 之所以存在单独的loop关键字, 是为了方便编译器优化.

loop {
continue;
break;
}
// 带有label的循环
'a: loop {
'b: loop {
break 'a;
continue 'a;
}
}
let v = loop {
break 10;
}
while condition {
...
}
while let Some(x) = optVal {
}

Rust没有C类的for循环, 只提供for-each.

let array = &[1, 2, 3, 4, 5]; // 一个可迭代对象
for i in array {
println!("The number is {}", i);
}
// 用语法题生成一个Range, Range是可迭代的
for i in 1..10 {
println!("The nubmer is {}", i);
}

Rust中的函数偏向于函数式语言的用法, 通常不使用return返回值, 而是直接返回最后一个表达式.
空返回值表示为 ().

fn add((x, y): (i32, i32)) -> i32 {
x + y
}

发散函数相当于TypeScript里返回值类型为never的函数.

fn diverges() -> ! {
panic!("This function never returns!");
}

每个函数都有自己单独的类型, 因此不能直接作为类型赋值, 需要用fn类型.

let mut func = add as fn((i32, i32)) -> i32;
let mut func: fn((i32, i32)) -> i32 = add;

Rust里的一般函数不能访问外部环境的变量, 但闭包函数可以.

由于不能支持垃圾回收, 在Rust里使用闭包比高级语言要复杂很多.

let add = |a: i32, b: i32| -> i32 { return a + b; };
let add = |a, b| { return a + b; };
let add = |a, b| { a + b };
let add = |a, b| a + b;
// Fn, FnMut, FnOnce是专用于闭包的trait, 用来表示闭包捕获变量的方式
// `Fn` 相当于 `&T`
// `FnMut` 相当于 `&mut T`
// `FnOnce` 相当于 `T`
// Fn(i32) -> i32是专用于闭包的语法糖, 等价于Fn<i32, i32>
fn make_adder(x: i32) -> Box<Fn(i32) -> i32> {
// 当闭包的生命周期超出被捕捉变量的生命周期时, 需要用move关键字将所有权转移.
Box::new(move |y| x + y)
}

生成器是内部带有yield关键字的闭包.

trait是Rust的一种类似于Haskell的功能, 它有两个功能:

  • 用来表示此类型具有某种特型, 让符合此特型的类型自动获得该特型支持的方法(即"分派").
  • impl 为特型扩展方法.
    Rust对此行为有一些限制规则(称为一致性原则 coherence rule 或孤儿规则 orphan rule),
    以避免对来自外部的trait扩展方法:
    • impl块与trait声明在同一个crate中
    • impl块与type声明在同一个crate中.
// Rust可以在trait里直接编写实现.
trait Shape {
fn area(&self) -> f64;
}
trait Round {
fn get_radius(&self) -> f64;
}
struct Circle {
radius: f64,
}
// 为Cirlce实现Shape特型
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
// 也可以直接针对结构体添加方法
impl Circle {
fn get_radius(&self) -> f64 { self.radius }
}
// 也可以为Shape实现Round特型(为特型实现特型)
impl Shape for dyn Round {
fn area(&self) -> f64 {
std::f64::consts::PI * self.get_radius() * self.get_radius()
}
}
// 当一个类型满足多个trait, 且具有相同方法签名的多个同名方法时, 必须手动指定trait:
<Cook>::start(&me);
<Chef as Wash>::start(&me);

self是trait方法支持的第一个变量名, 其类型为Self.
只有当self作为第一个变量名时, 才能使用 =Type.FunctionName()= 调用函数.
Type.FunctionName()<Type>::FunctionName(&Name) 的语法糖.

// 不使用语法糖
trait T {
fn method1(self: Self);
fn method2(self: &Self);
fn method3(self: &mut Self);
}
// 使用语法糖时, 可以省略Self类型.
trait T {
fn method1(self);
fn method2(&self);
fn method3(&mut self);
}

第一个函数的变量名不是self时, 函数只能通过 Type::FunctionName() 的形式调用.

struct T(i32);
impl T {
fn func(this: &Self) {
println!("value {}", this.0);
}
}
fn main() {
let x = T(42);
T::func(&x);
}
use std::fmt:Debug;
// 写法1
fn my_print<T: Debug>(x: T) {
...
}
// 写法2
fn my_print(x: T) where T: Debug {
...
}

在将trait作为约束使用这件事上, dyn前缀可以解决相同的问题, 但dyn解决此问题的方法不同:
dyn会在运行时导致动态分派(dynamic dispatch), 而泛型只是编译时生成相应的代码而已.
因此, 使用dyn比使用泛型的成本要高得多.

trait Base { ... }
trait Dervied: Base { ... }
// 多重继承
trait Ord: Eq + PartialOrd<Self> {}

为结构体添加多个特型需要编写很多impl代码块, 为了简化这一过程, Rust提供了一个名为derive的attribute.

注: 只有支持自动derive的trait才可以用derive.

#[derive(Copy, Clone, Default, Debug, Hash, PartialEq, Eq, PartialOrd, Ord]
struct Foo {
data: i32
}
// 相当于
impl Copy for Foo { ... }
impl Clone for Foo { ... }
impl Default for Foo { ... }
impl Debug for Foo { ... }
impl Hash for Foo { ... }
impl PartialEq for Foo { ... }
...

类似于其他语言, Rust可以给函数添加属性/注解.

#[test]
fn test_add() {
// ...
}

Rust里的宏是卫生宏(hygiene), 是基于语法解析而不是文本替换的.

用来以数组表达式创建Vec对象的宏.

let v = vec![1, 2, 3];

在编译时将UTF-8编码的文本文件读取为字符串, 字符串会被编译到二进制里.
这个宏的亮点在于它接受的路径是相对于当前文件的, 因此很适合用来读取那些需要打包到二进制里的内容.

来自社区的宏, 生成直到运行时才会初始化的静态变量.

社区的另一个库once_cell有不使用宏的等价实现.

所有权是Rust实现内存安全和可靠并发的基石.
由于需要所有权, Rust在一定程度上放弃和反对建立对象之间的相互引用/循环引用的能力.

Rust的所有权基于以下规则:

  • 每块内存在Rust里被一个变量所有.
  • 每块内存在一个时间点上只能有一个所有者.
    注意, 一个所有者可以被另一个所有者所有, 这些所有权关系可以用一棵树来表示.
  • 变量在作用域结束时, 变量及其值会被销毁.

转移语义是Rust里向标识符传值时的默认语义, 所有权从一个标识符被转移给了另一个标识符, 这种转移是零和的.
这与C/C++不同, 因为C/C++的默认语义是复制.
这与Python等语言基于引用的赋值不同, 因为Rust不允许多个绑定同时指向一块内存.

Rust的所有权转移在大多数情况下是无成本的.

以下操作会使没有实现 std::marker::Copy 特型的类型发生所有权转移:

  • 赋值
  • 函数调用
  • 函数返回
  • 模式匹配

由于编译器难以跟踪基于索引的所有权转移(例如只转移其中的一个元素), 编译器会直接以编译错误拒绝掉相关代码.
因此, 基于索引的访问几乎总是需要事先借用(取得的是引用而不是所有权).

调用函数时的传参会导致所有权转移, 除非该函数将同一个变量以返回值形式转移, 否则调用者会失去所有权.

由于失去所有权的变量会被Rust销毁, 无法在接下来的代码中使用,
所以函数参数经常是借用或克隆(clone), 很少直接用转移.

常见于在一个代码块里多次使用一个没有实现copy特型的数据类型的时候:
由于没有实现copy特型, 在第一次操作此数据类型时,
由于某种原因发生了所有权转移(比如作为函数参数传递), 所有权在转移之后没有归还,
导致之后的代码不能获得此数据类型的所有权.

如果是因为调用函数发生了所有权转移, 则可以通过将函数的参数改为数据类型的借用(&)来解决此问题.

由于大多数函数都不需要真正的所有权转移, 因此借用可以解决大多数问题, 是推荐的方法.

可以通过克隆数据类型来避免发生所有权转移.

克隆数据不是无开销操作, 需要斟酌使用.

带有std:marker::Copy trait的类型将使用复制语义而不是移动语义.

  • 数字
  • 字符
  • bool

带有std::marker::Copy特型的类型会被安全地复制, 因此不会发生所有权转移.

复制是有成本的操作, 必然会出现memcpy, 导致内存增加.

move语义很苛刻, 为了降低编写代码的难度, Rust还有一种被称作borrow的语义.

& 只读借用, 这同时也被称为取引用.
&mut 可读写借用

借用指针规则:

  • 借用指针不能比它指向的变量存活更长时间.
  • &mut 借用只能指向本身具有mut修饰符的变量.
  • &mut 借用指针存在时, 被借用的变量会处于冻结状态.
  • 对单一变量的 & 借用指针可以同时存在多个, &mut 借用指针只能存在一个.
move copy borrow mut borrow
类型较小时的性能
类型较大时的性能
反直觉
可变

智能指针会自动发生解引用(deref, *), 从而让这些指针能够调用它们内部类型的方法.
解引用是取引用(ref, &, &mut)的反向操作符.

在使用 . 运算符时, 通常会自动发生隐式借用和解引用:

let mut v = vec![1, 2];
v.sort();
// 等价于
(&mut v).sort();

Box类型是永远 只会发生转移语义 的类型, 是一种智能指针.
除了会将将数据存储在heap而不是stack里之外, Box没有性能开销.

Box的常见使用场景:

  • 需要确保不会发生复制语义

智能指针, 带有总大小和元素个数大小的数据.

utf8字符序列的指针.

通过引用计数来允许同一个值被多个所有者使用.
通过调用clone方法来复制指针, 然后将指针转移给需要它的所有者.

根据Rust的"共享不可变, 可变不共享"原则, 共享的引用不能提供可变性, 因此Arc类型在多线程中是只读的.
为了让引用可变, 通常会与Mutex搭配使用形成 Arc<Mutex<T>>.

Arc的非线程安全版本, 性能更好, 但不允许跨线程使用(编译器会报错).

一些类型具有内部可变性, 在使用时 不需要mut修饰符就可以改变内部的值.
这之所以不违反Rust的内存安全原则, 是因为这些类型包装了内部值, 对内部值的修改都是由类型本身间接完成的.

  • Cell
  • RefCell
  • Mutex
  • RwLock
  • Atomic*

Rust里能在多线程使用的类型必须具有Sync或Send约束, 否则会出现编译错误.

一种marker trait, 带有此trait的类型在不同线程中的访问是安全的.

一种marker trait, 带有此trait的类型在不同线程中传递所有权是安全的.

使用方式是通过调用lock方法来获取锁, 得到 LockResult<MutexGuard<T>>.

Mutex有自己的析构函数, 会在作用域结束时自动调用内部的unlock方法来解锁, 用户无法自己手动调用unlock方法,
但可以通过 std::mem::drop(mutexguard) 间接实现unlock.

当锁的持有者发生panic, 导致锁不可能被释放时, Mutex会陷入中毒状态.
对于处于中毒状态而Mutex, 其他线程在获取锁时会立即抛出 PoisonError.

RwLock与Mutex相似, 但它的锁是一写多读的, 通过调用read方法获得读锁, 通过调用write方法获得写锁.

RwLock要求它的内容具有Sync.

RwLock只有写锁会中毒, 读锁不会中毒.

条件变量, 需要与Mutex成对使用, Arc<(Mutex<T>, Condvar)>.

一个变量从出生到死亡的过程被称作它的生命周期.

当类型具有 std::ops::Drop 特型时, 类型会在变量生命周期结束时自动调用它的析构函数, 从而销毁自己.

有析构函数的类型被编译器禁止拥有Copy特型.

生命周期参数使用单引号(读作"撇")开头, 后跟一个合法的标识符: 'name.
生命周期参数的name通常是从a开始的英文字母(类似于泛型的T, U, V), 它只用作标识, 没有任何其他意义.

特殊值 'static 表示生命周期为整个程序的运行阶段, 任何生命周期都等于或短于 'static.

函数, 类型可以带有生命周期参数, 它像泛型一样写在函数名后的尖括号 <> 里.
Rust里的所有值/对象都有生命周期, 只是大部分生命周期参数都会根据生命周期补全规则自动补充, 因此可以省略.

手写生命周期参数的主要目的是让一个变量/参数/成员可以与其相关的主体共享同一个生命周期参数,
从而让编译器放宽限制.
需要注意的是, 生命周期参数总是意味着"临时借用"的语义(这是编译器持续追踪它的原因),
然而, 一些场景显然并不适用这种语义:
例如, 大多数结构体在语义上都不适合"借用"来自外部的变量, 而是应该自己"拥有"这些变量,
这种情况下, 最好用不需要生命周期参数的方式重构它.

// 函数
fn select<'a>(arg1: &'a i32, arg2: &'a i32) -> &'a i32 {
if *arg1 > *arg2 { arg1 } else { arg2 }
}
// 类型
struct Test<'a> {
member: &'a str
}
impl<'t> Test<'t> {
fn test<'a>(&self, s: &'a str) {}
}

借用指针(函数的引用参数)都会带有生命周期参数, 用于局部变量时, 生命周期参数可以省略.

Rust会根据以下规则补全生命周期:

  • 带有生命周期参数的输入参数将对应不同的生命周期参数.
  • 如果只有一个输入参数有生命周期参数, 但其中有 &self&mut self, 那么返回值的生命周期会被指定为此参数.
  • 不满足以上条件时, 不能自动补全返回值的生命周期参数.

Rust的panic代表线程级别的 致命错误, 一般的错误处理应该由Option和Result完成.

自定义错误要求实现Display和Error特型.
出于调试目的, 也可以实现Debug特型, 这是可选的.

struct MyError {
details: String
}
impl MyError {
fn new(msg: &str) -> Self {
Self { details: msg.to_string() }
}
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.details)
}
}
impl Error for MyError {
fn description(&self) -> &str {
&self.details
}
}
// 所有标准库里的错误类型都可以隐式转换为Box<std::error::Error>, 利用这一点, 可以将它作为通用错误类型使用
type GenError = Box<dyn std::error::Error>;
// 一种使用GenError的新型Result
type GenResult<T> = Result<T, GenError>;
// 当遇到GenError时, 处理程序可以通过downcast_ref转换为具体错误:
if let Some(err) = gen_err.downcast_ref::<MyError>() {
// ...
}
// Debug是可选的, 用于提供人类可读的错误描述
#[derive(Debug)]
enum GenError {
Io(io::Error),
Parse(num::ParseIntError),
}
type GenResult<T> = Result<T, GenError>;

? 操作符是一个语法糖, 写在产生Result的表达式后面.
在Result是Ok时, 取消装箱;
在Result是Err时, 将Err作为当前函数的返回值.

? 操作符只能在返回类型为Result的函数里使用.

let weather = get_weather(hometown)?;
// 相当于以下代码
let weather = match get_weather(hometown) {
Ok(success_value) => success_value,
Err(err) => return Err(err)
};

? 操作符之前, 一种处理错误的风格是使用标准库里的 try! 宏, ? 操作符取代了它的作用.

  • ok_or()Option<T> 转换为 Result<T, E>
  • .is_ok(), .is_err() 用于判定Result是Ok还是Err
  • .unwrap() 取消装箱, 如果Result是 Ok<T>, 返回 T, 如果Result是Err, 会panic.
  • .expect(message) unwrap的自定义错误信息版本.
  • .unwrap_or(fallback) 取消装箱, 如果Result是 Ok<T>, 返回 T, 如果Result是Err, 则用fallback作为 T 返回.
  • .unwrap_or_else(fallback_fn) unwrap_or的函数版本, 会将err作为参数传给函数然后生成fallback.
  • .ok() 返回 Option<T>: 如果Result是 Ok<T>, 就返回 Some<T>; 如果Result是Err, 就返回 None
  • .err() 返回 Option<E>: .ok() 的Err版本.
  • map_err(fn) 转换 Err<E>, 在将错误类型标准化的时候很有用.
    map_err 将错误转换为String是一种反模式:
    • String只是字符串, 很容易将错误信息与正常变量混淆.
    • String是从具体的错误有损转换得来, 错误会更难处理.

Result的大部分方法都会发生所有权转移, 使用借用可以避免那些方法销毁掉Result.

  • .as_ref()Result<T, E> 转换为 Result<&T, &E>
  • .as_mut()Result<T, E> 转换为 Result<&mut T, &mut E>