当前位置: 首页 > 文档资料 > RustPrimer 中文版 >

可变性、所有权、租借和生命期 - 所有权

优质
小牛编辑
134浏览
2023-12-01

所有权(Ownership)

在进入正题之前,大家先回忆下一般的编程语言知识。
对于一般的编程语言,通常会先声明一个变量,然后初始化它。
例如在C语言中:

  1. int* foo() {
  2. int a; // 变量a的作用域开始
  3. a = 100;
  4. char *c = "xyz"; // 变量c的作用域开始
  5. return &a;
  6. } // 变量a和c的作用域结束

尽管可以编译通过,但这是一段非常糟糕的代码,现实中我相信大家都不会这么去写。变量a和c都是局部变量,函数结束后将局部变量a的地址返回,但局部变量a存在栈中,在离开作用域后,局部变量所申请的栈上内存都会被系统回收,从而造成了Dangling Pointer的问题。这是一个非常典型的内存安全问题。很多编程语言都存在类似这样的内存安全问题。再来看变量cc的值是常量字符串,存储于常量区,可能这个函数我们只调用了一次,我们可能不再想使用这个字符串,但xyz只有当整个程序结束后系统才能回收这片内存,这点让程序员是不是也很无奈?

备注:对于xyz,可根据实际情况,通过堆的方式,手动管理(申请和释放)内存。

所以,内存安全和内存管理通常是程序员眼中的两大头疼问题。令人兴奋的是,Rust却不再让你担心内存安全问题,也不用再操心内存管理的麻烦,那Rust是如何做到这一点的?请往下看。

绑定(Binding)

重要:首先必须强调下,准确地说Rust中并没有变量这一概念,而应该称为标识符,目标资源(内存,存放value)绑定到这个标识符

  1. {
  2. let x: i32; // 标识符x, 没有绑定任何资源
  3. let y: i32 = 100; // 标识符y,绑定资源100
  4. }

好了,我们继续看下以下一段Rust代码:

  1. {
  2. let a: i32;
  3. println!("{}", a);
  4. }

上面定义了一个i32类型的标识符a,如果你直接println!,你会收到一个error报错:

error: use of possibly uninitialized variable: a

这是因为Rust并不会像其他语言一样可以为变量默认初始化值,Rust明确规定变量的初始值必须由程序员自己决定

正确的做法:

  1. {
  2. let a: i32;
  3. a = 100; //必须初始化a
  4. println!("{}", a);
  5. }

其实,let关键字并不只是声明变量的意思,它还有一层特殊且重要的概念-绑定。通俗的讲,let关键字可以把一个标识符和一段内存区域做“绑定”,绑定后,这段内存就被这个标识符所拥有,这个标识符也成为这段内存的唯一所有者
所以,a = 100发生了这么几个动作,首先在栈内存上分配一个i32的资源,并填充值100,随后,把这个资源与a做绑定,让a成为资源的所有者(Owner)。

作用域

像C语言一样,Rust通过{}大括号定义作用域:

  1. {
  2. {
  3. let a: i32 = 100;
  4. }
  5. println!("{}", a);
  6. }

编译后会得到如下error错误:

b.rs:3:20: 3:21 error: unresolved name a [E0425]
b.rs:3 println!(“{}”, a);

像C语言一样,在局部变量离开作用域后,变量随即会被销毁;但不同是,Rust会连同变量绑定的内存,不管是否为常量字符串,连同所有者变量一起被销毁释放。所以上面的例子,a销毁后再次访问a就会提示无法找到变量a的错误。这些所有的一切都是在编译过程中完成的。

移动语义(move)

先看如下代码:

  1. {
  2. let a: String = String::from("xyz");
  3. let b = a;
  4. println!("{}", a);
  5. }

编译后会得到如下的报错:

c.rs:4:20: 4:21 error: use of moved value: a [E0382]
c.rs:4 println!(“{}”, a);

错误的意思是在println中访问了被moved的变量a。那为什么会有这种报错呢?具体含义是什么?
在Rust中,和“绑定”概念相辅相成的另一个机制就是“转移move所有权”,意思是,可以把资源的所有权(ownership)从一个绑定转移(move)成另一个绑定,这个操作同样通过let关键字完成,和绑定不同的是,=两边的左值和右值均为两个标识符:

  1. 语法:
  2. let 标识符A = 标识符B; // 把“B”绑定资源的所有权转移给“A”

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一次
通过这个机制,就保证了内存安全。是不是觉得很强大?

