Rust 所有权

rust通过所有权系统管理内存,该系统具有一组在编译时检查的规则。程序运行时,所有所有权功能都不会减慢其运行速度。

所有权规则

首先,让我们看一下所有权规则。在通过示例进行说明时,请牢记以下规则:

  • Rust中的每个值都有一个变量,称为其所有者。
  • 一次只能有一个所有者。
  • 当所有者超出范围时,该值将被删除。

可变范围

假设我们有一个看起来像这样的变量:

let s = "hello";

接下来我们来看s的有效范围

fn main() {
    {                      // s尚未声明,无效
        let s = "hello";   // s声明

                           // s有效
    }                      //括号后,s将不再有效。
}

换句话说,这里有两个重要的时间点:

  • 当s在作用域中,它是有效的。
  • 作用域结束,s将无效。

内存的分配与释放

内存分为栈和堆。在rust中栈内存的管理又其他编程语言类似,这里就不说明。
特殊的是堆中的内存。
在具有垃圾收集器(GC)的语言中,GC会跟踪并清理不再使用的内存,因此我们无需考虑它。如果没有GC,我们有责任确定何时不再使用内存,并调用代码以显式返回内存,就像我们请求内存一样。从历史上看,正确执行此操作一直是编程难题。如果我们忘记了,我们将浪费内存。如果我们做得太早,我们将有一个无效的变量。如果我们做两次,那也是一个错误。我们需要将正一allocate与正一配对free。

Rust采取了另一条路径:拥有内存的变量超出范围后,内存将自动回收。

fn main() {
    {
        let s = String::from("hello"); // 从现在开始,s是有效的

        //s有效
    }                                  // 现在这个范围结束了,所以s无效了
                                       
}

s超出范围时。当变量超出范围时,Rust为我们调用一个特殊函数。该函数称为drop。Rust会drop在右花括号处自动调用。

对堆:移动

堆中的数据交互,不同与栈中的数据交互。移动仅仅是对堆栈的操作。

我们先来看栈,栈中的数据交互是与其他语言类似的。

fn main() {
    let x = 5;
    let y = x;
}

我们可能会猜到它在做什么:“将值绑定5到x;然后复制其中的值x并将其绑定到y。” 现在,我们有两个变量x 和y,并且都相等5。确实发生了这种情况,因为整数是具有已知固定大小的简单值(标量),并且这两个5值被压入堆栈。

我们再来看堆,String是一种典型的在堆中分配的内存。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
}

这里s1被初始化为一个String,内容为hello,后将s1的值移动到s2,此时s2=“hello”,而s1失效。这与其他语言不相同,但对内存安全有极大的意义。

移动原理

现在我们来看其底层原理:
在这里插入图片描述
上图中,可见String由三个部分组成

  • 指向右图(string内容)的指针。
  • 字符串长度。
  • 数据结构的容量。

左图这部分数据存在栈中,右图的数据存在堆中。
在将String数据复制时,rust只复制了栈上的指针(还没结束,不是结论)。如下图:
在这里插入图片描述

之前说过,当变量超出范围后,rust会自动调用drop函数清楚该变量的堆内存,但是上图明显有两个指向同一个个堆内存的指针,显而易见,该堆内存将会被释放两次,我们将这种错误成为双重释放错误。
为了确保内存安全,rust使移动后的s1失效,而不是尝试复制分配的内存,因此rust不需要在s1超出范围后释放任何内容。

接下来给你展示的使调用失效变量的错误:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

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

错误:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 | 
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership`.

To learn more, run the command again with --verbose.

克隆

在有些必要的时候,我们确实想深入复制,拥有两个副本,那我们可以使用clone。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1 = {}, s2 = {}", s1, s2);
}

底层则会如下图:
在这里插入图片描述

对栈:复制

移动是对堆数据的操作,复制是对栈数据的操作。

fn main() {
    let x = 5;
    let y = x;
}

x与y都将等于5且有效。

以下数据类型都是指向复制操作:

  • 所有整数类型,例如u32。
  • 布尔类型,bool值true和false。
  • 所有浮点类型,例如f64。
  • 字符类型char。
  • 元组(如果它们仅包含also的类型)Copy。例如, (i32, i32)是Copy,但(i32, String)不是。

关于函数的所有权问题

将值传递给函数作为参数,就像赋值一样,也会触发复制与移动操作。
以下代码将展示变量的作用域。

fn main() {
    let s = String::from("hello");  //s进入范围

    takes_ownership(s);             // s的值传入函数
                                    // 应为s是堆中内存,所以s被移动到了函数中的参数中。s无效了。
                                    
    let x = 5;                      // x进入范围

    makes_copy(x);                  // 因为x是栈中内存,所以x被复制到函数的参数中
                                    //x依旧有效
                                    
} //x超出范围,x从此无效。


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超出范围,从此无效。

返回值和范围

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

fn main() {
    let s1 = gives_ownership();         //函数的返回值移动至s1

    let s2 = String::from("hello");     // s2 进入范围

    let s3 = takes_and_gives_back(s2);  // s2移动至函数参数,s2失效,s3接受函数返回值,进入范围
} // s1与s3超出范围,调用drop,释放内存。

fn gives_ownership() -> String {             
    let some_string = String::from("hello"); // some_string 进入范围

    some_string                              // some_string作为返回值移动到调用的函数。
}


fn takes_and_gives_back(a_string: String) -> String { // a_string 进入范围

    a_string  // a_string 作为返回值移动到调用的函数。
}

在默认情况下rust变量是不可变的。这样可以使代码更加安全。让我们探讨一下Rust如何以及为什么鼓励您支持变量不可变,以及为什么有时您可以选择可变变量。 声明变量 通过let关键字声明变量,可以不声明变量类型,交由编 ...