Skip to content

所有权

所有权(系统)是 Rust 最为与众不同的特性,对语言的其他部分有着深刻含义。它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全,因此理解 Rust 中所有权如何工作是十分重要的。

什么是所有权规则?

Rust中所有权的规则是:

1.Rust 中的每一个值都有一个被称为其 所有者owner)的变量。

意味着所有的值都有对应的所有者,并且只有一个所有者。

rust
let s = String::from("rust");// rust字符串的所有者是变量s

2.值在任一时刻有且只有一个所有者。

也就是说如果执行了变量所有权改变的话,例如执行了:s2 = s1 的话,为防止二次释放,会对数据的所有权进行转移。

rust
let s1 = String::from("hello");
let s2 = s1; 
println!("{}, world!", s1); // 错误!
// 发生了所有权转移,s1失去了所有权,不可能出现对同一个值的两个所有者s1和s2

3.当值的所有者离开作用域,这个值将被丢弃;

意味着当该变量跳出有效作用域的话,该变量将自动释放,也就是不可再使用。

rust
{  
    // 在声明以前,变量 s 无效  
    let s = "rust";  
    // 这里是变量 s 的可用范围
}
// 变量范围已经结束,变量 s 无效,不可再用

编译器会对这些规则进行检查,如果违反了任何规则,那么程序都不能编译通过。此外,在运行时,所有权系统的任何功能都不会减缓程序。

变量作用域

作用域是一个变量在程序中有效的范围, 假如有这样一个变量:

rust
let s = "hello"

变量 s 绑定到了一个字符串字面值,该字符串字面值是硬编码到程序代码中的。s 变量从声明的点开始直到当前作用域的结束都是有效的:

rust
{                      // s 在这里无效,它尚未声明
    let s = "hello";   // 从此处起,s 是有效的

    // 使用 s
}                      // 此作用域已结束,s不再有效

简而言之,s 从创建伊始就开始有效,然后有效期持续到它离开作用域为止,可以看出,就作用域来说,Rust 语言跟其他编程语言没有区别。

变量与数据交互的方式

所有权与Move

先来看一段代码:

rust
let x = 5;
let y = x;

代码背后的逻辑很简单, 将 5 绑定到变量 x;接着拷贝 x 的值赋给 y,最终 xy 都等于 5,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存。

可能有同学会有疑问:这种拷贝不消耗性能吗?实际上,这种栈上的数据足够简单,而且拷贝非常非常快,只需要复制一个整数大小(i32,4 个字节)的内存即可,因此在这种情况下,拷贝的速度远比在堆上创建内存来得快的多。实际上,我们讲到的 Rust 基本类型都是通过自动拷贝的方式来赋值的,就像上面代码一样。

然后再来看一段代码:

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

之前也提到了,对于基本类型,Rust 会自动拷贝,但是 String 不是基本类型,而且是存储在堆上的,因此不能自动拷贝。

实际上, String 类型是一个复杂类型,由存储在栈中的堆指针字符串长度字符串容量共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存,容量是堆内存分配空间的大小,长度是目前已经使用的大小。

总之 String 类型指向了一个堆上的空间,这里存储着它的真实数据, 下面对上面代码中的 let s2 = s1 分成两种情况讨论:

  1. 拷贝 String 和存储在堆上的字节数组 如果该语句是拷贝所有数据(深拷贝),那么无论是 String 本身还是底层的堆上数据,都会被全部拷贝,这对于性能而言会造成非常大的影响
  2. 只拷贝 String 本身 这样的拷贝非常快,因为在 64 位机器上就拷贝了8字节的指针8字节的长度8字节的容量,总计 24 字节,但是带来了新的问题,还记得我们之前提到的所有权规则吧?其中有一条就是:一个值只允许有一个所有者,而现在这个值(堆上的真实字符串数据)有了两个所有者:s1s2

好吧,就假定一个值可以拥有两个所有者,会发生什么呢?

当变量离开作用域后,Rust 会自动调用 drop 函数并清理变量的堆内存。不过由于两个 String 变量指向了同一位置。这就有了一个问题:当 s1s2 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free) 的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。

因此,Rust 这样解决问题:s1 赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2s1 在被赋予 s2 后就马上失效了

再来看看,在所有权转移后再来使用旧的所有者,会发生什么:

rust
// 错误示例
let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

由于 Rust 禁止你使用无效的引用,你会看到以下的错误:

