看过一遍 Rust 的语法,如果其他语言也能按照这种思路去写,其实也可以很安全。
本文会简单梳理一下所有权、Trait、生命周期,以重新构建脑内代码思维模式。
所有权
所有权(Ownership)是 Rust 最独特的特征,它让 Rust 无需 GC 就可以保证内存安全。
访问堆内存中的数据要比访问栈内存中的数据慢,因为要使用指针寻址。对于现代的处理器来说,由于缓存的缘故,如果指令在内存中跳转的次数越少,那么速度就越快。
- 如果数据存放的距离比较近,那么处理器的速度就会快一些(stack 上)
- 如果数据存放的距离比较远,那么处理速度就会慢一些(heap 上)
- 在 heap 上分配大量的空间也是需要时间的
所有权解决的问题:
- 跟踪代码的哪些部分正在使用 heap 的哪些数据
- 最小化 heap 上重复数据量
- 清理 heap 上未使用的数据以避免空间不足
所有权规则
- 每一个值都有一个变量,这个变量是该值的所有者
- 每个值同时只能有一个所有者
- 当所有者超出作用域时,该值被删除
默认的移动
1 | let s1 = String::from("hello"); |
这里展现一个移动语义,s1
在移动给 s2
后便失效了。
从设计角度看,其实是 String
类型没有实现 Copy
Trait。
Copy
Trait 用于给栈对象定义默认拷贝行为(隐式)Clone
/Drop
Trait 用于给堆对象定义深拷贝行为
如果一个类型实现了 Copy
,那么旧的变量在赋值后仍然可用。
如果一个类型或者该类型的一部分实现了 Drop
,那么 Rust 不会允许让它再去实现 Copy
了。
任何简单标量的组合类型都可以是 Copy 的
任何需要分配内存或某种资源的都不是 Copy 的
一些拥有 Copy trait 的类型:
- 所有整数、浮点类型
u32
f64
- bool
- char
- tuple 成员都是 Copy 的
(i32,i32)
所有权与函数
在语义上,将值传递给函数和把值赋给变量是类似的:将值传递给函数将发生移动或复制。
引用和借用
&
符号就表示引用:允许你引用某些值而不取得其所有权(默认不可变)。
把引用作为函数参数这个行为叫做借用。
可变引用有一个重要的限制:在一个作用域内,对某一块数据,只能有一个可变的引用。
这样做的好处是可以在编译时防止数据竞争
以下三种行为会发生数据竞争:
- 两个或多个指针同时访问同一个数据
- 至少有一个指针用于写入数据
- 没有使用任何机制来同步对数据的访问
可以通过创建新的作用域,来允许非同时的创建多个可变引用
另一个限制:可变引用与不可变引用不可共存,但多个不可变引用可以共存。
悬空引用 Dangling References
一个指针引用了内存中的某个地址,但是这块内存可能已经被释放或分配给其他人使用
在 Rust 里,编译器可以保证引用永远都不是悬空引用。
- 如果你引用了某些数据,编译器将保证在引用离开作用域之前数据都有效。
切片 Slice
特殊的引用。特殊的,字符串切片被定义为 &str
。
Trait
泛型:提高代码泛用的能力
泛型是具体类型或其他属性的抽象代替:你编写的代码不是最终的代码,而是一种模板,里面有一些”占位符”。编译器在编译期将占位符替换为具体的类型。
例如 fn largest<T>(list: &[T]) -> T { ... }
。
为 struct 或 enum 实现方法的时候,可以在定义中使用泛型
- 把 T 放在 impl 关键字后,表示在类型 T 上实现方法
impl<T> Point<T>
- 针对具体类型则不需要加上 T
impl Point<i32>
泛型无运行时性能消耗。执行单态化后,仅增加指令数。
带约束实现 Trait:默认给所有匹配的泛型,添加指定 Trait。
impl <T: Display> ToString for T {...}
Lifetime
Rust 的每个引用都有自己的生命周期。
生命周期:引用保持有效的作用域。
大多数情况:生命周期是隐式的、可被推断的
当引用的生命周期可能以不同的方式互相关联的时候,需要手动标注生命周期。
生命周期主要目标:避免指针悬空。
错误例:
1 | let r; |
Rust 编译器的借用检查器:比较作用域来判断所有的借用是否合法。
函数泛型生命周期
在函数内有多个借用时需要标注。
fn longest<'a>(x: &'a str, y: &'a str) ->&'a str {...}
生命周期标注语法
- 生命周期的标注不会改变引用的生命周期长度(因为是调用处去匹配,而非函数自适应)
- 当指定了泛型生命周期参数,函数可以接受带有任何生命周期的引用
- 生命周期的标注:描述了多个引用的生命周期之间的关系,但不影响传入的生命周期
规则:
- 以
'
开头 - 通常全小写且非常短
- 很多人使用
'a
- 在引用的符号
&
后 - 使用空格将标注和引用类型分开
几个例子:
&i32
&'a i32
&'a mut i32
多个引用生命周期的情况就跟泛型一样需要特化。只有使用者匹配了函数的生命周期条件,才能使用函数。
相同生命周期取最短。
结构体生命周期
在结构体内储存有引用时需要标注。
1 | struct StrRef<'a> { |
生命周期的省略
- 每个引用都有生命周期
- 需要为使用生命周期的函数或 struct 指定生命周期参数
Rust v1.1
的时代,需要显式为所有引用添加生命周期。
在 Rust 引用分析中所编入的模式成为生命周期省略规则
- 这些规则无需开发者来遵守
- 是一些特殊情况,由编译器来考虑
- 如果你的代码符合这些情况,那么就无需显式标注生命周期
生命周期省略规则不会提供完整的判断:
- 如果应用规则后,引用的生命周期仍然模糊不清,则直接编译错误
- 解决办法:添加生命周期标注,表明引用间的相互关系
定义:
- 输入生命周期::函数/方法的参数
- 输出生命周期:函数/方法的返回值
编译器使用 3 个规则在没有显式标注生命周期的情况下,来确定生命周期
- 每个引用类型的参数都有自己的生命周期
- 如果只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期
- 如果有多个输入生命周期参数,但其中一个时
&self
或&mut self
(即成员函数),那么self
的生命周期会被赋给所有的输出生命周期
如果编译器应用完 3 个规则之后,仍然有无法确定的生命周期引用,则直接编译错误
这些规则适用于 fn
与 impl
块。
静态生命周期
'static