Rust学习笔记/所有权+引用+借用

renyuneyun 2018年08月06日(周一) 1 mins

Rust的一大特色(甚至在官方教程中 被称为the most unique feature )就是其借用及所有权机制。这两个机制由编译器进行强制,并且可以极大限度地保证变量安全(并行安全)。

所有权

前面说过,同C/C++一样,Rust中区分堆和栈(但不直接提及)。C/C++中语义上禁止从函数中返回指向局部变量的指针,是因为当离开函数时,栈会被回收,所以局部变量也会被回收,导致指针失效。

在C/C++中,该禁止并非语法上的禁止。而在Rust中,所有权机制将该概念显式表达,编译器会拒绝编译不满足所有权机制的代码。并且Rust设计者们希望通过所有权机制让程序员不需要考虑堆栈,只需要考虑所有权即可(应当是出于一致性以及或许对未来自动优化的考量?)。

Rust中的 寿元 机制也涉及堆栈的区别。 两者共同使得Rust不需要GC也可以保证内存安全。

所有权机制实际上就是三条规则:

  • 每个值具有唯一的所有者变量

  • 在同一时刻,所有者唯一

  • 所有者离开作用域时,值会被丢弃

    或者说(我个人的记法):每个值在每个时刻只有唯一的所有者,且有效性跟随所有者。

所有权本身不涉及特殊语法,只要记住上面三条规则即可。 官方教程所有权规则部分 中有例子可供参考。

切换变量时的所有权转移及保留

当将一个变量的值传给另一变量时(如赋值或函数调用时),有可能涉及所有权的变更。 Rust中与此相关的是move(移动语义)、copy(浅复制)和clone(深复制)。

和C++11中move的结果一样,Rust中将变量 a 传给另一变量 b 时,会将变量 a 中的值移动到变量 b 中去。这时,值的所有权从变量 a 转移到了变量 b 。该行为在赋值( a=b )或函数调用传参时会自动发生,除非其右值符合copy或clone的条件(见下)。

当一个类型实现了 Clone trait时,Rust会对其进行深复制。

当一个类型实现 Copy trait时,Rust会对其进行浅复制。注意实现 Copy trait需要该struct的所有成员也都实现了 Copy ,且该struct不能实现 Drop trait(以保证不会double freeing)。 官方文档的栈上数据复制部分 中对哪些类型实现了 Copy trait进行了总结,且对于基本类型来说: iXXfXXboolchar 均实现了 Copy (且元素均实现 Copy 的元组 () 也实现 Copy )。

引用

Rust中的引用类似C/C++中的指针,但使用 & 作为其标记(如 &i32 );在其上调用方法或访问成员不需要使用 -> 而照常使用 . 即可(事实上Rust中没有 -> 这个操作符)。

当然,也可以认为Rust中的引用类似于Java/Python中的引用,只是类型并非原数据类型而是原数据类型的引用类型,且在明确需要原数据类型时需要使用 * 来解引用。但这样需要额外再解释栈对其影响,反而更麻烦。

因为类似指针,所以可以对引用继续进行引用 && 。不同于C/C++,在使用一个引用类型时,Rust编译器会自动尝试加入足够量的解引用操作,以便操作可以正确执行(显然,该机制只是为方便而存在,不会涵盖更复杂的情况)。

由于Rust变量默认为不可变,在涉及引用时会有更细致的设定: 引用类型的变量 的可变性、 引用本身 的可变性和 该变量所引用变量 的可变性是相互独立的。

let mut a = 1; //a: i32
let b = &a; //b: &i32
*b = 0; //非法:因为b是不可变引用(虽然a本身可变)

let mut x = 2; //x: mut i32
let y = &mut x; //y: &mut i32
*y = -1; //合法:由于y是可变引用,所以可以修改

let i = 3; //i: i32
let j = 4; //j: i32
let mut k = &i; //mut k: &i32
k = &j; //合法:因为k本身可变(无关i的可变性)

let m = 5; //m: i32
let n = &mut m; //非法:不允许对不可变变量进行可变引用

借用

借用机制通过引用来实现,是对所有权机制的扩充。可以认为Rust中的借用和引用是同一机制的两个名称,分别强调不同的侧面:

  • 当谈及所有权时,称之为 借用
  • 当谈及数据类型时,称之为 引用

例如:函数参数为 引用 类型这一现象被称为 借用

另外,借用/引用还与 寿元 机制相关。

在引用的基本原理之外,借用添加了额外的约束:

  • 同一时刻只能有多个不可变借用或一个可变借用
  • 借用必须始终有效

这些约束均是为了保证变量和内存安全,避免数据竞争和访问无效内存。

上面引用的各个例子有意避免了违反借用的规则。如下的代码违反借用规则:

let mut a = 1; //x: mut i32
let b = &a; //y: &i32
let c = &a; //c: &i32
let d = &mut a; //非法

可以使用明确的作用域区分来解决该问题:

let mut a = 1; //x: mut i32
{
    let b = &a; //y: &i32
    let c = &a; //c: &i32
} //b和c均已离开作用域,借用消失
let d = &mut a; //合法

您可以在Hypothesis上的該群組內進行評論,或使用下面的Disqus評論。