Rust中的所有权系统

前言

所有权系统是Rust语言最为基础,最为独特且最最重要的特性。它让Rust无须内存垃圾回收机制就可以保障内存安全和运行效率。因此,正确地了解所有权概念及其在Rust中的实现方式,对于所有Rust开发者(爱好者)来讲都是十分重要的。

栈内存和堆内存

在开始学习所有权之前让我们先对栈和堆进行简单的了解。

  1. 栈内存

栈内存存储的数据大小在编译时就已知且固定,通常我们把数据放入栈中叫作入栈,从栈中取出数据叫作出栈。数据放入栈的顺序是先放入栈中的数据最后被取出,最后入栈的数据最先被取出,即先入后出。Rust中所有的基础类型都可以存储在栈上,它们的大小都是固定的。

2. 堆内存

堆内存存储的数据大小是在编译时未知或者将来可能发生变化,即只有在程序运行时才能确定其数据的大小。例如,我们在使用字符串类型时在编译期间其大小是未知的,且运行时大小可能发生变化,因此我们会把其存储在堆上。当我们希望将数据放入堆中时,就可以向系统请求特定大小的空间。操作系统会根据你的请求在堆中找到一块足够大的可用空间,将它标记为已使用,并把指向这片空间地址的指针返回给我们。这一过程就是所谓的堆分配,它也常常被简称为分配。

3. 栈和堆的区别

什么是所有权

Rust中的所有权是笼统的讲就是一套管理内存的方案,根据这套内存管理方案使得Rust无须内存垃圾回收机制就可以保障内存安全和运行效率。

所有权规则

  1. Rust中的每一个值都有一个与之对应的变量作为它的所有者,及一个值对应一个变量。
  2. 在同一时间内,值的所有者且仅有一个。
  3. 当所有者离开其作用域时,它持有的值就会被销毁。

变量作用域

作用域是指一个对象在程序中有效的作用范围。例如,下面的代码例子

fn main() { // 变量s未声明,这个时候s是不可见的
    let mut s = String::from("hello"); // 变量s在这被声明,从这开始s是可见的
    
    s.push_str(" world");
    
    println!("the s is : {}", s);
} // 到这s的生命已到了尽头,从这开始s就被销毁变无效了

String类型

Rust提供了两种类型的字符串,字面量字符串和String类型的字符串,这两种的区别如下:

内存与分配

fn main() {
    let s = String::from("string type"); // s开始有效
    
    println!("s = {}", s);
} // 在这里s就失效了,rust会调用drop函数进行回收

数据交互方式(移动)

Rust中的多个变量可以采用一种独特的方式与同一数据进行交互(移动)。

  1. 对于普通类型

对于普通类型,例如,整数是已知固定大小的简单值,下面例子中x,y两个值都会同时被推入当前的栈中。

fn main() {
    let x = 4;
    let y = x;
    println!("x = {} y = {}", x, y);
}

2. 对于String类型

String的内存布局,它实际上是由3部分组成,一个指向存放字符串内容的指针(pointer)、一个长度(length)及一个容量(capacity),内容部分的数据是存储在了栈中。如下图所示:

3. String与String间的数据移动

fn main() {
    let s1 = String::from("Rust");
    let s2 = s1;
    
    println!("the s1 = {} s2 = {}", s1, s2);
}

上面代码会编译不通过,错误提示如下所示:

⣿
Standard Error

   Compiling playground v0.0.1 (/playground)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:37
  |
2 |     let s1 = String::from("Rust");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     
5 |     println!("the s1 = {} s2 = {}", s1, s2);
  |                                     ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` due to previous error

首先我们把s1的赋值给s2,这相当于String的数据被复制了一份,即s2在栈上复制了一份指针,长度和容量,实际上并没有复制堆上的数据内容,此时s1和s2的关系如下图所示:

我们知道当变量离开它所在的作用域时,Rust就会调用drop函数,并将变量所使用的堆内存做释放操作。上图中字符串s1和s2指向了一块相同的内存地址,这就导致了一个问题就是当字符串s1和s2离开它们的作用域时,它们就会尝试释放相同的内存,这就造成了内存的二次释放。为了保证内存的安全性,Rust并没有尝试复制被分配的内存,它的作法就是让字符串s1失效,这样当s1离开作用域是就不用释放任何东西了,它只需释放s2即可。因为我们将s1赋值给s2,导致s1失效,当我们再次使用s1进行输出时就出现了上面的错误信息。

如果我们不让s1失效,那么怎么办呢?其实Rust也提供另外一种独特的方式与同一数据进行交互方式,那就是克隆(clone)

fn main() {
    let s1 = String::from("Rust");
    let s2 = s1.clone();
    
    println!("the s1 = {} s2 = {}", s1, s2);
}

正常输出内容如下所示:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 2.04s
     Running `target/debug/playground`

Standard Output

the s1 = Rust s2 = Rust

这时s1和s2的内存布局如下所示:

这么看有点像其他编程语言中的浅拷贝和深拷贝,但是在Rust中永远不会自动地创建数据的深度拷贝。

不知道你发现没有,对于上面的代码例子中普通数据类型中的数据进行交互,即使没有发生数据的移动,x在赋值给y之后x仍然是有效的,这是因为类似于整型这样的类型是可以在编译时确定自己的大小,并且能够将自己的数据完整地存储在栈中,对于这些类型值的复制操作永远都是非常快速的。这也同样意味着,在创建变量y后,我们没有任何理由去阻止变量x继续保持有效。换句话说,对于这些类型而言,深度拷贝与浅度拷贝没有任何区别,调用clone并不会与直接的浅度拷贝有任何行为上的区别。因此,我们完全不需要在类似的场景中考虑上面的问题。还有一个原因就是Rust提供了一个名为Copy的trait,它可以用于整数这类完全存储在栈上的数据类型。一旦某种类型拥有了Copy这种trait,那么它的变量就可以在赋值给其他变量之后保持可用性。如果一种类型本身或这种类型的任意成员实现了Drop这种trait,那么Rust就不允许其实现Copy这种trait。尝试给某个需要在离开作用域时执行特殊指令的类型实现Copy这种trait会导致编译时错误。

4. 实现了Copy Trait的类型,有如下这么几种

所有权与函数

在语义上,将值传递给函数和把值赋值给变量是类似的,将值传递给函数要么会发生复制,要么会发生移动。

让我们用例子来说明一下把,具体代码如下所示:

fn main() {
    let x = 23;
    let s = String::from("Rust");
    
    print_value(x);
    
    println!("x == {}", x); // 因为x是i32类型是实现了Copy trait的所以不影响输出
    
    print_string(s); // s被移动到了print_string这个函数里面,s不再有效
    
    println!("the s = {}", s); // 因为s发生了移动所以不能在使用s了
    
}

fn print_value(x: u32) {
    println!("the x = {}", x);
}

fn print_string(s: String) {
    println!("the s = {}", s);
}

上面代码例子不能成功的编译运行,错误输出信息如下所示:

   Compiling playground v0.0.1 (/playground)
error[E0382]: borrow of moved value: `s`
  --> src/main.rs:11:28
   |
3  |     let s = String::from("Rust");
   |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
...
9  |     print_string(s);
   |                  - value moved here
10 |     
11 |     println!("the s = {}", s);
   |                            ^ value borrowed here after move
   |
note: consider changing this parameter type in function `print_string` to borrow instead if owning the value isn't necessary
  --> src/main.rs:19:20
   |
19 | fn print_string(s: String) {
   |    ------------    ^^^^^^ this parameter takes ownership of the value
   |    |
   |    in this function
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
   |
9  |     print_string(s.clone());
   |                   ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` due to previous error

返回值与作用域

函数在返回值的过程中也会发生所有权的转移。我们还是继续使用例子来说明一下吧

fn main() {
    let s1 = String::from("rust lang");
    
    let s2 = get_string(); // 函数返回值被转移到s2变量中
    
    let s3 = get_str_from_parameter(s1); // 变量s1被转移到函数当中,
                                        // 并当作函数的返回值移动到了变量s3当中
    
    println!("the s3 = {}", s2);
    println!("the s2 = {}", s2);
}

fn get_string() -> String {
    let s = String::from("rust");
    s
}

fn get_str_from_parameter(ss: String) -> String {
    ss
}

变量所有权的转移总是遵循相同的模式,即将一个值赋值给另一个变量时就会发生所有权的转移。当一个持有堆数据的变量离开作用域时,它的数据就会被drop函数清理回收,除非这些数据的所有权移动到了另一个变量上。

借用和引用

  1. 引用

什么是引用?在Rust当中引用就是指允许你引用某些值而不取得其所有权的操作,&符号表示应用。用一个例子来说明一下,代码如下所示:

fn main() {
    let s = String::from("hello rust");
    
    let l = return_str_len(&s); // 将s的引用传入到函数当中,s的所有权未发生转移,函数只是引用了s但是不会拥有它
    
    println!("the str = {} length = {}", s, l);
}

fn return_str_len(s: &String) -> usize { // 返回字符串长度函数,参数为字符串的引用
    s.len()
}

引用的内存布局如下所示:

2. 借用

把引用作为函数参数的行为称之为借用,即借用的东西是要还的。默认情况下修改借用的值是不可行的,因为引用跟不可变的变量一样,默认也是不可改变的。

如果想要修改引用的东西,我们可以像定义可变变量一样,在定以使用时使用&mut来表示其可被修改的,例如下面的例子,代码如下所示:

fn main() {
    let mut s = String::from("hello rust"); // 声明可变字符串s
    
    let l = return_str_len(&mut s); // 使用时加上&mut表示可变引用
    
    println!("the str = {} length = {}", s, l);
}

fn return_str_len(s: &mut String) -> usize { // 同样的定义函数参数是也要加上&mut表示接收可变的引用
    s.push_str(" language!");
    s.len()
}

3. 可变引用

fn main() {
    let mut s = String::from("hi rust");
    
    let s1 = &mut s;
    let s2 = &mut s;
    
    println!("the s1 = {}", s1);
    println!("the s2 = {}", s2);
}

错误信息如下所示:

   Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let s1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let s2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |     
7 |     println!("the s1 = {}", s1);
  |                             -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `playground` due to previous error

fn main() {
    let mut s = String::from("hi rust");
    
    let s1 = &s;
    let s2 = &s;
    
    let s3 = &mut s;
    
    println!("the s1 = {}", s1);
    println!("the s2 = {}", s2);
    println!("the s3 = {}", s3);
}

错误输出信息如下所示:

   Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:7:14
  |
4 |     let s1 = &s;
  |              -- immutable borrow occurs here
...
7 |     let s3 = &mut s;
  |              ^^^^^^ mutable borrow occurs here
8 |     
9 |     println!("the s1 = {}", s1);
  |                             -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground` due to previous error

4. 悬垂引用

悬垂引用是指一个指针引用了内存中的某个地址,而这块内存地址可能已经释放并分配给其他变量使用了。在Rust中,编译器可以保证引用永远都不是悬垂,即如果我们引用了某些数据,编译器将保证在引用离开作用域之前数据不会离开作用域。具体例子如下所示:

fn main(){
    let s = return_ref();
}

fn return_ref() -> &String {
    let s = String::from("rust language");
    &s
}

错误输出信息如下所示:

   Compiling playground v0.0.1 (/playground)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:20
  |
5 | fn return_ref() -> &String {
  |                    ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn return_ref() -> &'static String {
  |                     +++++++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `playground` due to previous error

切片

切片是Rust提供的一种不持有所有权的数据类型。

  1. 字符串切片

字符串切片就是指向String对象中某个连续部分的引用,具体使用例子如下所示:

fn main(){
    let s = String::from("rust language");
    
    let rs = &s[0..5]; // 从索引为0到索引为5但不包括5
    let lg = &s[5..]; // 从索引5开始到字符串结尾
    println!("the rs = {}", rs);
    println!("the ls = {}", lg);
}

输出结果如下所示:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/playground`

Standard Output

the rs = rust 
the ls = language

字符串操作图解,如下所示:

2. 字符串切片就是字符串字面量

字符串切片就是字符串字面量,其类型其实就是&str,我们在使用字符串切片作为函数参数时尽量使用&str来表示,而不是使用&String,因为这样代码表示比较清晰。

fn get_str(s: &str) -> u32 {
 // 具体功能
}

小结

所有权,借用和切片的概念是Rust可以在编译时保证内存安全的关键所在。像其他系统级语言一样,Rust语言给予了程序员完善的内存使用控制能力。今天的内容就这样,拜拜!

展开阅读全文

页面更新:2024-05-04

标签:所有权   字符串   变量   函数   例子   大小   内存   作用   类型   数据   系统

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top