error[E0382]: use of moved value: `s1`
 --> src/main.rs:5:28
  |
3 |     let s2 = s1;
  |         -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value used here after move
  |
  = note: move occurs because `s1` has type `std::string::String`, which does
  not implement the `Copy` trait

如果你在其他语言中听说过术语 浅拷贝(shallow copy)深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据听起来就像浅拷贝,但是又因为 Rust 同时使第一个变量 s1 无效了,因此这个操作被称为 移动(move),而不是浅拷贝。上面的例子可以解读为 s1移动到了 s2 中。那么具体发生了什么,用一张图简单说明:

s1_moved_s2

这样就解决了我们之前的问题,s1 不再指向任何数据,只有 s2 是有效的,当 s2 离开作用域,它就会释放内存。 相信此刻,你应该明白了,为什么 Rust 称呼 let a = b变量绑定了吧?

再来看一段代码:

rust
fn main() {
    let x: &str = "hello, world";
    let y = x;
    println!("{},{}",x,y);
}

这段代码和之前的 String 有一个本质上的区别:在 String 的例子中 s1 持有了通过String::from("hello") 创建的值的所有权,而这个例子中,x 只是引用了存储在二进制中的字符串 "hello, world",并没有持有所有权。

因此 let y = x 中,仅仅是对该引用进行了拷贝,此时 yx 都引用了同一个字符串。

所有权与Copy

在上面的例子中,我们如果执行的话,会提示数据的所有权已经发生了移动,发生编译错误。如下所示:

rust
// 错误示例
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{} {}", s1, s2);
}

如果把s1换成整型的话,会是什么样的?如下所示:

rust
fn main() {
    let s1: i8 = 5;
    let s2 = s1;
    println!("{} {}", s1, s2);
}

编译是通过的。原因在于,本例子中,整型这样的在编译时已知大小的类型被整个存储在栈上,执行let s2 = s1;的话会发生拷贝,这时候,s1s2 是完全独立的数据。换句话说,这里没有深浅拷贝的区别。

这里稍微要超前讲一点关于 trait 的知识。Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上(Trait章节将会详细讲解 trait)。如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。

这种不拥有其他资源并且可以按位复制的类型称为复制类型。它们由Rust自己实现了 Copy Trait。目前所有基本类型,如整数、浮点数和字符都是Copy类型。默认情况下,struct/enum 不是Copy类型,但你可以派生 Copy trait,更多详细介绍可以学习Trait章节。

那么哪些类型实现了 Copy trait 呢?你可以查看给定类型的文档来确认,不过作为一个通用的规则,任何一组简单标量值的组合都可以实现 Copy,任何不需要分配内存或某种形式资源的类型都可以实现 Copy 。如下是一些编译器自动实现的 Copy 的类型:

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 truefalse
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

如果回头看第一个例子的报错信息,会提示 s1 没有实现 Copy trait,也就是String类型没有实现 Copy trait:

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:23
  |
3 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
4 |     let s2 = s1;
  |              -- value moved here
5 |     println!("{} {}", s1, s2);
  |                       ^^ value borrowed here after move

注:Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用 Copy 注解,将会出现一个编译时错误。要学习如何为你的类型添加 Copy 注解以实现该 trait,请阅读附录 C 中的 “可派生的 trait”

有关Move和Copy的深入理解请参考Rust中级知识点-理解Move和Copy

所有权与Clone

Rust中,每一个值都有对应的所有权变量。一旦发生下方的代码就意味着所有权发生了转移:

rust
let s1 = String::from("hello");   // 数据存放在堆上
let s2 = s1;  // s1将字符串的所有权转移到了s2。这个时候s1失去所有权被清除。

利用图来解释上面的代码的执行过程:左边的数据存放在栈上,内容包括数据存放的地址,数据的长度、容量;右边的数据存放在堆上。

point

s1的值赋值到s2,栈上的数据被复制了,并没有复制堆上的数据。此外,为了避免在释放的时候造成二次释放,复制以后Rust认为s1已经失去了数据的所有权,会被释放掉。在Rust中编译器会自己调用drop函数。所以转移之后,s1无效,s2有效。

copy

复制栈上的数据指针就是一个浅拷贝,Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动 的复制可以被认为对运行时性能影响较小。

如果在实际的使用中,确实需要对数据进行复制操作,Rust也提供了clone函数。

