https://fasterthanli.me/articles/a-rust-match-made-in-hell
我经常会写一些展示代码用于展示Rust可以如何棒的为你工作,Rust可以如何让你构建强大的抽象,避免你犯一系列错误。
其中某些代码令我感到非常难受。
我写过的文章 Some mistakes Rust doesn’t catch (Rust不会捕获的错误)总是会受到一些由于我说了Rust的好话而生气的人的反对。
所以,自然地,质疑就来了:“你对其他语言不公平!Rust不可能那么棒,你肯定隐瞒了什么,我在2014年试过Rust,感觉很糟!”。
好吧,你们这些等着幸灾乐祸的家伙,我们来看下我遇到的,花费了我,一个Rust拥护者,足足一周的一个footgun(译者注:footgun指在一个产品上添加一个新东西,容易让枪打着自己脚。表明设计不好,促使用户不敢加东西。对应到这里,就是用来实现这个产品的语言不够好)。
基本情况
我们先来复习下Rust语法!
Rust如你理解的命令式语言一样,有if else两个关键字。
fn is_good() -> bool {
true
}
fn main() {
if is_good() {
println!("It is good");
} else {
println!("It isn't good, yet");
}
}
cargo run --quiet
It is good
他们不是声明,它们是表达式!可以组成三元运算表达式:
fn is_good() -> bool {
true
}
fn main() {
let msg = if is_good() {
"It is good"
} else {
"It isn't good, yet"
};
println!("{msg}");
}
而由于它们是表达式,它们可以用在任何地方:
fn is_good() -> bool {
true
}
fn main() {
println!(
"{}",
if is_good() {
"It is good"
} else {
"It isn't good, yet"
}
)
}
但是,Rust也有match,它像一个更强大的switch。
fn is_good() -> bool {
true
}
fn main() {
let msg = match is_good() {
true => "It is good",
false => "It isn't good, yet",
};
println!("{msg}")
}
酷熊:这里也没看出更强大啊
确实,但是你可以用多种模式匹配被检查项目,有多个”arms” ( 在上面的例子就是 true => {}, false => {} ).
use rand::Rng;
fn main() {
let msg = match rand::thread_rng().gen_range(0..=10) {
// match only 10
10 => "Overwhelming victory",
// match anything 5 or above
5.. => "Victory",
// match anything else (fallback case)
_ => "Defeat",
};
println!("{msg}")
}
$ cargo run --quiet
Victory
$ cargo run --quiet
Overwhelming victory
$ cargo run --quiet
Defeat
我们等会看到更多的match。
Rust也有enum类型。它们不只是一组integer或者String,它们是一堆真正的类型的组合。
假设我们有一个函数叫process,它们可以安全,或者不安全的工作。
fn process(secure: bool) {
if secure {
println!("No hackers plz");
} else {
println!("Come on in");
}
}
fn main() {
process(false)
}
从调用方的角度很难搞懂参数到底在干什么。如果你在类似VSCode这样的编辑器中用了 Rust Analyzer 支持嵌入提示,那就比较明显了,这里加了参数名。
但是我们还是更想要个枚举。
pub enum Protection {
Secure,
Insecure,
}
fn process(prot: Protection) {
match prot {
Protection::Secure => {
println!("No hackers plz");
}
Protection::Insecure => {
println!("Come on in");
}
}
}
fn main() {
process(Protection::Insecure)
}
因为这样的话,调用方即使脱离ide(比如 github上review PR,gitlab的MR,或者是邮件的补丁)也具有可读性。
并且,由于Protection::Secure 和 Protection::Insecure 是唯一的符号(相对于true和false),借助于我的IDE,我可以通过查找谁引用了Protection::Insecure 找出不安全处理的代码。
并且,比如,我也可以把它标记为废弃,那么在不改变类型签名的前提下,任何调用它的地方都会产生警告。
pub enum Protection {
Secure,
#[deprecated = "using secure mode everywhere is now strongly recommended"]
Insecure,
}
fn process(prot: Protection) {
match prot {
Protection::Secure => {
println!("No hackers plz");
}
// We still need to handle this case
#[allow(deprecated)]
Protection::Insecure => {
println!("Come on in");
}
}
}
fn main() {
process(Protection::Insecure)
}
cargo check
Checking lox v0.1.0 (/home/amos/bearcove/lox)
warning: use of deprecated unit variant `Protection::Insecure`: using secure mode everywhere is now strongly recommended
--> src/main.rs:21:25
|
21 | process(Protection::Insecure)
| ^^^^^^^^
|
= note: `#[warn(deprecated)]` on by default
warning: `lox` (bin "lox") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
由于我的IDE有 Error Lens 这个插件,它甚至直接在行内展示了出来。
如果你想知道为什么我花费大量时间展示这些工具,那是因为这是“Rust学习经历”中重要的一部分:强大的诊断功能?这是一个feature;能看到所有对一个符号的引用?这是一个feature,或者简单的重命名一个符号?这是一个feature。这是java早就有的东西(通过eclipse,netbean),但是在Python和C++中实现非常困难。
酷熊:C++现在的IDE支持了吗?
并非不存在,但是一些语言功能使它非常难以优化。
酷熊:Rust的宏编程不是有同样的问题吗?
某种程度上是的,尽管 rust-analyzer 在这方面做的已经非常好了,甚至过程宏都能处理了。但是还是会有“哦 这里没有自动补全”,“哦 这里没有导入建议”。但是情况在过去几个月内已经好多了。
枚举可以有关联数据。
pub enum Protection {
Secure { version: u64 },
Insecure,
}
fn process(prot: Protection) {
match prot {
Protection::Secure { version } => {
println!("Hacker-safe thanks to protocol v{version}");
}
Protection::Insecure => {
println!("Come on in");
}
}
}
fn main() {
process(Protection::Secure { version: 2 })
}
$ cargo run --quiet
Hacker-safe thanks to protocol v2
你能看到在第一个match匹配项中,我们解构了变量,从中提取出version。最终变成一个u64的变量绑定。这里嵌入提示也非常有用:
我们不大可能有大量版本的安全协议,因此我们在真正的安全协议中通常会用固定大小的整数,我们直接在代码里处理它们,我们也许会想把它们显示展示出来:
pub enum Protection {
Secure(SecureVersion),
Insecure,
}
#[derive(Debug)]
pub enum SecureVersion {
V1,
V2,
V2_1,
}
fn process(prot: Protection) {
match prot {
Protection::Secure(version) => {
println!("Hacker-safe thanks to protocol {version:?}");
}
Protection::Insecure => {
println!("Come on in");
}
}
}
fn main() {
process(Protection::Secure(SecureVersion::V2_1))
}
$ cargo run --quiet
Hacker-safe thanks to protocol V2_1
Clone和Copy
我们的Protection 类型,目前为止既不是Copy也不是Clone。这意味着,我们传给process时,是把参数move进去的。一旦值被move进某个东西时,我们就不再有它的所有权了。所以这样的代码就不能工作了:
fn main() {
let prot = Protection::Secure(SecureVersion::V2_1);
process(prot);
process(prot);
}
$ cargo run --quiet
error[E0382]: use of moved value: `prot`
--> src/main.rs:27:13
|
25 | let prot = Protection::Secure(SecureVersion::V2_1);
| ---- move occurs because `prot` has type `Protection`, which does not implement the `Copy` trait
26 | process(prot);
| ---- value moved here
27 | process(prot);
| ^^^^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `lox` due to previous error
Protection 是POD(https://stackoverflow.com/questions/146452/what-are-pod-types-in-c)所以它做bit级别的Copy到别的地方是无害的:它会有相同的行为。
所以此处,我们可以用过程宏Clone和Copy,所以此处并没有move进process中,将会是copy。
#[derive(Clone, Copy)]
pub enum Protection {
Secure(SecureVersion),
Insecure,
}
cargo run --quiet
error[E0204]: the trait `Copy` may not be implemented for this type
--> src/main.rs:1:17
|
1 | #[derive(Clone, Copy)]
| ^^^^
2 | pub enum Protection {
3 | Secure(SecureVersion),
| ------------- this field does not implement `Copy`
|
= note: this error originates in the derive macro `Copy` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0204`.
error: could not compile `lox` due to previous error
嗯,好吧, 我们还需要SecureVersion实现Copy,我们来写下:、
#[derive(Clone, Copy)]
pub enum Protection {
Secure(SecureVersion),
Insecure,
}
// 👇 👇
#[derive(Clone, Copy, Debug)]
pub enum SecureVersion {
V1,
V2,
V2_1,
}
$ cargo run --quiet
Hacker-safe thanks to protocol V2_1
Hacker-safe thanks to protocol V2_1
实现Copy和Clone的过程宏对这些类型非常有意义,但是假如我们由于学习的原因,我们不实现,我们回到这样的代码:
pub enum Protection {
Secure(SecureVersion),
Insecure,
}
#[derive(Debug)]
pub enum SecureVersion {
V1,
V2,
V2_1,
}
fn process(prot: Protection) {
match prot {
Protection::Secure(version) => {
println!("Hacker-safe thanks to protocol {version:?}");
}
Protection::Insecure => {
println!("Come on in");
}
}
}
fn main() {
let prot = Protection::Secure(SecureVersion::V2_1);
process(prot);
process(prot);
}
然后它编译失败了:
$ cargo run --quiet
error[E0382]: use of moved value: `prot`
--> src/main.rs:27:13
|
25 | let prot = Protection::Secure(SecureVersion::V2_1);
| ---- move occurs because `prot` has type `Protection`, which does not implement the `Copy` trait
26 | process(prot);
| ---- value moved here
27 | process(prot);
| ^^^^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `lox` due to previous error
我们无法将一个值多次将同一个Protection传给process?
好吧,我们不能传值(包括move和copy),我们可以传引用。
fn process(prot: &Protection) {
match prot {
Protection::Secure(version) => {
println!("Hacker-safe thanks to protocol {version:?}");
}
Protection::Insecure => {
println!("Come on in");
}
}
}
fn main() {
let prot = Protection::Secure(SecureVersion::V2_1);
// 👇
process(&prot);
// 👇
process(&prot);
}
$ cargo run --quiet
Hacker-safe thanks to protocol V2_1
Hacker-safe thanks to protocol V2_1
这样可以!
我们改下process函数的签名,但是我们没有改match的部分:它仍是这样:
match prot {
Protection::Secure(version) => {
println!("Hacker-safe thanks to protocol {version:?}");
}
Protection::Insecure => {
println!("Come on in");
}
}
这意味着这里的代码仍然能正常工作,无论prot是Protection还是&Protection。这里很有趣…,非常有趣。
这里我给你们提个问题,第一个match项的version类型是什么?
我们都不用想,因为我能看到潜入的提示:
这里很有趣了,我们能想匹配Protection一样匹配&Protection,在传值的情况下,我们拿到的是SecureVersion,在传引用的情况下,我们拿到的是&SecureVersion。
这看起来是个好事情,因为我们正想让代码这样工作。
现在,我们试点不一样的。
Locks
Rust不会让你在同一时间创建多于一个的可变引用。这是加强内存安全的一种方式。
例如:这样的代码不能编译:
fn main() {
let mut counter = 0_u64;
crossbeam::scope(|s| {
for _ in 0..3 {
s.spawn(|_| {
counter += 1;
});
}
})
.unwrap();
}
因为我们不能把同一个值的可变引用给三个线程。
$ cargo run --quiet
error[E0499]: cannot borrow `counter` as mutable more than once at a time
--> src/main.rs:6:21
|
4 | crossbeam::scope(|s| {
| - has type `&Scope<'1>`
5 | for _ in 0..3 {
6 | s.spawn(|_| {
| - ^^^ `counter` was mutably borrowed here in the previous iteration of the loop
| _____________|
| |
7 | | counter += 1;
| | ------- borrows occur due to use of `counter` in closure
8 | | });
| |______________- argument requires that `counter` is borrowed for `'1`
For more information about this error, try `rustc --explain E0499`.
error: could not compile `lox` due to previous error
解决此问题的一种方式是Mutex
,Rust中,mutex拥有自己所保护的数据,所以此处我们想要的是Mutex
use parking_lot::Mutex;
fn main() {
let counter = Mutex::new(0_u64);
crossbeam::scope(|s| {
for _ in 0..3 {
s.spawn(|_| {
// let's increment it a "couple" times
for _ in 0..100_000 {
*counter.lock() += 1;
}
});
}
})
.unwrap();
let counter = counter.into_inner();
println!("final count: {counter}");
}
$ cargo run --quiet
final count: 300000
类型系统和Mutex的设计,保证了我们只有在拥有锁时才能读写数据。
parking_alot::Mutext::lock的签名如下:
pub fn lock(&self) -> MutexGuard<'_, R, T> {
它的意思是:
- 获取一个Mutex的不可变引用
- 返回一个生命周期不长于Mutex的不可变引用的
MutexGuard
- 参数化锁类型R (此处是RawMutex)。
- 参数化被保护的类型T,此处是u64。
MutexGuard
实现了Deref和DerefMut,这意味着它可以作为指向T的智能指针。
换句话说,在乏味的类型注解的帮助下,我们可以这么玩:
use parking_lot::Mutex;
fn main() {
let counter = Mutex::new(0_u64);
let mut guard = counter.lock();
// Using `DerefMut`
let mutable_ref: &mut u64 = &mut guard;
*mutable_ref = 42;
// Using `Deref`
let immutable_ref: &u64 = &guard;
dbg!(immutable_ref);
}