Rust的所有权(一)

作为一个Ruby开发者,我所知道的关于内存分配的所有内容都是由一些称为垃圾收集的进程处理的,这是Aaron Patterson的问题,而不是我的问题。

因此,当我阅读Rust Book并看到Rust的一个明确的特性是垃圾收集的替代品时,我有点担心。

处理内存管理的责任是不是要摊派到我的身上?

显然,对于其他系统编程语言,如C语言,处理内存分配是一件大事,如果做得不好,可能会产生重大后果。
随着所有其他新事物的学习,我觉得事情开始堆积起来。

栈和堆

不,它不是一个时髦的服装品牌,栈和堆是在运行时管理内存的方法。

首先,我们有。栈被认为是快速的,因为它根据顺序存储和访问数据。最后一个放在(推入)栈上的数据是从栈中取出(弹出)的第一个数据。这被称为LIFO,即后进先出,这意味着我们只需要在释放内存的时候跟踪堆栈顶部的位置。

栈很快的另一个原因是堆栈所需的空间量在编译时是已知的。 这意味着我们可以在将任何内容存储到其中之前分配一个固定大小的内存部分。

例如,如果你知道有4来你家吃饭聚会,你就可以预先安排好他们的座位,准备好食物,并在他们到来之前练习如何叫他们的名字。这样非常高效!

接下来,我们有另一个选择,。当你不能预先知道来你家吃饭聚会的确切人数,你可以使用堆。随着越来越多的人来到你的晚餐聚会,使用堆意味着找到额外的椅子并发出姓名标签。

当运行时需要存储未知大小的数据时,计算机搜索堆上的内存,标记它,并返回指针,指针指向内存中的那个位置。这叫做内存分配。然后,你可以将指针压入栈,但是,当你想要检索实际数据时,你需要跟随指针返回到堆。

当我继续深入研究栈和堆,感觉到似乎在堆中管理数据是很困难的。例如,你需要确保在使用完内存后,允许计算机重新分配内存中的位置。但是,如果代码的某个部分释放了内存中的一个位置,而代码的另一部分仍然有指针指向这个位置,那么就会发生一些奇怪的事情。

跟踪代码的哪些部分正在使用堆上的哪些数据,最小化堆上重复数据的数量,清理堆上未使用的数据,以免耗尽空间,这些都是所有权要解决的问题。
-Rust Book

所有权和作用域

Rust的所有权有三个规则:

  • Rust中的每个值都有一个称为其所有者的变量。
  • 任何时刻只能有一个所有者。
  • 当所有者超出作用域时,该值将被删除。

这种所有权魔力的最简单说明是变量的作用域:

变量作用域

一旦当前函数范围结束,由 } 表示,变量hello超出作用域,并被删除。

“嗯,呸!”这就是我第一次看到这个时的想法。这在大多数其他编程语言中是一样的。这就是我所知道的“局部作用域变量”的行为。”

如果这就是所有权的全部知识,我真就有点不知所措了。

然而,当我们开始传递值并从使用存储在栈中的字符串字面值切换到使用存储在堆中的字符串类型时,事情变得更加有趣。

复制 vs 移动

我们可以看到,当使用字符串字面量的时候,Rust把hello的值复制到hello1里,这个符合我们的预期。但是当使用String类型的时候,Rust反而是移动值。当我们尝试获取已经被移走的值的时候,Rust会报错:error[E0382]: use of moved value: ‘hello’。

看起来在使用字符串字面量时,Rust会将该一个变量的值复制到另一个变量中,但是当我们使用String类型时,它会移动该值。

要找到哪些类型实现了copy trait,“你可以查看文档,但是一般来说,任何一组简单标量值都可以被复制,任何需要分配或某种形式的资源都不会被复制”。

为什么不复制一切?

当使用&str的时候,用Copy trait

字符串字面量”Hello, World!”存储在只读内存的某个地方(既不在栈中也不在堆中),而指向这个字符串字面量的指针存储在栈中。因为它是一个字符串字面量,它通常显示为引用,这意味着我们使用指向永久内存中存储的字符串的指针(关于引用的更多信息,请参见Rust的所有权,第2部分),并且它保证在整个程序的持续时间内有效,(它有一个静态生命周期)。

这里,hello和hello1中存储的指针正在使用栈。当我们使用=运算符时,Rust将hello中存储的指针的新副本压入栈,并将其绑定到hello1。在作用域的末尾,Rust添加了一个drop调用,从栈中弹出值以释放内存。这些指针可以被存储并容易地复制到栈中,因为它们的大小在编译时是已知的。

当使用堆,用Move trait

在堆上,字符串类型值为“Hello,World!”使用String : : from方法绑定到变量hello。然而,与字符串字面量不同,绑定到hello的数据不仅仅是指针,这些数据的大小会在运行时发生变化。在这里,=运算符将hello中的数据绑定到新变量hello1,有效地将数据从一个变量移动到另一个变量。可怜的hello现在无效,因为根据所有权第二条规则:“一次只能有一个所有者。”

但是为什么要这样做?Rust为什么不一直仅仅拷贝数据并把它绑定到新变量?

如果我们回想一下栈和堆之间的差异,我们会记得堆上存储的数据大小在编译时是未知的,这意味着我们需要在运行时运行一些内存分配步骤。 这可能是很昂贵的操作。 根据我们存储的数据量,如果我们整天都在制作数据副本,我们可能会快速耗尽内存。

除此之外,Rust的默认行为有助于保护我们免受在其他语言中可能遇到的记忆问题。

在堆上存储数据的一部分是在栈上存储指向该数据的指针。然而,与使用指针定位只读存储器不同,比如当使用字符串字面量时,指向堆的指针末端的数据可能会改变。指针是绑定到存储字符串类型的hello变量的数据的一部分。如果我们将同一个指针数据绑定到两个不同的变量,它可能如下所示:

复制字符串类型数据的粗略草图

我们有两个变量,hello和hello1,它们共享同一份数据的所有权,这违反所有权第二条规则:“一次只能有一个所有者。”让我们继续往下看。

在hello和hello 1的作用域结束时,我们必须将堆中的内存释放,以便在其他地方再次使用。

释放hello1

首先,我们对绑定到hello1的指针指向的存储数据调用drop,但是当我们在hello上调用drop时,接下来会发生什么?

重复释放错误

这称为重复释放错误,我认为对此最好的总结是 Stack Overflow 上的回答

从技术上讲,在C语言中,重复释放会导致未定义的行为。这意味着程序可以完全任意地运行,并且对所发生的一切都没有把握。发生这种事当然是件坏事!在实践中,重复释放内存块将破坏内存管理器的状态,这可能导致现有内存块被破坏,或者导致未来的分配以奇怪的方式失败(例如,相同的内存被两个连续的不同malloc调用来处理)。
重复释放在各种情况下都可能发生。一个相当常见的情况是,多个不同的对象都有指向彼此的指针,并开始通过调用free来清理。当这种情况发生时,如果你不小心,你可能会在清理对象时多次释放同一个指针。不过,还有很多其他的情况。
templatetypedef

这就是Rust试图防止发生的!通过使hello无效,编译器知道只在hello1上调用drop(在幕后调用free)。

这一切都很好,但是在某些情况下,我们确实希望复制存储在堆中的数据。Rust为此提供了一种简单的方法,就是clone()函数。

深度复制

请记住,对clone()的调用可能很昂贵,这就是Rust默认禁止这种“深度复制”的原因。

显然,关于Rust所有权的问题比这里所提到的要多得多;还有一些概念叫做借用、引用和切片!

迄今为止,似乎了解所有权更多的是为了浏览Rust的内存管理解决方案,而不是了解它解决的问题。但是,Rust Book鼓励你去了解为什么语言的作者渴望创造一种更安全的语言,而不是把它当作语言的怪癖。

参考

原文链接: https://medium.com/@thomascountz/ownership-in-rust-part-1-112036b1126b