rust
let s1 = String::from("hello");   // 数据存放在堆上let s2 = s1.clone();  // 这个时候s1和s2都是有效的。

类似于下图的方式:

clone

这里必须再说明一下,我们在下面的代码中会发现是可以运行的:

rust
let a = 1;let b = a;println!("a = {}, b = {}", a, b);

这似乎与我们上面所讲的矛盾?其实并不是的,Rust对于像整型这样的在编译时已知大小的类型被整个存储在栈上,Rust对于类似于整型这样的类型提供了Copy trait。如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。

那么这些类型都有哪些?

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 truefalse
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用 Copy 注解,将会出现一个编译时错误。

要学习如何为你的类型添加 Copy 注解以实现该 trait,请阅读的 “可派生的 trait”

所有权与函数

在函数中,将某一个变量作为参数输入后,该变量同样失去所有权。

rust
fn main() {    
    let s = String::from("hello");  // s 进入作用域    
    takes_ownership(s);             // s 的值移动到函数里,    							
    							 // s 没有Copy属性,所以之后失效    
    let x = 5;                      // x 进入作用域    
    makes_copy(x);                  // x 应该移动函数里,                                   
                                    // 但 i32 是 Copy 的,                                    
                                    // 所以在后面可继续使用 x
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走。
fn takes_ownership(some_string: String) { // some_string 进入作用域    
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。  
  // 占用的内存被释放
fn makes_copy(some_integer: i32) { // some_integer 进入作用域    
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。

所有权与返回值

返回值也可以返回所有权。

rust
fn main() {    
    let s1 = gives_ownership();         // gives_ownership 将返回值转移给 s1    
    let s2 = String::from("hello");     // s2 进入作用域    
    let s3 = takes_and_gives_back(s2);  // s2 被移动到takes_and_gives_back中,s2失效,它也将返回值移给 s3
} // 这里,s1, s3 移出作用域并被丢弃。

fn gives_ownership() -> String {             // gives_ownership 会将返回值所有权移动给调用它的函数    
    let some_string = String::from("yours");     
    some_string
}

fn takes_and_gives_back(a_string: String) -> String {    
    a_string  // 返回 a_string 并移出给调用的函数
}

所有权与借用

在实际的场景中,有这样的情况:我们利用函数对输入参数进行某种运算后得到一个结果,并且原数据我们还将继续使用。这个时候我们用到了借用与引用。

借用不拥有数据的所有权,只是借过来使用一下,采用的方式是引用。

rust
fn main() {    
    let s1 = String::from("hello");    
    let len = calculate_length(&s1);    
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {   // 采用引用的方式对数据进行借用    
    s.len()
}

通过引用来借用的行为类似于下方的示意图:

brown

因为并不拥有这个值,所以当引用停止使用时,它所指向的值也不会被丢弃。同样,直接借来的值也并不能修改。如下会报错:

rust
fn main() {    
    let s = String::from("hello");    
    change(&s);
}

fn change(some_string: &String) {    
    some_string.push_str(", world");  // 试图修改借来的值
}

如果想要修改借来的值,只需要将引用改变为可变引用,这样是可以修改借来的值,相当于由所有权的拥有者赋权给了借用者修改值的权利:

rust
fn main() {    
    let mut s = String::from("hello");  // 首先声明变量是可变的    
    change(&mut s);  // 然后,这里指明是可变引用
}

fn change(some_string: &mut String) {  // 参数的声明也要指出是可变的    
    some_string.push_str(", world");
}

可变借用的限定

Rust规定,在同一个时间只能有数据的一个可变引用。这个很容易理解,在多线程的情况下,当同时存在多个数据的可变引用的话,那么如果同时修改的话,会造成数据竞争;单线程情况下不会发生数据竞争。

数据竞争,它可由这三个行为造成:

  • 两个或更多指针同时访问同一数据。
  • 至少有一个指针被用来写入数据。
  • 没有同步数据访问的机制。

此外,Rust也不能在拥有不可变引用的同时拥有可变引用。这会造成数据在使用的过程中被其他借用者修改的情况。但是可以在不可变引用的生命周期结束后建立可变引用。这不算同时拥有。

rust
fn main() {    
    let mut s = String::from("hello");    
    let r1 = &s; // 没问题    
    let r2 = &s; // 没问题    
    println!("{} and {}", r1, r2);    // 此位置之后 r1 和 r2 不再使用    
    let r3 = &mut s; // 没问题    
    println!("{}", r3);
}