写给前端看的Rust教程(5)Borrowing & Ownership

1,821 阅读7分钟

原文:24 days from node.js to Rust

前言

在介绍strings之前,我们需要先介绍一下所有权(ownership),当我们介绍到所有权(ownership)的时候,就开始步入rust中复杂的部分了,这并不是说这很难理解,而是说rust中的规则会在所有地方强迫你重新审视逻辑化和结构化

rust的流行和受欢迎是因为它可以在不使用垃圾收集的同时保证内存安全,而其它诸如JavaScriptGo等语言则是使用垃圾收集来做内存管理,这些语言追踪对象的引用,直到引用数量降到0时释放内存。垃圾收集器以资源和性能为代价为开发人员提供了方便,这套机制大多数情况下是好用的,可一旦遇到问题,就会很棘手,故障排除和优化垃圾收集本身就是一种黑魔法。在rust世界里,当你严格遵循规则的时候,就可以抛开垃圾收集实现内存安全

内存安全不仅仅涉及到程序稳定性,还涉及到安全性。例如SQL注入,该漏洞源于数据库客户端通过未经处理的用户输入来创建SQL语句,黑客通过传递精心设计的输入来改变数据库内容以及运行新的指令。幸运的是,这类攻击完全可以被100%的预防,不过即便如此,它依然是最普遍的网络攻击。而内存不安全的代码就有点类似SQL注入变量,它可以在任何地方,很难查找。内存安全漏洞是大多数严重漏洞的根本原因,完全消除它们而不影响性能是一个有吸引力的概念

相关阅读

本指南在可能的情况下会利用已有的资源进行概念的澄清,下面这些内容可以帮助你理解本文相关的知识:

  1. Rust book Ch.3: Common Programming Concepts
  2. Rust book Ch.4: Understanding Ownership
  3. Rust by Example: Variable Bindings
  4. Rust by Example: Primitives
  5. Rust by Example: Flow control
  6. Rust by Example: Functions

基础知识

变量声明 & 可变性

JavaScript中的变量分为可变和不可变,分别用letconst来修饰,rust中也有letconst,不过这里需要先忽略const。在JavaScript中使用const的场景,在rust中需要的是let而非const;在在JavaScript中使用let的场景,在rust中需要的是let mut。默认情况下在rust的世界里变量都是不可变的,这是个好事情,当你用习惯了之后,甚至会觉得如果JavaScript也是这样就好了

JavaScript中你可以这么写代码:

let one = 1;
console.log({ one });
one = 3;
console.log({ one });

对应的rust版本是这样的:

fn main() {
    let mut mutable = 1;
    println!("{}", mutable);
    mutable = 3;
    println!("{}", mutable);
}

rust中如果变量是可变的,则更改数值的时候需要注意不要更改变量类型,例如下面的代码是无法正常工作的:

fn main() {
    let mut mutable = 1;
    println!("{}", mutable);
    mutable = "3"; // Notice this isn't a number.
    println!("{}", mutable);
}

不过你可以用let声明一个类型不同,名称相同的变量

fn main() {
    let myvar = 1;
    println!("{}", myvar);
    let myvar = "3";
    println!("{}", myvar);
}

Rust的借用审查

为了保证内存安全,rust采用了一套严格的规则来加以规范数据的传递、借用以及所有权

规则1:所有权(Ownership)

当你把数据赋值给另外一个变量时,原有的变量将因失去数据的所有权而不能再访问变量,例如下面的代码当你试着运行时将会报错:

use std::{collections::HashMap, fs::read_to_string};

fn main() {
    let source = read_to_string("./README.md").unwrap();
    let mut files = HashMap::new();
    files.insert("README", source);
    files.insert("README2", source);
}

注意:在我们的示例代码中你会大量见到.unwrap(),不过在正式版代码中最好不要用,后面到Result & Option的部分时会详细谈。现在需要了解的关键点是,在示例中这么用没问题,但在你自己的程序中,除非确定你的代码不会出现错误,否则不要用.unwrap()

当你试图运行上面的代码时,会发现出现错误,请注意这段报错信息use of moved value: source,这个提示将会在你的rust编程生涯中大量见到

error[E0382]: use of moved value: `source`
  |
