基础语法#
rust没有truthy/falsy值
语句(Statements)是执行一些操作但不返回值的指令。 表达式(Expressions)计算并产生一个值。 表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。
数字字面值也可以使用 _ 作为视觉分隔符,方便阅读,例如 1_000,它和 1000 的值完全相同。
同名的第一个变量被第二个变量 遮蔽(shadowed)
遮蔽和把变量标记为 mut 是不同的。如果你不小心尝试在没有使用 let 关键字的情况下重新给变量赋值,就会得到编译时错误。而通过再次使用 let,我们可以对这个值做一些变换,同时又能让变量在变换完成后继续保持不可变。
变量默认是不可变的,声明常量时要用 const
let (x, y, z) = tup; // 元组解构(destructuring)rust函数必须声明其参数的类型。
当你在 debug 模式下编译时,Rust 会加入整型溢出的检查,并在发生这种情况时让程序在运行时 panic。当你使用 —release flag 在 release 模式下编译时,Rust 不会加入会导致 panic 的整型溢出检查。相反,如果发生溢出,Rust 会执行一种叫做 two’s complement wrapping 的行为。简而言之,超过该类型最大值的数会“回绕”到该类型所能表示的最小值。
数组只能包含单一类型的元素
const 可以在全局作用域中使用, let只能在函数中使用
const 变量可以在函数之外定义,并进行有限的计算。
变量不能被赋予与其原始类型不同的值
所有权与借用#
值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的抉择
在编译时大小未知或大小可能变化的数据,要改为存储在堆上。
访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问
所有权规则:
- Rust 中的每一个值都有一个 所有者(owner)。
- 值在任一时刻有且只有一个所有者。
- 当所有者离开作用域,这个值将被丢弃。
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。
为了确保内存安全,在 let s2 = s1; 之后,Rust 认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西。
拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动(move)
Rust 永远也不会自动创建数据的 “深拷贝”。
编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的,只在栈上的数据:拷贝
如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然有效。Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用 Copy 注解,将会出现一个编译时错误。
任何一组简单标量值的组合都可以实现 Copy,任何不需要分配内存或某种形式资源的类型都可以实现 Copy 。元组,当且仅当其包含的类型也都实现 Copy 的时候。
不包含任何值的元组有一个特殊名称:unit。该值及其对应的类型都用括号 () 表示,分别代表空值或空返回类型。如果表达式不返回任何其他值,则隐式返回 unit 值。
向函数传递值可能会移动或者复制,就像赋值语句一样。
变量的所有权总是遵循相同的模式:将值赋给另一个变量时它会移动。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。
与指针不同,引用在其生命周期内保证会指向某个特定类型的有效值。
正如变量默认是不可变的,引用默认也是不可变的。我们不允许通过引用修改它指向的值。
可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。 我们也不能在拥有不可变引用的同时拥有可变引用
一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。
在 Rust 中,编译器保证引用永远不会变成悬垂引用:如果你持有某些数据的引用,编译器会确保这些数据不会在它们的引用之前离开作用域。
引用自带读者写者问题解决方案。在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用。 引用必须总是有效的。
Slice 与字符串#
slice的表示是左闭右开的[..) 表示“字符串切片”的类型写为 &str
slice可以与底层字符串绑定关联,从而在底层无效时能即使报错 &str (切片引用): 它是一个胖指针 (Fat Pointer)。它直接指向那串真实的、赤裸裸的 UTF-8 字节数据,并带上数据的长度。 &String (老板的引用): 它是指向“指针”的指针。它指向的是堆分配的 String 结构体(那个包含指针、长度、容量的三词结构),而那个结构体里面的指针,才最终指向真实的数据。 两者转换用了解引用强制多态 (Deref Coercion)
“xxx”的类型是&str,在内存只读区
&str :它是一个指向二进制文件中特定位置的切片。这也是为什么字符串字面量是不可变的; &str 是一个不可变的引用。
结构体与方法#
struct整个实例必须是可变的;Rust 不允许我们只将某些字段标记为可变
结构体更新语法使用 = 就像赋值一样;这是因为它会移动数据。有copy trait的则是复制
在花括号内放置说明符 :? 表示 Debug(用{:#?}格式化打印) 并在结构体定义之前添加外部属性 #[derive(Debug)]
dbg! 宏接收一个表达式的所有权(与 println! 宏相反,后者接收的是引用),打印出代码中调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权。
当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &、&mut 或 * 以便使 object 与方法签名匹配。
在 impl 块中定义的函数被称为 关联函数(associated functions)
:: 语法用于关联函数和模块创建的命名空间。
关键字 Self 在函数的返回类型和函数体中,都是对 impl 关键字后所示类型的别名
枚举、匹配与模块#
Rust 并没有很多其他语言中有的空值功能。空值(Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
对于 if,表达式必须返回一个布尔值,而在match它可以是任何类型的
=> 运算符将模式和将要运行的代码分开
大括号分支后的逗号是可选的
Rust 中的匹配是 穷尽的(exhaustive)
使用 if let 意味着更少的输入、更少的缩进,也更少的样板代码。然而,这样也会失去 match 所强制的穷尽性检查,也就无法确保你没有遗漏某些情况。换句话说,可以认为 if let 是 match 的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。
包中可以包含至多一个库 crate(library crate)
使模块公有并不使其内容也是公有的
结构体具有私有字段,所以这个结构体需要提供一个公共的关联函数来构造其实例
在一个结构体定义的前面使用了 pub,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的。枚举则它的所有变体都将变为公有
每一个我们定义的枚举变体的名字也变成了一个构建枚举的实例的函数。无需像struct一样必须impl构造函数
如果枚举变体不是公有的,那么枚举会显得用处不大;给枚举的所有变体挨个添加 pub 是很令人恼火的,因此枚举变体默认就是公有的。结构体在许多情况下即使字段不可公有也能正常使用,所以结构体字段遵循默认私有的通用规则,除非使用 pub 关键字。
集合、泛型与 Trait#
vector只能储存相同类型,可以用enum打破限制
如果在编写程序时,你并不知道运行时究竟会有哪些类型需要存进 vector,那么这种枚举技巧就不适用了。相反,你可以使用 trait 对象
for item in &mut v 在整个循环期间“锁死”了整个 Vector;而 for i in 0..v.len() 只是在生成一堆数字,根本没有触碰 Vector,所以你在循环体内部是自由的。所以前者只能改变元素不能增删,后者则不然,但是建议从后向前删除
只能将 &str 和 String 相加,不能将两个 String 值相加。
遍历hashmap 会以任意顺序进行
我们可以为泛型参数选择一个与结构体定义中声明的泛型参数所不同的名称,不过依照惯例使用了相同的名称。
只有在 trait 或类型至少有一个属于当前 crate 时,我们才能对类型实现该 trait。
不能为外部类型实现外部 trait。这个限制是被称为相干性(coherence)的程序属性的一部分,或者更具体的说是 孤儿规则(orphan rule),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你的代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。
newtype模式:可以使用元组结构包装外部类型,并为包装实现trait的方式绕过孤儿规则 如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 Deref trait并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则只需自行实现所需的方法即可。 newtype 模式也可以用于抽象掉某个类型的部分实现细节:新的类型可以暴露与其私有内部类型不同的共有 API。
无法从一个方法的重写实现中调用与其同名的默认实现,重写时会被遮蔽
返回impl trait时只能返回单一类型,因为编译器会推断出唯一类型,所以不能在不同分支返回不同类型
声明了没有初始值的变量,这些变量存在于外部作用域。
生命周期、测试与项目结构#
返回值的生命周期通常被定义为:它与输入参数生命周期的关联关系
当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用没有指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值。然而它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。
生命周期省略(elision)的三条编译器规则:
- 第一条规则是编译器为每一个引用参数都分配一个生命周期参数。
- 第二条规则是如果只有一个输入生命周期参数,那么将它赋予给所有输出生命周期参数
- 第三条规则(也就是默认使用&self的生命周期)是如果方法有多个输入生命周期参数并且其中一个参数是 &self 或 &mut self,说明这是个方法,那么所有输出生命周期参数被赋予 self 的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。
(实现方法时)结构体字段的生命周期必须总是在 impl 关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。
所有的字符串字面值都拥有 ‘static 生命周期
每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。
Rust 默认使用线程来并行运行。这意味着测试会更快地运行完毕,所以你可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该确保测试不能相互依赖,或依赖任何共享的状态,包括依赖共享的环境,比如当前工作目录或者环境变量。
传递给 cargo test 的参数,接着是分隔符 —,再之后是传递给测试二进制文件的参数
可以指定部分测试的名称,任何名称匹配这个名称的测试会被运行
运行ignore用—ignored,全部用—include-ignored
三个部分的输出:单元测试、集成测试和文档测试。注意如果一个部分的任何测试失败,之后的部分都不会运行。
—test适用于集成测试
这个模式的一切就是为了关注分离:main.rs 处理程序运行,而 lib.rs 处理所有的真正的任务逻辑。因为不能直接测试 main 函数,这个结构通过将所有的程序逻辑移动到 lib.rs 的函数中使得我们可以测试它们。仅仅保留在 main.rs 中的代码将足够小以便阅读就可以验证其正确性。
非零的退出状态是一个惯例信号,用来告诉调用程序的进程:该程序以错误状态退出了
二进制程序的关注分离:
- 将程序拆分成 main.rs 和 lib.rs 并将程序的逻辑放入 lib.rs 中。
- 当命令行解析逻辑比较小时,可以保留在 main.rs 中。
- 当命令行解析开始变得复杂时,也同样将其从 main.rs 提取到 lib.rs 中。
- 经过这些过程之后保留在 main 函数中的责任应该被限制为:
- 使用参数值调用命令行解析逻辑
- 设置任何其他的配置
- 调用 lib.rs 中的 run 函数
- 如果 run 返回错误,则进行错误处理
TDD:
- 编写一个失败的测试,并运行它以确保它失败的原因是你所期望的。
- 编写或修改足够的代码来使新的测试通过。
- 重构刚刚增加或修改的代码,并确保测试仍然能通过。
- 从步骤 1 开始重复!
反斜杠 \ 在字符串内的作用:压缩空白
闭包、智能指针与并发#
闭包在完成第一次类型推断后就会确定下来,并非默认泛型
即使闭包体不严格需要所有权,如果希望强制闭包获取它在环境中所使用的值的所有权,可以在参数列表前使用 move 关键字。
让闭包获取所有权是为了:闭包能够脱离当前的作用域而独立存在
接受闭包的地方一般会给出(fn,fnonce,fnmute,从左到右为包含于关系)3个trait中的必要条件
在 Rust 中,由于所有权和借用的概念,引用和智能指针之间还有一个额外区别:引用只会借用数据,而智能指针在很多情况下会拥有它们所指向的数据。
因为 enum 实际上只会使用其中的一个变体,所以 Message 值所需的空间等于储存其最大变体的空间大小。
Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换:
- 当 T: Deref<Target=U> 时从 &T 到 &U。
- 当 T: DerefMut<Target=U> 时从 &mut T 到 &mut U。
- 当 T: Deref<Target=U> 时从 &mut T 到 &U。 第三个情况有些微妙:Rust 也会将可变引用强转为不可变引用。但反之是不可能 的:不可变引用永远也不能强转为可变引用。
当实例离开作用域时,Rust 会自动替我们调用 drop,并运行我们指定的代码。变量会按照创建顺序的逆序被丢弃
Rust 并不允许我们主动调用 Drop trait 的 drop 方法;当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的 std::mem::drop 函数。
Rust 不允许我们显式调用 drop,因为 Rust 仍然会在 main 结束时自动对该值调用 drop。这会导致二次释放(double free)错误,因为 Rust 会尝试清理同一个值两次。
如下为选择 Box<T>,Rc<T> 或 RefCell<T> 的理由:
- Rc<T> 允许相同数据有多个所有者;Box<T> 和 RefCell<T> 则只有单一所有者。
- Box<T> 允许在编译时执行不可变或可变借用检查;Rc<T> 仅允许在编译时执行不可变借用检查;RefCell<T> 允许在运行时执行不可变或可变借用检查。
- 因为 RefCell<T> 允许在运行时执行可变借用检查,所以我们可以在即便 RefCell<T> 自身是不可变的情况下修改其内部的值。
Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为 内存泄漏,memory leak),但并非不可能。Rust 并不保证完全防止内存泄漏,这意味着内存泄漏在 Rust 中被认为是内存安全的。这一点可以通过 Rc<T> 和 RefCell<T> 看出 Rust 允许出现内存泄漏:创建引用循环的可能性是存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了 0,持有的数据也就永远不会被释放。
mpsc,multiple producer, single consumer
tx 和 rx 这两个缩写在许多领域里传统上分别表示 发送端(transmitter)和 接收端(receiver)
recv阻塞主进程,而try_recv不会
join 的主要目的是 “确保安全退出”,阻塞主进程直到子进程结束返回
尽管 counter 是不可变的,我们仍然可以获取其内部值的可变引用;这意味着 Mutex<T> 提供了内部可变性,就像 Cell 系列类型那样。使用 RefCell<T> 可以改变 Rc<T> 中内容,同样地,使用 Mutex<T> 我们也可以改变 Arc<T> 中的内容。 包和计数器循环应用----对应到无畏并发-----锁和原子计数器死锁
尽量不要在同一个作用域里写两次 .lock()。 情况1:如果是一把锁,第二次请求会永久阻塞导致死锁,原因是第二次请求导致的阻塞只有在开锁后才会结束。(被污染状态poisoned state,上一个线程panic会在下次lock时返回err)
情况2: 多把锁必须解决顺序问题
try_lock+主循环会导致非公平和活锁(如果没有合理的退避)
借用检查器(Borrow Checker)”只管空间,不管时间。
任何完全由 Send 的类型组成的类型也会自动被标记为 Send。几乎所有基本类型都是 Send 的,除了第二十章将会讨论的裸指针(raw pointer)。
换一种方式来说,对于任意类型 T,如果 &T(T 的不可变引用)实现了 Send 的话 T 就实现了 Sync(&T: Send => T: Sync),这意味着其引用就可以安全的发送到另一个线程。类似于 Send 的情况,基本类型都实现了 Sync,完全由实现了 Sync 的类型组成的类型也实现了 Sync。
&T,&mut T,&T是三种不同的类型。 send和sync是正交的。recell<T>只实现了send,可以搬迁但不能共用。
&mut str仅能做长度没变,UTF-8 合法性也没破坏的变化
Chars、slice::Iter,Lines,Bytes都是&str和String的迭代器结构体
fold是图灵完备的,针对序列()的方法都可以换fold实现,或者try_fold
Async、Pin 与状态模式#
todo: 当 Rust 遇到一个 async 关键字标记的代码块时,会将其编译为一个实现了 Future trait 的唯一的、匿名的数据类型。当 Rust 遇到一个被标记为 async 的函数时,会将其编译成一个函数体是异步代码块的非异步函数。异步函数的返回值类型是编译器为异步代码块所创建的匿名数据类型。
Cow<T> 用在“所有权可能发生改变,且你希望尽可能推迟或避免分配内存”的场景。Cow 机制中,克隆是 “全或无” (All-or-Nothing) 的。
async块相当于手动实现future特性的语法糖,内部包含一个过程,await等待并获取future结果,spawn_task执行一个future不返回结果,block_on启动运行时直到执行完毕,join可以合并多个future
rust的thread直接向操作系统申请线程,async相当于用户级线程 运行时就是程序所需的部分内核级线程和用户级线程之间的接口层,另外可以在运行时之外开启独立的内核线程
let (tx, mut rx) = trpl::channel();rust这里mut是因为异步接受可能会修改rx内部的队列指针,唤醒器列表等
一个 async 代码块中的代码会线性执行
对 self 进行类型注解,和给其他函数参数写类型注解类似,但有两个关键区别:
- 它告诉 Rust:要调用这个方法,self 必须是什么类型。
- 它不能随便写成任意类型。它必须是方法所实现类型本身、该类型的引用或智能指针,或者是一个包裹了该类型引用的 Pin。
基础类型默认是unpin,pin会穿透指针固定内存
在思考什么时候该用哪种方式时,可以先记住这些经验法则:
- 如果工作是非常适合并行化的,也就是典型的 CPU 密集型任务,比如有一大批数据而且每一部分都能单独处理,那么线程通常是更好的选择。
- 如果工作是高度并发的,也就是典型的 I/O 密集型任务,比如要同时处理来自很多不同来源、且到达间隔和频率都各不相同的消息,那么 async 通常是更好的选择。
状态模式(State Pattern):每一个状态对象负责其自身的行为,以及该状态何时应当转移至另一个状态。持有一个状态对象的值对于不同状态的行为以及何时状态转移毫不知情。在 Rust 中,通过 Box<dyn Trait>(动态分发)来实现。context对象与用户交互并维护一个状态对象指针,state trait封装状态行为,concretestate实现trait
优点在于,程序的业务需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。 状态模式的缺点:
- 因为状态实现了状态之间的转换,一些状态会相互联系。
- 会发现一些重复的逻.,可以用macros解决
类型状态模式 (Typestate Pattern):将context与state合并
if let模式匹配的缺点是不会检查是否穷尽
irefutable pattern适用于必须匹配的函数参数,for,let,单分支match(以及最后一个分支)。无法覆盖全部情况会报错 refutable pattern适用于条件匹配的,if let,while let,match(除最后一个分支的),let…else,只会陷入一种情况会警告
编译器不会尝试为包含匹配守卫的模式检查穷尽性。即使逻辑上已经穷尽也需要手动告诉编译器
使用 @ 可以在一个模式中同时测试和保存变量值。
Unsafe 与高级类型#
不安全的超能力(unsafe superpowers)。这些超能力包括:
- 解引用裸指针
- 调用不安全的函数或方法
- 访问(要保证单线程访问)或修改可变静态变量
- 实现不安全 trait
- 访问 union 的字段
和引用一样,裸指针是不可变或可变的,分别写作 const T 和mut T。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文中,不可变 意味着指针解引用之后不能直接赋值。 裸指针与引用和智能指针的区别在于
- 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针
- 不保证指向有效的内存
- 允许为空
- 不能实现任何自动清理功能
只能在unsafe块内解引用裸指针,但可以在其他地方创建通过&raw
struct和enum等的底层存储与定义无关,内存布局是由编译器自由重排的
从其他语言调用需要为fn指定extern关键字,调其他语言放入需要unsafe extern块
关联类型的本质是告诉编译器:这个 Trait 属于这个类型,且只能有一种实现。泛型的本质是允许你为一个类型提供多种不同的实现。
类型上的方法名和其需要实现的trait上的方法都可以同名,默认使用类型上的
continue 的值,panic!的返回值以及表达式loop的值是 !。! 类型永远不会有值.描述这种行为的正式方式是,类型为 ! 的表达式可以被强制转换为任意其他类型。
Rust 使用动态大小类型的方式:它们有一些额外的元信息来储存动态信息的大小。这引出了动态大小类型的黄金法则:必须将动态大小类型的值置于某种指针之后。
?Sized 这个 trait bound 表示 “T 可以是 Sized,也可以不是 Sized” 同时这个注解会覆盖泛型类型必须在编译时拥有固定大小的默认规则。所以T类型必须在某种指针之后
不同于闭包,fn 是一个类型而不是一个 trait,所以直接指定 fn 作为参数而不是声明一个带有 Fn 作为 trait bound 的泛型参数。
函数指针实现了所有三个闭包 trait(Fn、FnMut 和 FnOnce),所以总是可以在调用期望闭包的函数时传递函数指针作为参数。倾向于编写使用泛型和闭包 trait 的函数,这样它就能接受函数或闭包作为参数。
闭包表现为 trait,这意味着不能直接返回闭包。对于大部分需要返回 trait 的场景中,可以使用实现了期望返回的 trait 的具体类型来替代函数的返回值。但是这不能用于闭包,因为它们没有一个可返回的具体类型;例如,当闭包从其作用域捕获任何值时,就不允许使用函数指针 fn 作为返回类型。
宏与工程细节#
宏(Macro)指的是 Rust 中一系列的功能:使用 macro_rules! 的 声明宏(declarative macro)(类似于match匹配替换),和三种 过程宏(procedural macro)(类似于函数输入处理输出):
- 自定义 #[derive] 宏,用于在结构体和枚举上通过添加 derive 属性生成代码(只追加不修改)
- 类属性宏,定义可用于任意项的自定义属性(替换)
- 类函数宏,看起来像函数,但操作的是作为其参数传递的 token(无中生有)
在一个文件里调用宏 之前 必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。一个函数签名必须声明函数参数的数量和类型。相比之下,宏能够接收可变数量的参数
使用美元符号($)在宏系统中声明一个变量来包含匹配该模式的 Rust 代码.
$x:expr ,其匹配 Rust 的任意表达式,并将该表达式命名为 $x
紧随逗号之后的 说明该模式匹配零个或更多个 之前的任何模式。
创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。这么做出于一些复杂的技术原因(为过程宏剪一个独立的crate可以解决交叉编译场景将过程宏 crate 编译为宿主(Host)架构,将主 crate 编译为目标(Target)架构。另外需编译完宏才能调用).用syn库拆解(tokenstream->ast抽象语法树),quote拼装代码(ast->rust代码)
new和build的暗含意思,前者认为不会失败,后者有失败风险
在 while let(以及 if let 和 match)的条件表达式中创建的临时变量,其生命周期会被延长,直到与该条件关联的代码块(大括号 {},一次循环结束时)结束时才会丢弃。在一个普通的 let 语句中,等号右边产生的任何临时变量,会在这个语句的结尾(也就是分号 ; 所在的地方)立即被丢弃。
Mutex 结构体没有公有 unlock 方法,因为锁的所有权依赖 lock 方法返回的 LockResult<MutexGuard<T>> 中 MutexGuard<T> 的生命周期。这允许借用检查器在编译时确保绝不会在没有持有锁的情况下访问由 Mutex 守护的资源,不过如果没有认真的思考 MutexGuard<T> 的生命周期的话,也可能会导致比预期更久的持有锁。所以应使用let而非while let等来即使释放锁.
在生产环境中,处理 Drop 里的逻辑有一个黄金法则:在析构函数(Drop)中,永远只记录错误,绝不主动引发新的 Panic。否则会双重panic(此时会abort防止无限崩溃循环)
‘b: ‘a 泛型 ‘b 生命周期必须长于泛型 ‘a
带!表示对块内部生效或者写在块内部,不带一般是写在外部或对后面块生效