新手眼中的 Rust 所有权规则

如果你有关注本人博客,那么很明显,从今年年初开始,我便开始学习 Rust。此文与之前风格略有不同,旨在总结阅读 Rust 书籍时遇到的要点。到目前为止,它包含了我对 Rust 所有权规则的所有理解。

Rust 的主要亮点之一是它提供了内存安全性。通过提供编译时保证,将可能会导致内存错误的标志代码标记为编译时错误来做到这一点。编译时保证是通过所有权规则来实现。在这篇文章中,我总结了 Rust 所有权规则关键点,简述如下:

  • 值归变量所有。
  • 当变量超出使用范围时,变量值所占用的内存将被释放。
  • 变量值可以由其他变量使用,但是需遵守由编译器强制执行的若干规则。

变量的值可以被其他变量使用有 4 种方法,需要遵循的规则如下:

  • 克隆(clone):此处将值复制到新的变量。新变量拥有新的复制值的所有权,而原始变量保留其原始值的所有权。
  • 移动(move):所有权被转移到另一个要使用该值的变量,原始变量不再拥有所有权。
  • 不可变借用(immutable borrow)。这里没有发生所有权转移,但是可以通过另一个变量访问该值以进行读取。如果借用变量超出范围,内存不会被回收,因为借用变量没有所有权。
  • 可变借用(mutable borrow)。可以通过另一个变量对该值进行读取和写入操作。如果借用变量超出范围,内存也不会回收,因为借用变量没有所有权。

为了确保上述所有用法都不会造成内存错误,Rust 在编译时强制执行其所有权规则,概述如下:

克隆(clone)所有权规则

对值进行克隆没有什么特别,如果你已经编程了一段时间,并且知道如何传递数字和字符串之类的变量,则几乎与你习惯的机制相同。因此,实际上没有多少 Rust 特定的规则可以在这里强调。大多数情况下,你不需要克隆值,因为成本比较高。变量的其他用法与 Rust 特定的规则一起提供,因此我们将对其进行详细介绍。

移动(move)所有权规则

随着所有权 move,保存该值的原始变量将不再可用。现在只能通过新的变量访问该值。尝试使用已 move 所有权的变量,将导致编译错误:

fn main() {
    let mut original_owner = format!("Hello world");
    
    // move occurs to new owner    
    let new_owner = original_owner;
    
    // attempt to use original_owner will 
    // lead to compile time error    
    println!("{}", original_owner)
}
error[E0382]: borrow of moved value: `original_owner`

不可变借用(immutable borrow)所有权规则

使用不可变借用,借用的变量可以读取该值,但不能修改(即使原始值是 mutable 类型)不可变借用保证借用变量的值不会修改。任何试图违反这些条件的代码都将导致编译错误。

由于尝试修改不可变借用导致的编译错误


fn main() {

  let mut original_owner = format!("Hello world");
  // immutable borrow occurred
  let borrow_owner = &original_owner;

  // multiple reads possibe via owner
  println!("{}", original_owner);
  println!("{}", original_owner);
  
  // multiple reads possible via borrower
  println!("{}", borrow_owner);
  println!("{}", borrow_owner);
  //error: mutating not possible via borrower
  borrow_owner.push('.')
}

error[E0596]: cannot borrow `*borrow_owner` as mutable, as it is behind a `&` reference
  --> src/main.rs:14:5

违反不可变借用约束,修改借用的变量值而导致的编译错误

如果违反不可变借用变量不能修改的约束,Rust 编译器会在编译时报错。

fn main() {

  let mut original_owner = format!("Hello world");
  // immutable borrow occurred
  let borrow_owner = &original_owner;

  // multiple reads possible via owner
  println!("{}", original_owner);
  println!("{}", original_owner);

  // multiple reads possible via borrower
  println!("{}", borrow_owner);

  // original owner trying to mutate
  original_owner.push('.');

  println!("{}", borrow_owner);
}

error[E0502]: cannot borrow `original_owner` as mutable because it is also borrowed as immutable

如果将变量修改移到借用方超出范围的位置,则该修改不会导致编译错误。这是因为不存在违反对不可变借用的担保(即值不会改变)。例如以下编译:


fn main() {

  let mut original_owner = format!("Hello world");
  // immutable borrow occurred
  let borrow_owner = &original_owner;
  
  // multiple reads possible via owner
  println!("{}", original_owner);
  println!("{}", original_owner);
  
  // multiple reads possible via borrower
  println!("{}", borrow_owner);
  println!("{}", borrow_owner);
  
  // original owner trying to mutate
  original_owner.push('.');
}

可变借用(mutable borrow)的所有权规则

使用可变借用,借用方将获得该变量的所有权,这意味着新的读写必须经过借用方。具有可变所有权的原始变量也将无法读取或写入,直到可变借用方超出使用范围。该限制强制执行内存一致性。由于它有效地不允许通过除变量之外的其他方式进行任何其他写入或读取,因此它可以避免出现数据数据争用之类的情况。

可变借用的规则还确保始终只有一个活动的可变借用。这样做的道理在于,一旦你拥有多个可变借用并且具有修改的能力,就无法保持数据一致性。这些规则在编译时进行检查。

多次使用可变借用导致编译错误

fn main() {

  let mut original_owner = format!("Hello world");
  // mutable borrow occurred    
  let borrow_mutable_owner = &mut original_owner;
  // compile error due to second mutable borrow     
  let illegal_borrow = &mut original_owner; 
  println!("{}", borrow_mutable_owner);
}

error[E0499]: cannot borrow `original_owner` as mutable more than once at a time

下面是违反了可变借用独占访问的编译错误

fn main() {
    let mut original_owner = format!("Hello world");
    // mutable borrow occurred
    let borrow_mutable_owner = &mut original_owner;

    // borrowing owner can also mutate
    borrow_mutable_owner.push('!');
    // compile error due to: 
    //original owner can no longer read
    println!("{}", original_owner);

    // compile error due to:
    // original owner also cannot write
    original_owner.push('.');
    println!("{}", original_owner);
    println!("{}", borrow_mutable_owner);
    println!("{}", borrow_mutable_owner);
}

error[E0502]: cannot borrow `original_owner` as immutable because it is also borrowed as mutable

如果将读取及修改操作移动到可变借用变量超出范围之后,则不会有编译错误。这很好,因为不再需要执行可变借用所要求的互斥。

fn main() {
   
 
 let mut original_owner = format!("Hello world");
  // mutable borrow occurred
  let borrow_mutable_owner = &mut original_owner;

  // borrowing owner can also mutate
  // below is the last usage
  borrow_mutable_owner.push('!');
  
  println!("{}", borrow_mutable_owner);
  // borrow_mutable_owner is now out of scope
  // original owner can now read
  println!("{}", original_owner);
 
  // original owner can now write
  original_owner.push('.');
  println!("{}", original_owner);
}

编译以上代码片段,将不会发生任何错误。

小结

简洁地总结一下:

  • 使用 clone:
  • 没有什么合适规则防止内存 bug
  • 对于非常规数据结构,通常代价昂贵
  • 使用 move:
  • 将所有权从一个变量中移出后,该变量将无法再访问其最初拥有的值。
  • 使用不可变借用 (immutable borrow):
  • 可以创建无限的不可变借用
  • 所有不可变借用只能读
  • 原始拥有变量在修改其拥有的值方面存在限制,只有不存在不可变借用,它才可以修改。这样可以确保 Rust 保证对不可变借用的担保不会改变。
  • 基本上:许多读操作,没有写操作(条件是一直有读操作,否则就可以写)
  • 使用可变借用(mutable borrow):
  • 只能使用一次可变借用。
  • 所有的读写操作都只能通过活动的可变借用(active borrow)完成。
  • 只要有活动的可变借用,原始拥有变量也将无法再读取或写入。
  • 基本上:如果只有一个读和写:使用可变借用

原文: https://www.geekabyte.io/2020/02/rust-ownership-rules.html

参考阅读:

  • Rust Web框架怎么选?研究本文就够了!
  • 深入浅出Rust异步编程之Tokio
  • Gateway技术革命 - Tengine开源Dubbo功能
  • Joe Armstrong最喜欢的一段Erlang程序
  • Clojure 语言在 2020 年的现状

本文作者 dade,由高可用架构翻译。技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。

高可用架构

改变互联网的构建方式