4 |     let source = read_to_string("./README.md").unwrap();
  |         ------ move occurs because `source` has type `String`, which does not implement the `Copy` trait
5 |     let mut files = HashMap::new();
6 |     files.insert("README", source);
  |                            ------ value moved here
7 |     files.insert("README2", source);
  |                             ^^^^^^ value used here after move

For more information about this error, try `rustc --explain E0382`.

当我们将source插入HashMap的时候,我们放弃了source的所有权,如果你想让上面的这段代码编译成功,需要在第一次使用source将其克隆:

use std::{collections::HashMap, fs::read_to_string};

fn main() {
    let source = read_to_string("./README.md").unwrap();
    let mut files = HashMap::new();
    files.insert("README", source.clone());
    files.insert("README2", source);
}

如果不明白为什么会出现所有权丢失,那么重新阅读下 Ownership and Functions

注意:在上面最初的报错信息里,你可能会留意到有does not implement the Copy trait的提示,关于Copy 和 Clone 的差异,这里想强调一点,Clone代价更加昂贵且需要程序员手动调用

规则2:借用(Borrowing)

引用不会获得值的所有权,只是借用值的所有权。引用数据的时候,如果数据是不可变的,那么可以无数次的借用;如果是可变的,则只能引用一次(主要出于对并发状态下发生数据访问碰撞的考虑)。我们在变量前面增加&来表示这是一个引用,最常见的使用场景是不用克隆的情况下传递大量的数据

use std::{collections::HashMap, fs::read_to_string};

fn main() {
    let source = read_to_string("./README.md").unwrap();
    let mut files = HashMap::new();
    files.insert("README", source.clone());
    files.insert("README2", source);

    let files_ref = &files;
    let files_ref2 = &files;

    print_borrowed_map(files_ref);
    print_borrowed_map(files_ref2);
}

fn print_borrowed_map(map: &HashMap<&str, String>) {
    println!("{:?}", map)
}

注意:println!中的{:?}语法是一种debug格式化器,在输出数据信息方面比较有用处

如果你想创建一个可变的引用,需要将&改成&mut

use std::{collections::HashMap, fs::read_to_string};

fn main() {
    let source = read_to_string("./README.md").unwrap();
    let mut files = HashMap::new();
    files.insert("README", source.clone());
    files.insert("README2", source);

    let files_ref = &mut files;
    let files_ref2 = &mut files;

    needs_mutable_ref(files_ref);
    needs_mutable_ref(files_ref2);
}

fn needs_mutable_ref(map: &mut HashMap<&str, String>) {}

但是这样一来当你编译的时候就会报错:

error[E0499]: cannot borrow `files` as mutable more than once at a time
   |
9  |     let files_ref = &mut files;
   |                     ---------- first mutable borrow occurs here
10 |     let files_ref2 = &mut files;
   |                      ^^^^^^^^^^ second mutable borrow occurs here
11 |
12 |     needs_mutable_ref(files_ref);
   |                       --------- first borrow later used here

For more information about this error, try `rustc --explain E0499`.

rust的编译器很智能,而且随着不断的迭代更新也是越来越完善,做出如下调整后,代码会顺利通过编译:

use std::{collections::HashMap, fs::read_to_string};

fn main() {
    let source = read_to_string("./README.md").unwrap();
    let mut files = HashMap::new();
    files.insert("README", source.clone());
    files.insert("README2", source);

    let files_ref = &mut files;

    needs_mutable_ref(files_ref);

    let files_ref2 = &mut files;

    needs_mutable_ref(files_ref2);
}

fn needs_mutable_ref(map: &mut HashMap<&str, String>) {}

当你开始使用rust后会发现,大多数的错误都可以通过调整顺序来解决,在想不通时可以试试

引用

如果你之前用的一直是JavaScript,没有用过诸如C之类的语言,你或许会对引用感到困惑,甚至觉的自己根本用不到引用,而实际上在JavaScript中你一直在使用引用,JavaScript中的每个Object都是。如果不使用引用,你就需要每次给函数传递对象时都做一次深拷贝

总结

所有权是个核心问题,在rust中会反复出现。在面对Strings之前,我们需要在下一篇文章中先一个更深入的学习

更多