0
已有3015人阅读此文 - - Rust - luoyy -

所有权(Ownership)

所有权是 Rust 最独特的功能。它使 Rust 能够在不需要垃圾收集器(GC)的情况下保证内存安全

Rust的核心部分,也是最为重要的知识点,理论知识(倍感压力

什么是所有权?

所有权是一组规则,用于控制 Rust 程序如何管理内存。所有程序都必须管理它们在运行时使用计算机内存的方式。有些语言具有垃圾收集功能,会在程序运行时定期查找不再使用的内存;在其他语言中,程序员必须显式分配和释放内存。 Rust 使用第三种方法:通过所有权系统和编译器检查的一组规则来管理内存。如果违反任何规则,程序将无法编译。所有权的任何功能都不会减慢程序运行的速度。

跟踪代码的哪些部分正在使用堆上的哪些数据、最大限度地减少堆上的重复数据量以及清理堆上未使用的数据以免耗尽空间,这些都是所有权解决的问题。一旦理解了所有权,您就不需要经常考虑栈和堆,但是知道所有权的主要目的是管理堆数据可以帮助解释为什么它会这样工作。

栈(Stack)和堆(Heap)

许多编程语言并不要求您经常考虑堆栈和堆。但在像 Rust 这样的系统编程语言中,值是在堆栈上还是在堆上会影响语言的行为方式以及为什么必须做出某些决定。

栈(Stack)

由系统自动分配。例如在声明函数的一个局部变量int b,系统自动在栈中为b开辟空间。栈就像装数据的桶或箱子

栈和堆都是可供代码在运行时使用的内存部分,但它们的结构方式不同。栈按照获取值的顺序存储值,并按照相反的顺序删除值。这称为后进先出(Last In, First Out,LIFO)。想象一叠盘子:当你添加更多盘子时,你把它们放在一堆盘子的顶部,当你需要一个盘子时,你从上面拿一个。从中间或底部添加或移除板也不起作用!添加数据称为压入栈,删除数据称为从栈弹出。存储在栈上的所有数据都必须具有已知的固定大小。编译时大小未知或大小可能更改的数据必须存储在堆上。

压入栈比在堆上分配更快,因为分配器永远不需要搜索存储新数据的位置;该位置始终位于栈的顶部。相比之下,在堆上分配空间需要更多的工作,因为分配器必须首先找到足够大的空间来容纳数据,然后进行簿记,为下一次分配做准备。

当您的代码调用函数时,传递给函数的值(可能包括指向堆上数据的指针)和函数的局部变量被推送到栈上。当函数结束时,这些值将从栈中弹出。

堆(Heap)

需要程序员自己申请,并指明大小,在C中用malloc函数;在C++中用new运算符。堆像一棵倒过来的树

堆的组织性较差:当您将数据放入堆上时,您会请求一定量的空间。内存分配器在堆中找到一个足够大的空位,将其标记为正在使用,并返回一个指针,它是该位置的地址。这个过程称为在堆上分配,有时缩写为分配(将值推入栈不被视为分配)。因为指向堆的指针是已知的固定大小,所以您可以将指针存储在栈上,但是当您需要实际数据时,必须跟随指针。想象一下坐在一家餐馆里。当您进入时,请说出您的团体人数,然后主人会找到一张适合每个人的空桌子并带您前往那里。如果您的团队中有人迟到,他们可以询问您坐在哪里以便找到您。

访问堆中的数据比访问栈中的数据慢,因为您必须遵循指针才能到达那里。如果现代处理器在内存中的跳跃次数更少,那么它们的速度就会更快。继续类比,考虑餐厅的服务员从许多桌子上点菜。在转到下一张桌子之前先在一张桌子上获得所有订单是最有效的。从 A 表中获取订单,然后从 B 表中获取订单,然后再次从 A 中获取订单,然后再次从 B 中获取订单,这将是一个慢得多的过程。出于同样的原因,如果处理器处理靠近其他数据(因为它在栈上)而不是较远的数据(因为它可以在堆上)的数据,那么它可以更好地完成工作。

所有权规则

  • Rust 中的每个值都有一个所有者(Owner)。
  • 一次只能有一位所有者。
  • 当所有者超出范围时,该值将被删除。

变量范围

作用域和变量何时有效之间的关系与其他编程语言中的类似

fn main() {
    {
        // _s 在这里无效,它还没有声明
        let _s = "你好"; // _s 从此时开始有效
                         // 用 _s 做一些事情
    } // 这个范围现在结束了,_s 不再有效
}

String 类型

字符串是它们是不可变,String 类型可变。区别在于这两种类型如何处理内存。

use std::string::String;

fn main() {
    let mut s = String::from("hello");

    s.push_str(", world!"); // push_str() 将文字追加到字符串中

    println!("{}", s); // 这将打印“hello, world!”
}

对于该String类型,为了支持可变的、可增长的文本片段,我们需要在堆上分配一定量的内存(在编译时未知)来保存内容

  • 必须在运行时向内存分配器请求内存。
  • 当我们完成我们的String.

第一部分是由我们完成的:当我们调用 时String::from,它的实现会请求它所需的内存。这在编程语言中几乎是通用的。

第二部分则不同。在带有垃圾收集器(GC)的语言中,GC 会跟踪并清理不再使用的内存,我们不需要考虑它。在大多数没有 GC 的语言中(C、C++),我们有责任确定内存何时不再被使用,并调用代码来显式释放它,就像我们请求它一样。历史上,正确执行此操作一直是一个困难的编程问题。如果我们忘记了,我们就会浪费记忆。如果我们做得太早,我们就会得到一个无效的变量。如果我们这样做两次,这也是一个错误。我们需要将正好一allocate与正好一配对free。

Rust 采用了不同的路径:一旦拥有内存的变量超出范围,内存就会自动回收。

use std::string::String;

fn main() {
    {
        let _s = String::from("你好"); // _s 从此时开始有效

        // 用 _s 做一些事情
    } // 这个作用域现在结束了,并且 _s 不存在
      // 更长的有效时间
}

有一个特点,我们可以将String所需的内存返还给分配器:当s超出范围时。 当变量超出范围时,Rust 会为我们调用一个特殊的函数。 这个函数称为dropString的开发者可以在其中放置回收内存的代码。 Rust 会在右大括号处自动调用drop

注意:在 C++ 中,这种在项目生命周期结束时释放资源的模式有时称为资源获取即初始化 (RAII)。 如果您使用过 RAII 模式,那么您会熟悉 Rust 中的 drop 函数。

Move操作变量和数据

“将值 5 绑定到 x; 然后复制 x 中的值并将其绑定到 y。”

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

现在我们有两个变量 x 和 y,并且都等于 5。这确实是正在发生的情况,因为整数是具有已知固定大小的简单值,并且这两个 5 值被压入栈。

String 类型版本:

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

String由三部分组成,如左图所示:指向保存字符串内容的内存的指针、长度和容量。 这组数据存放在栈中。 右侧是堆上保存内容的内存。

图4-1: 内存中String的表示形式,其值hello绑定到s1

String长度(len)是当前使用的内容的内存量(以字节(bit)为单位)。容量(capacity)是String从分配器接收的内存总量(以字节(bit)为单位) 。长度和容量之间的差异很重要,目前不需要理解容量(capacity)这玩意,我们先忽略容量(capacity)(ε=ε=ε=┏(゜ロ゜;)┛

当我们分配s1给时s2String数据被复制,这意味着我们复制栈上的指针、长度和容量。我们不会复制指针引用的堆上的数据。换句话说,内存中的数据表示下图所示:

图4-2:变量s2在内存中的表示,它具有s1的指针、长度和容量的副本

如果堆上的数据很大,则操作s2 = s1在运行时性能方面可能会非常昂贵

图4-3: 如果 Rust 也复制了堆数据,s2 = s1 可能会做的另一种可能性

当变量超出范围时,Rust 会自动调用该drop函数并清理该变量的堆内存。但图 4-2 显示两个数据指针都指向同一位置。这是一个问题:当s2s1超出范围时,它们都会尝试释放相同的内存。这称为双重释放错误,是我们之前提到的内存安全错误之一。两次释放内存可能会导致内存损坏,从而可能导致安全漏洞。

为了确保内存安全,在let s2 = s1;行之后,Rust 认为s1不再有效。 因此,当s1超出范围时,Rust 不需要释放任何内容。 当您在创建s2之后尝试使用s1它不会工作:

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

你会得到这样的错误,因为 Rust 阻止你使用无效的引用:

$ cargo run
...
warning: unused variable: `s2`                                                                                                                                                            
 --> src\main.rs:3:9                                                                                                                                                                      
  |                                                                                                                                                                                       
3 |     let s2 = s1;                                                                                                                                                                      
  |         ^^ help: if this is intentional, prefix it with an underscore: `_s2`                                                                                                          
  |                                                                                                                                                                                       
  = note: `#[warn(unused_variables)]` on by default                                                                                                                                       

error[E0382]: borrow of moved value: `s1`                                                                                                                                                 
 --> src\main.rs:4:18                                                                                                                                                                     
  |                                                                                                                                                                                       
2 |     let s1 = String::from("hello");                                                                                                                                                   
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait                                                                                      
3 |     let s2 = s1;                                                                                                                                                                      
  |              -- value moved here                                                                                                                                                      
4 |     println!("s2 {s1}");                                                                                                                                                              
  |                  ^^^^ 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`.
...

如果您在使用其他语言时听说过浅拷贝深拷贝这两个术语,那么复制指针、长度和容量而不复制数据的概念可能听起来就像进行浅拷贝。 但由于 Rust 还会使第一个变量无效,因此它不被称为浅拷贝,而是被称为移动(Move)。 在这个例子中,我们会说s1移动(Move)s2中。 因此,实际情况如下图所示:

图4-4:s1失效后内存中的表示

至此上述问题内存释放问题就搞定啦! 只有s2有效,当它超出范围时,它会单独释放内存

由此暗示的一个设计选择是:Rust 永远不会自动创建数据的“深层”副本。因此,可以假定任何自动复制在运行时性能方面都是高效的。

Clone操作变量和数据

如果我们确实想要深度复制String的堆数据,而不仅仅是栈数据,我们可以使用一个名为clone的通用方法

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

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

这工作得很好,并显式地产生如图 4-3 所示的行为,其中堆数据确实被复制。

当您看到对clone的调用时,您就知道正在执行一些代码并且该代码可能在运行时很消耗性能

仅栈数据:复制

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

    println!("x = {}, y = {}", x, y);
}

我们没有调用clone,但x仍然有效并且没有移入y

我们没有调用clone,但x仍然有效,并且没有移动到y中。

原因是在编译时具有已知大小的整数等类型完全存储在堆栈中,因此可以快速创建实际值的副本。这意味着我们没有理由在创建变量y后阻止x有效。这里的深拷贝浅拷贝没有区别,所以调用clone不会做任何与通常的浅拷贝不同的事情,因此我们可以省略它。

Copy trait 的含义:

  • Rust 语言中有一个特殊的注解,称为 Copy trait,可以用于位于栈上存储的类型,例如整数。
  • 如果一个类型实现了 Copy trait,使用该类型的变量在赋值操作时不会发生所有权转移,而是会进行简单的复制操作。这意味着即使将变量赋值给另一个变量,原始变量仍然有效。

Copy trait 的限制:

  • Rust 不允许对实现了 Drop trait 的类型或其任何部分使用 Copy trait。Drop trait 用于指定当值超出作用域时需要进行的特殊操作。如果为实现了 Drop trait 的类型添加 Copy trait,编译器会报错。

实现 Copy trait 的类型:

  • 通常,简单的标量值类型可以实现 Copy trait,而需要分配内存或涉及资源的类型则不能实现 Copy trait。以下是一些实现 Copy trait 的类型:
    • 整数类型(i32、u8 等)
    • 浮点数类型(f32、f64)
    • 布尔类型(bool)
    • 字符类型(char)
    • 元组(tuple),只要其所有元素都实现了 Copy trait,(i32, i32)实现Copy trait,但(i32, String)不实现。
    • 数组(array),只要其元素类型实现了 Copy trait
fn main() {
    let x = 5; // x 存储在栈上,实现了 Copy trait
    let y = x; // 这里会进行简单复制,x 和 y 都有效

    let s = String::from("hello"); // String 类型存储在堆上,没有实现 Copy trait
    let t = s; // 这里会发生所有权转移,s 不再有效
}

所有权和函数

将值传递给函数的机制与将值分配给变量时的机制类似。将变量传递给函数将会移动或复制,就像赋值一样

fn main() {
    let s = String::from("hello"); // s 进入作用域
    takes_ownership(s); // s 的值移入函数,所以在这里s不再有效

    let x = 5; // x 进入作用域

    makes_copy(x); // x 将移动到函数中,但 i32 是 Copy,所以之后使用 x 还是有效

} // 这里 x 超出范围,然后我们看 s 变量。 因为 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 超出了范围。没有什么特别的事情发生。

如果我们在调用 takes_ownership 之后尝试使用 s,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,这也将其返回值移入 s3
} // 在这里,s3 超出了范围并被删除。 s2 被移动,所以没有任何反应。 s1 超出范围并被删除。

fn gives_ownership() -> String {
    // gives_ownership 会将其返回值移至调用它的函数中

    let some_string = String::from("yours"); // some_string 进入作用域

    some_string // 返回 some_string 并移出到调用函数
}

// 该函数接受一个 String 并返回一个 String
fn takes_and_gives_back(a_string: String) -> String {
    // a_string 进入作用域

    a_string // a_string 返回并移出到调用函数
}

变量的所有权每次都遵循相同的模式:为另一个变量赋值会移动该变量。 当包含堆上数据的变量超出范围时,该值将通过 drop 进行清理,除非数据的所有权已移至另一个变量。

下面内容有些不懂,后面再来看下。。。

虽然这可行,但获取所有权然后返回每个函数的所有权有点繁琐。如果我们想让一个函数使用一个值但不取得所有权怎么办?非常烦人的是,如果我们想再次使用它,除了我们可能想要返回的函数体产生的任何数据之外,我们传递的任何内容也需要被传回。

Rust 允许我们使用元组返回多个值:

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

    let (s2, len) = calculate_length(s1);

    // s1 已经被移动,无法使用

    println!("The length of '{}' is {}", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() 返回字符串的长度

    (s, length)
}

Rust 有一个使用值而不转移所有权的功能,称为引用(下一篇文章讲述,这里还是转移了所有权)


本小节内容有些多,总算花了一天时间看完了。

期待你一针见血的评论,Come on!

发表评论: