Rust的所有权(二)

当我们上次学习Rust中的所有权时,我们学习了Rust如何使用作用域来确定何时应该删除或释放内存中的资源或数据。

我们发现,对于具有“copy trait”的类型(即其数据可以存储在栈上的类型),所有权模型的行为类似于其他可能使用不同范式的语言,如垃圾收集。但是对于没有这种trait的类型,我们需要更加意识到所有权规则。

尽管所有权可能会带来设计上的妥协,但它以灵活性、明确性和安全性来弥补。

所有权和函数

复制 vs 移动

在第一个例子里,我们先传递一个字符串字面值(它的数据存储在栈上)给函数foo()。在第二个例子里,我们传递一个字符串类型的值(它的值存储在堆上)给不同函数foo()。在这两个例子的main()和foo()的实现,我们把在各自作用域变量的内存地址打印出来。

在第一个例子中,当我们复制变量的值并将其绑定到一个新变量时,我们看到类似的行为。 这是因为字符串字面量使用栈;存储指针所需的大小在编译时是已知的,因此,我们可以轻松地复制它的值并将其压入栈中。

这意味着函数main()和foo()都拥有自己的存储在变量string的指针副本。 当foo()的作用域结束时,foo()负责删除它自己的string变量,当main()的作用域结束时,它也负责删除它拥有的string变量。

相反,在第二个例子里,main()把变量string的所有权移交给foo()。这意味着,main()不再有变量string的所有权,即指针指向的内存的所有权。如果在所有权移交后,我们还在main()里尝试访问string变量,我们将收到错误。

复制很昂贵,Rust不复制,而是使得foo()负责内存地址0x7efced01c010的数据,如示例注释所示。

现在,仅当foo()超过作用域,Rust才释放那个地址的内存,从而使指向同一地址的任何其他变量失效。同样,我们这样做是为了避免重复释放的错误。

克隆

对于第二个示例,如果我们确实要复制string的值,那么main()和foo()都拥有自己的副本,类似于在栈上使用字符串字面量时,我们可以通过使用clone()方法进行“深度复制” :

clone()

如注释所示,main()和foo()有各自的string复制的所有权。虽然这是一个有效的解决方案,但它不是最有效率的,因为Rust每次都需要在堆上进行内存分配的过程。有时你真的希望这两个函数与同一个数据交互!(稍后将详细介绍)。

给予所有权

正如通过调用另一个函数并传入变量来获取所有权一样,可以通过从不同函数返回来赋予函数所有权。

给予所有权

foo()现在通过将字符串返回到调用foo()的位置来赋予main()所有权。正如所料,只有当main()的作用域结束时,Rust才会释放0x7fc98be1c010地址的内存。

给予和获取

如果我们遵循这一趋势,那么我们既可以给予所有权,也可以通过接收foo()返回相同的字符串类型将所有权归还给我们。

传递所有权

但是,将这些值传入和传出函数似乎很麻烦。 幸运的是,这是Rust维护者考虑到的头痛问题:

取得所有权然后通过各种函数归还所有权有点单调乏味。 如果我们想让一个函数使用一个值但不取得所有权,该怎么办? 如果我们想要再次使用它,除了我们可能想要返回的函数体所产生的任何数据之外,我们传入的任何内容都需要传回。 对我们来说幸运的是,Rust有一个这个概念的特性,称为引用。
Rust Book

引用和借用

所有权适用于共享和传递数据,但是,你必须遵循一些规则。

借用看起来像这样:

main()让foo()访问string,但是,(如果图中的标签所示),main()仍然是string的所有者。这意味着,foo()的作用域结束后,string不会被从内存中释放;main()仍然负责string的内存空间。

以下是我们如何在Rust中编写这种交互:

传递引用或借用

就像我们的绘图一样,main()将字符串的引用传递给foo(),而foo()接收String类型引用。 引用由&符号表示。 在foo()的作用域结束之后,执行返回到它的调用者,main(),并且字符串仍然有效。 foo()不必返回所有权,因为它从未获得所有权,只是借用了它。

&表示引用,它允许传递值而不需要放弃所有权!当我们传递引用的时候,Rust知道,所有权以及内存空间的释放的责任依然属于原来的所有者。

Rust允许我们创建任意数量的引用。

传递同一个值的多个引用

不管我们传递string的引用多少次,它的所有权依然是原来的所有者。(在这种情况下,所有权返回到字符串最初被实例化的地方,但是请记住,我们可以传递所有权,然后创建引用。)

可变性

最后要提到的是可变性。 Rust通常以函数式编写,但作者非常务实,并且理解现代语言并不总是那么非黑即白,因此Rust适应了可变性。

mut

Rust允许我们使用 mut 关键字来使得值可变。注意内存地址的变化,这表明必须在堆上重新分配字符串。

既然我们有一个可变变量,我们就可以做出一个可变引用!

传递一个可变引用

此处的语法有点特别,但是我们看到首先我们需要声明一个可变变量 let mut string 。然后当我们传递可变引用的时候,使用 &mut。最后,我们在函数签名上用 &mut 严格声明我们的函数接收一个可变引用。

现在,我们仍然可以确定只有main()负责string变量的销毁,而同时允许其他函数来修改string。

那些熟悉内存管理的人可能会想,如果不加以控制,这将是多么危险。如果几个函数持有一个可变引用,并试图同时异步更新同一内存位置,会发生什么情况;比如当使用线程时?这会导致数据竞争情况。

当两个或多个线程可以访问共享数据并试图同时更改数据时,就会出现竞争情况。因为线程调度算法可以在任何时候在线程之间切换,所以你不知道线程试图访问共享数据的顺序。因此,数据变化的结果取决于线程调度算法,即两个线程都“竞相”访问或改变数据。
Lehane & Amit JokiSO上的回答

当使用低级语言(如Rust )时,这个问题会更加严重。Rust允许我们访问原始指针,这可能会导致许多不安全的情况。

这是所有权被设置来防止的事情,它通过强制实施以下规则来做到这一点:“在任何给定的时间点,你可以有一个可变引用或者任何数量的不可变引用。”

具有此限制的好处是Rust可以在编译时阻止数据争用。 数据竞争类似于竞争条件,并且在发生以下三种行为时发生:

  • 两个或多个指针同时访问相同的数据。
  • 至少有一个指针用于写入数据。
  • 没有用于同步访问数据的机制。

数据争用会导致未定义的行为,并且在你尝试在运行时跟踪它们时可能难以诊断和修复;Rust可以防止这个问题的发生,因为它甚至不会编译通过有数据争用的代码!
Rust Book

Rust的所有权规则再次拯救了我们,这被强调为Rust提供的核心安全特性,超过了其他系统语言。这意味着Ruby程序员和我一样,仍然不需要熟悉内存管理的内部工作。

悬空的引用

最后一件事是,在传递引用时,还有另一个条件会导致称为悬空引用的bug。

悬空引用是指向已释放的数据的指针,例如:

悬空引用—编译不会成功

在这个例子中,foo() 返回一个 string 的引用。但是,一旦 foo() 的作用域结束,string的内存被释放,这意味着引用指向无效的内存空间!

Rust在编译这样的代码的时候会抛出错误:

Rust使用者们可以享受所有权带来的好处而不需要理解它提供的保护。然而,理解所有权解决的问题只会帮助我们写出更好的代码而不需要和编译器斗争。

还有一些关于Rust所有权的问题没有涉及到,但是有了这两篇文章,希望你能有足够的机会开始使用这个优雅的解决方案来解决一个其他棘手的问题。

原文链接: https://medium.com/@thomascountz/ownership-in-rust-part-2-c3e1da89956e