Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

引用、借用与逃逸

Riddle 没有生命周期参数。引用的合法性由借用检查负责,引用的存储位置由逃逸分析决定。

共享引用与可变引用

&T 是共享引用,可以同时存在多个;&mut T 是可变引用,存活期间不能有别的引用指向同一位置:

fun main() {
    let mut value = 42;
    let shared = &value;
    println!("{}", *shared);

    let exclusive = &mut value;
    *exclusive = 43;
    println!("{}", value);
}

同时存在的引用类型不匹配时按四种情况报错:

现在要已经有了错误码
&mut&E0300
&&mutE0301
&mut&mutE0302
&&允许

借用在持有它的绑定的最后一次使用之后结束,不是等到作用域结束:

fun main() {
    let mut value = 1;
    let shared = &value;
    println!("{}", *shared);
    value = 2;
    println!("{}", value);
}

这是 NLL(非词法生命周期)的近似实现:判定依据是源码位置,不构建控制流活跃性;循环体里的冲突用不动点求解,收敛后只报告一次。

借用期间不能赋值或移动

E0303 是有活借用时赋值,E0304 是有活借用时移动:

fun main() {
    let mut value = 1;
    let shared = &value;
    value = 2; // E0303
    println!("{}", *shared);
}

不相交的结构体字段可以分别可变借用。已知常量下标可以分别借用,动态下标按重叠处理。

引用穿过函数

返回值里的引用保留它的来源:返回值派生自哪个参数,调用结束后那个参数就还在借用状态。

struct Holder { value: i32 }

fun pick(holder: &Holder) -> &i32 {
    &holder.value
}

fun main() {
    let holder = Holder { value: 7 };
    let borrowed = pick(&holder);
    println!("{}", *borrowed);
}

编译器为每个函数计算「参数怎么流到返回值」的摘要,泛型 bound 派发时取所有实现的并集;拿不到具体被调者(extern 声明、函数指针、dyn 调用)时按最保守的情况处理。

通过共享引用写入

&T 不允许写入指向的值:

struct Sample { n: i32 }

fun bump(sample: &Sample) {
    sample.n = 1; // E0309
}

字段可以声明为 mut,表示该字段在共享引用下也可写:

struct Counter { mut hits: i32 }

impl Counter {
    fun bump(&self) {
        self.hits += 1;
    }
}

fun main() {
    let counter = Counter { hits: 0 };
    counter.bump();
    counter.bump();
    println!("{}", counter.hits); // 2
}

可写性是字段自己的属性,与到达它的路径无关:经 &self、经普通字段、经另一个 mut 字段到达都一样可写。枚举变体字段不能声明 mut,写了报 E0014,因为变体字段只能通过模式绑定访问。

经共享引用取 &mut 到 mut 字段只在那一次调用内部有效:可以当接收者或实参,不能绑定到名字、不能返回。

struct Log { mut items: Vector<&str> }

impl Log {
    fun add(&self, item: &str) {
        self.items.push(item);
    }
}

fun main() {
    let log = Log { items: Vector::new() };
    log.add("first");
    println!("{}", log.items.len());
}

把 &mut 绑定到名字(let target = &mut counter.hits;)报 E0309,因为这个借用会活过那次调用。

调用别的函数时,被调方可能写哪些 mut 字段由每个函数的摘要决定。从某个 mut 字段派生的借用不能跨越可能写它的调用,否则报 E0300。dyn 调用和函数指针没有摘要,按「可能写该参数的所有 mut 字段」保守处理。

Drop 与引用

借用了实现 Drop 的局部值时,引用不能活得比拥有者久,否则报 E0306:

struct Resource { name: &str }

impl std::ops::Drop for Resource {
    fun drop(&mut self) { println!("drop {}", self.name); }
}

fun name_of(resource: &Resource) -> &str {
    resource.name
}

fun main() {
    let resource = Resource { name: "file" };
    println!("{}", name_of(&resource));
}

这个程序是合法的:resource 活到函数结束。把 name_of(&resource) 的结果存进比 resource 活得更久的绑定时才会报错。

逃逸与存储位置

逃逸分析判断值会不会在当前栈帧之外继续被使用:会逃逸的提到 GC 堆,不逃逸的留在栈上。所有权和借用规则在两种情况下完全相同。

默认开启 GC(Clue.toml 的 [runtime] gc = true)。关掉 GC 就没有堆可以提升,引用逃出栈存储报 E0310:

fun make() -> &i32 {
    let value = 1;
    &value // GC 关闭时报 E0310
}

原始指针不受借用检查约束,这是 unsafe 之外的绕行方式,代价是编译器不再保证它指向有效存储。