Rust学习总结之所有权(一)

发布于:2025-02-13 ⋅ 阅读:(9) ⋅ 点赞:(0)

        不管是计算机的哪种语言,都有内存的管理方式。主流有两种,一是以C为代表的由开发者来决定申请和释放内存,二是以Python为代表的通过语言本身的垃圾回收机制来自动管理内存。Rust开辟了第三种方式,通过所有权系统管理内存。

        Rust所有权规则,下面的例子都将围绕下面的3条所有权规则来展开讲解

1 Rust 中的每一个值都有一个被称为其 所有者owner)的变量
2 值在任一时刻有且只有一个所有者
3 当所有者(变量)离开作用域,这个值将被丢弃

一:变量作用域

fn main() {
    let s = String::from("hello"); // 从此处起,s 开始有效

    println!("{}", s);             // 使用 s
}                                  // 此作用域已结束,
                                   // s 不再有效

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

第二部分实现起来就各有区别了。在有 垃圾回收garbage collectorGC)的语言中, GC 记录并清除不再使用的内存,而我们并不需要关心它。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们的责任了,跟请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要精确地为一个 allocate 配对一个 free

Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。当 s 离开作用域的时候。当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop

二:移动

通过两个例子来对比,

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

运行结果:

 

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

运行结果:

同样的变量赋值,为什么会产生上面的差异

前者,将 5 绑定到 x;接着生成一个值 x 的拷贝并绑定到 y。现在有了两个变量,x 和 y,都等于 5。这也正是事实上发生了的,因为整数是有已知固定大小的简单值,所以这两个 5 被放入了栈中

后者的行为会不会和前面x,y的赋值逻辑是一致的呢?从结果上看明细是有差异的

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

长度表示 String 的内容当前使用了多少字节的内存。容量是 String 从分配器总共获取了多少字节的内存。当我们将 s1 赋值给 s2String 的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据。

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

为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。在 let s2 = s1 之后,Rust 认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西,如果这时候再使用s1就会报错,所以上面的图要修改一下

三:克隆

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

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

运行结果:

克隆就是复制一份数据出来,在堆上有两处hello的数据

在eg2中,x和y并没有调用clone,但是x也没移动到y,原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 clone 并不会与通常的浅拷贝有什么不同,我们可以不用管它

Rust 有一个叫做 Copy trait 的特殊标注,可以用在类似整型这样的存储在栈上的类型上(第 10 章详细讲解 trait)。如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用 Copy 标注,将会出现一个编译时错误。要学习如何为你的类型添加 Copy 标注以实现该 trait,请阅读附录 C 中的 “可派生的 trait”

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

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

四:所有权与函数

fn main() {
  let s = String::from("hello");  // s 进入作用域

  takes_ownership(s);             // s 的值移动到函数里 ...
                                  // ... 所以到这里不再有效

  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 移出作用域。不会有特殊操作

经常写C语言的在这里可能会非常不适应,变量传给函数,在C语言中是值传递,变量还是可以继续使用的,但是在Rust中这里和C有很大的区别。s作为函数传参进takes_ownership后s就转移失效了,但是如果函数后我想继续使用s,怎么办?可以通过返回值将s转移出来

五:返回值和作用域

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 并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

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

但是如果我们每次都是通过返回值来获取入参的所有权,这样就太蠢了,在Rust中可以通过引用来避免该问题