Copy特性

有读者仿照“move”小节中的例子写了下面一个例子,然后说“a被move后是可以访问的”:

  1. let a: i32 = 100;
  2. let b = a;
  3. println!("{}", a);

编译确实可以通过,输出为100。这是为什么呢,是不是跟move小节里的结论相悖了?
其实不然,这其实是根据变量类型是否实现Copy特性决定的。对于实现Copy特性的变量,在move时会拷贝资源到新内存区域,并把新内存区域的资源bindingb

Before move:
a <=> 内存(地址:A,内容:100)
After move:
a <=> 内存(地址:A,内容:100)
b <=> 内存(地址:B,内容:100)

move前后的ab对应资源内存的地址不同。

在Rust中,基本数据类型(Primitive Types)均实现了Copy特性,包括i8, i16, i32, i64, usize, u8, u16, u32, u64, f32, f64, (), bool, char等等。其他支持Copy的数据类型可以参考官方文档的Copy章节。

浅拷贝与深拷贝

前面例子中move String和i32用法的差异,其实和很多面向对象编程语言中“浅拷贝”和“深拷贝”的区别类似。对于基本数据类型来说,“深拷贝”和“浅拷贝“产生的效果相同。对于引用对象类型来说,”浅拷贝“更像仅仅拷贝了对象的内存地址。
如果我们想实现对String的”深拷贝“怎么办? 可以直接调用String的Clone特性实现对内存的值拷贝而不是简单的地址拷贝。

  1. {
  2. let a: String = String::from("xyz");
  3. let b = a.clone(); // <-注意此处的clone
  4. println!("{}", a);
  5. }

这个时候可以编译通过,并且成功打印”xyz”。

clone后的效果等同如下:

Before move:
a <=> 内存(地址:A,内容:”xyz”)
After move:
a <=> 内存(地址:A,内容:”xyz”)
b <=> 内存(地址:B,内容:”xyz”)
注意,然后a和b对应的资源值相同,但是内存地址并不一样。

可变性

通过上面,我们已经已经了解了变量声明、值绑定、以及移动move语义等等相关知识,但是还没有进行过修改变量值这么简单的操作,在其他语言中看似简单到不值得一提的事却在Rust中暗藏玄机。
按照其他编程语言思维,修改一个变量的值:

  1. let a: i32 = 100;
  2. a = 200;

很抱歉,这么简单的操作依然还会报错:

error: re-assignment of immutable variable a [E0384]

:3 a = 200;

不能对不可变绑定赋值。如果要修改值,必须用关键字mut声明绑定为可变的:

  1. let mut a: i32 = 100; // 通过关键字mut声明a是可变的
  2. a = 200;

想到“不可变”我们第一时间想到了const常量,但不可变绑定与const常量是完全不同的两种概念;首先,“不可变”准确地应该称为“不可变绑定”,是用来约束绑定行为的,“不可变绑定”后不能通过原“所有者”更改资源内容。

例如:

  1. let a = vec![1, 2, 3]; //不可变绑定, a <=> 内存区域A(1,2,3)
  2. let mut a = a; //可变绑定, a <=> 内存区域A(1,2,3), 注意此a已非上句a,只是名字一样而已
  3. a.push(4);
  4. println!("{:?}", a); //打印:[1, 2, 3, 4]

“可变绑定”后,目标内存还是同一块,只不过,可以通过新绑定的a去修改这片内存了。

  1. let mut a: &str = "abc"; //可变绑定, a <=> 内存区域A("abc")
  2. a = "xyz"; //绑定到另一内存区域, a <=> 内存区域B("xyz")
  3. println!("{:?}", a); //打印:"xyz"

上面这种情况不要混淆了,a = "xyz"表示a绑定目标资源发生了变化。

其实,Rust中也有const常量,常量不存在“绑定”之说,和其他语言的常量含义相同:

  1. const PI:f32 = 3.14;

可变性的目的就是严格区分绑定的可变性,以便编译器可以更好的优化,也提高了内存安全性。

高级Copy特性

在前面的小节有简单了解Copy特性,接下来我们来深入了解下这个特性。
Copy特性定义在标准库std::marker::Copy中:

  1. pub trait Copy: Clone { }

一旦一种类型实现了Copy特性,这就意味着这种类型可以通过的简单的位(bits)拷贝实现拷贝。从前面知识我们知道“绑定”存在move语义(所有权转移),但是,一旦这种类型实现了Copy特性,会先拷贝内容到新内存区域,然后把新内存区域和这个标识符做绑定。

哪些情况下我们自定义的类型(如某个Struct等)可以实现Copy特性?
只要这种类型的属性类型都实现了Copy特性,那么这个类型就可以实现Copy特性。
例如:

  1. struct Foo { //可实现Copy特性
  2. a: i32,
  3. b: bool,
  4. }
  5. struct Bar { //不可实现Copy特性
  6. l: Vec<i32>,
  7. }

因为Foo的属性ab的类型i32bool均实现了Copy特性,所以Foo也是可以实现Copy特性的。但对于Bar来说,它的属性lVec<T>类型,这种类型并没有实现Copy特性,所以Bar也是无法实现Copy特性的。

那么我们如何来实现Copy特性呢?
有两种方式可以实现。

  1. 通过derive让Rust编译器自动实现

    1. #[derive(Copy, Clone)]
    2. struct Foo {
    3. a: i32,
    4. b: bool,
    5. }

    编译器会自动检查Foo的所有属性是否实现了Copy特性,一旦检查通过,便会为Foo自动实现Copy特性。

  2. 手动实现CloneCopy trait

    1. #[derive(Debug)]
    2. struct Foo {
    3. a: i32,
    4. b: bool,
    5. }
    6. impl Copy for Foo {}
    7. impl Clone for Foo {
    8. fn clone(&self) -> Foo {
    9. Foo{a: self.a, b: self.b}
    10. }
    11. }
    12. fn main() {
    13. let x = Foo{ a: 100, b: true};
    14. let mut y = x;
    15. y.b = false;
    16. println!("{:?}", x); //打印:Foo { a: 100, b: true }
    17. println!("{:?}", y); //打印:Foo { a: 100, b: false }
    18. }

    从结果我们发现let mut y = x后,x并没有因为所有权move而出现不可访问错误。
    因为Foo继承了Copy特性和Clone特性,所以例子中我们实现了这两个特性。

高级move

我们从前面的小节了解到,let绑定会发生所有权转移的情况,但ownership转移却因为资源类型是否实现Copy特性而行为不同:

  1. let x: T = something;
  2. let y = x;
  • 类型T没有实现Copy特性:x所有权转移到y
  • 类型T实现了Copy特性:拷贝x所绑定的资源新资源,并把新资源的所有权绑定给yx依然拥有原资源的所有权。
move关键字

move关键字常用在闭包中,强制闭包获取所有权。

例子1:

  1. fn main() {
  2. let x: i32 = 100;
  3. let some_closure = move |i: i32| i + x;
  4. let y = some_closure(2);
  5. println!("x={}, y={}", x, y);
  6. }

结果: x=100, y=102

注意: 例子1是比较特别的,使不使用 move 对结果都没什么影响,因为x绑定的资源是i32类型,属于 primitive type,实现了 Copy trait,所以在闭包使用 move 的时候,是先 copy 了x ,在 move 的时候是 move 了这份 clone 的 x,所以后面的 println!引用 x 的时候没有报错。

例子2:

  1. fn main() {
  2. let mut x: String = String::from("abc");
  3. let mut some_closure = move |c: char| x.push(c);
  4. let y = some_closure('d');
  5. println!("x={:?}", x);
  6. }

报错:
error: use of moved value: x [E0382]

:5 println!(“x={:?}”, x);

这是因为move关键字,会把闭包中的外部变量的所有权move到包体内,发生了所有权转移的问题,所以println访问x会如上错误。如果我们去掉println就可以编译通过。

那么,如果我们想在包体外依然访问x,即x不失去所有权,怎么办?

  1. fn main() {
  2. let mut x: String = String::from("abc");
  3. {
  4. let mut some_closure = |c: char| x.push(c);
  5. some_closure('d');
  6. }
  7. println!("x={:?}", x); //成功打印:x="abcd"
  8. }

我们只是去掉了move,去掉move后,包体内就会对x进行了可变借用,而不是“剥夺”x的所有权,细心的同学还注意到我们在前后还加了{}大括号作用域,是为了作用域结束后让可变借用失效,这样println才可以成功访问并打印我们期待的内容。

关于“Borrowing借用”知识我们会在下一个大节中详细讲解。