Rust 的核心功能-所有权(ownership)
什么是所有权
Rust要核心功能之一是所有权(ownership)。虽然该功能很容量解释,但它对语言的其它部分有着深刻的影响。
所有运行的程序都必顺管理其使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时不断地寻找不再使用的内存;在另一些语言中,程序员必顺亲自分配和释放内存。Rust则选择了第三种的方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。
因为所有权对很多程序员来说都是一个新概念,需要一些时间来适应。好消息是随着你对Rust和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。持之以恒!
当我们理解了所有权,就将有一个坚实的基础来理解那些使Rust独特功能。
栈(Stack)与堆(Heap)
在很多语言中,我们并不需要经常考虑到stack和heap。不过在像Rust这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必顺做出这样的抉择。
跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过明白了所有权的存在就是为了管理堆数据,能够帮助解释为什么所有权要以这种方式工作。
所有权规则
首先,让我们看一下所有权的规则。当我们通举例说明时,请谨记这些规则:
- Rust中的每一个值都有一个被称为其 所有者 (owner)的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
绑定(Binding)
重要:首先必须强调下,准确地说Rust中并没有变量这一概念,而应该称为标识符,目标资源(内存,存放value)绑定到这个标识符:
{ let x:i32; //标识符x, 没有绑定什么资源 let y:i32 = 100;//标识符y, 绑定资源100 }
上面定义了一个i32类型的标识符x,如果直接println!,会收到一个error报错:
error[E0381]: borrow of possibly-uninitialized variable: `x` --> src/main.rs:6:25 | 6 | println!("x:{}",x); | ^ use of possibly-uninitialized `x`
这是因为Rust并不会像其它语言一样可以为变量默认初始化值,Rust明确规定变量的初始值必须由程序员自已决定。
正确的做法:
{ let x:i32; x = 100; println!("x:{}",x); }
其实,let关键字并不只是声明变量的意思,它还有一层特殊且重要的概念-绑定。通俗的讲,let关键字可以把一个标识符和一段内存区域做“绑定”,绑定后,这段内存就被这个标识符所拥有,这个标识符也成为这段内存的唯一所有者。所以,x=100发生了这么几个动作,首先在栈内存上分配一个i32的资源,并填充值100,随后,把这个资源与x做绑定,让x成为资源的所有者(Owner)。
作用域
像c语言一样,Rust通过{ }大括号定义作用域,在局部变量离开作用域后,变量随即会被销毁;但不同是,Rust会连同变量绑定的内存,不管是否为常量字符串,连同所有者变量一起被销毁释放。
移动语义(move)
先看如下代码:
fn main() { let a = String::from("xyz"); let b = a; println!("{}",a); }
编译后会得到如下的报错:
error[E0382]: borrow of moved value: `a` --> src/main.rs:5:19 | 3 | let a = String::from("xyz"); | - move occurs because `a` has type `String`, which does not implement the `Copy` trait 4 | let b = a; | - value moved here 5 | println!("{}",a); | ^ value borrowed here after move
错误的意思是在println中访问了moved的变量a。那为什么会有这种报错呢?具体含义是什么? 在Rust中,和“绑定”概念相辅相成的另一个机制就是“转移move所有权”,意思是,可以把资源的所有权(ownership)从一个绑定转移(move)成另一个绑定,这个操作同样通过let
关键字完成,和绑定不同的是,=
两边的左值和右值均为两个标识符:
语法:
let 标识符A = 标识符B; // 把“A”绑定资源的所有权转移给“B”
Move前后的内存示意如下:
Before move:
a <=> 内存(地址:A,内容:"xyz")
After move:
a
b <=> 内存(地址:A,内容:"xyz")
被move的变量不可以继续被使用。否则提示错误error: use of moved value
。这里有些人可能会疑问,move后,如果变量A和变量B离开作用域,所对应的内存会不会造成“Double Free”的问题?答案是否定的,Rust规定,只有资源的所有者销毁后才释放内存,而无论这个资源是否被多次move
,同一时刻只有一个owner
,所以该资源的内存也只会被free
一次。 通过这个机制,就保证了内存安全。是不是觉得很强大?
所有权与函数
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。
如下函数:
fn foo(self, arg2: Type2) -> ReturnType { //body }
主要存在3种self可以采用的形式:self, &mut self, &self。分别代表了Rust中所有权的三种主要形式:
- self, 值(value)
- &mut self, 可变引用(mutable reference)
- &self, 共享引用(shared reference)
一个值代表了真正的所有权。你可以对值做你想做的任何事:移动它,销毁它,改变它的内容,或者通过一个引用借出它。当你通过值传递东西时,它就被移动到了新的位置。这个新位置现在拥有了这个值,并且老位置不能再访问该值。因此,对于大部分函数我们都不想使用self--如果调用函数让我们无法再访问它,那还真是很糟糕啊。
一个可变引用代表了对你不拥有的一个值的临时唯一访问权。当你拥有一个可变引用时,你被允许做几乎任何想做的事,只要满足该引用过期时,被借用者仍然可以看见合法的值。这意味着你可以完全覆盖这个值。一个有用的特殊情况是把这个值和另外一个做交换——我们会经常使用这一技巧。唯一不能对&mut做的一件事是不加替换的将它的值移出。对于要对self加以修改的方法,&mut self是极好的。
一个共享引用代表对你不拥有的值的临时共享访问。由于访问是共享的,通常改变任何内容是不被允许的。可以把&想作把值丢到博物馆里用于展览。如果我们只想观察self的值,&是很好用的。
转移返回值的所有权
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过drop被清理掉,除非数据被移动为另一个变量所有。
在每一个函数中都获取所权并接着返回所有权有些罗嗦。如果我们想要函数使用一个值但不获取所有权该怎么办呢?如果我们还要接着使用它的话,每次都传进去再返回来就有点麻烦人了,除此之外,我们也可能想返回函数体中产生的一些数据。我们可以使用元组来反回多个值。
返回参数的所有权:
fn main() { let s1 = String::from("test"); let (s2, l) = calculateLen(s1); println!("s:{},l:{}",s2,l) } fn calculateLen(s: String) -> (String, usize) { let l = s.len(); (s, l) }
但是这未免有些形式主义,而且这种场景应该很常见。幸运的是,Rust对比提供了一个功能,叫做引用(references)。