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仓库, 而不是简单查询服务器获得结果.
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.0let 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;// 写法1fn my_print<T: Debug>(x: T) { ...}// 写法2fn 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的新型Resulttype 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>