Rust规则书

看过一遍 Rust 的语法,如果其他语言也能按照这种思路去写,其实也可以很安全。

本文会简单梳理一下所有权、Trait、生命周期,以重新构建脑内代码思维模式。

所有权

所有权(Ownership)是 Rust 最独特的特征,它让 Rust 无需 GC 就可以保证内存安全。

访问堆内存中的数据要比访问栈内存中的数据慢,因为要使用指针寻址。对于现代的处理器来说,由于缓存的缘故,如果指令在内存中跳转的次数越少,那么速度就越快。

  • 如果数据存放的距离比较近,那么处理器的速度就会快一些(stack 上)
  • 如果数据存放的距离比较远,那么处理速度就会慢一些(heap 上)
    • 在 heap 上分配大量的空间也是需要时间的

所有权解决的问题:

  • 跟踪代码的哪些部分正在使用 heap 的哪些数据
  • 最小化 heap 上重复数据量
  • 清理 heap 上未使用的数据以避免空间不足

所有权规则

  • 每一个值都有一个变量,这个变量是该值的所有者
  • 每个值同时只能有一个所有者
  • 当所有者超出作用域时,该值被删除

默认的移动

1
2
let s1 = String::from("hello");
let s2 = s1;

这里展现一个移动语义,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
2
3
4
5
6
let r;
{
let x = 5;
r = &x; // error
}
foo(r);

Rust 编译器的借用检查器:比较作用域来判断所有的借用是否合法。

函数泛型生命周期

在函数内有多个借用时需要标注。

fn longest<'a>(x: &'a str, y: &'a str) ->&'a str {...}

生命周期标注语法

  • 生命周期的标注不会改变引用的生命周期长度(因为是调用处去匹配,而非函数自适应)
  • 当指定了泛型生命周期参数,函数可以接受带有任何生命周期的引用
  • 生命周期的标注:描述了多个引用的生命周期之间的关系,但不影响传入的生命周期

规则:

  • ' 开头
  • 通常全小写且非常短
  • 很多人使用 'a
  • 在引用的符号 &
  • 使用空格将标注和引用类型分开

几个例子:

  • &i32
  • &'a i32
  • &'a mut i32

多个引用生命周期的情况就跟泛型一样需要特化。只有使用者匹配了函数的生命周期条件,才能使用函数。
相同生命周期取最短。

结构体生命周期

在结构体内储存有引用时需要标注。

1
2
3
struct StrRef<'a> {
ref:&'a str,
}

生命周期的省略

  • 每个引用都有生命周期
  • 需要为使用生命周期的函数或 struct 指定生命周期参数

Rust v1.1 的时代,需要显式为所有引用添加生命周期。

在 Rust 引用分析中所编入的模式成为生命周期省略规则

  • 这些规则无需开发者来遵守
  • 是一些特殊情况,由编译器来考虑
  • 如果你的代码符合这些情况,那么就无需显式标注生命周期

生命周期省略规则不会提供完整的判断:

  • 如果应用规则后,引用的生命周期仍然模糊不清,则直接编译错误
  • 解决办法:添加生命周期标注,表明引用间的相互关系

定义:

  • 输入生命周期::函数/方法的参数
  • 输出生命周期:函数/方法的返回值

编译器使用 3 个规则在没有显式标注生命周期的情况下,来确定生命周期

  1. 每个引用类型的参数都有自己的生命周期
  2. 如果只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期
  3. 如果有多个输入生命周期参数,但其中一个时 &self&mut self(即成员函数),那么 self 的生命周期会被赋给所有的输出生命周期

如果编译器应用完 3 个规则之后,仍然有无法确定的生命周期引用,则直接编译错误

这些规则适用于 fnimpl 块。

静态生命周期

'static

Share