关于rust:译理解Rust的-borrow-checker

40次阅读

共计 3536 个字符,预计需要花费 9 分钟才能阅读完成。

原文链接:Understanding the Rust borrow checker

初尝 Rust 的这一天终于到来了,你满怀期待地写下几行 Rust 代码,而后在命令行输出 cargo run 指令,期待着编译通过。之前就据说过,Rust 是一门只有编译能通过,就能运行地语言,你兴奋地期待着程序是否会失常运行。编译跑起来了,而后立马输入了谬误:

error[E0382]: borrow of moved value

看来你是遭逢了“借用查看器”的问题。

什么是借用查看器?

借用查看器是 Rust 之所以为 Rust 的基石之一,它可能帮忙(或者说是强制)你治理“所有权”,即官网文档第四章介绍的ownership:“Ownership 是 Rust 最特地的特色,它确保 Rust 不须要垃圾回收机制也可能保障内存平安”。

所有权,借用查看器以及垃圾回收:这些概念开展讲能讲很多,本文将介绍借用查看器能为咱们做什么(能阻止咱们做什么),以及它和其余内存管理机制的区别。

本文假如你对高级语言有,比方 Python,JavaScript 或 C# 之类的肯定理解就行,不要求计算机内存工作原理相干的常识。

垃圾回收 vs. 手动内存调配 vs. 借用查看

对于很多罕用的编程语言,你都不必思考变量是存在哪儿的,间接申明变量,剩下的局部,语言的运行时环境会通过垃圾回收来解决。这种机制形象了计算机内存治理,使得编程更加轻松对立。

不过这就须要咱们额定深刻一层能力展现它和借用查看的区别,就从栈 stack 和堆 heap 开始吧

栈与堆

咱们的程序有两种内存来存值,栈 stack 和堆 heap。他们的区别有好些,但咱们只用关怀其中最重要的一点:栈上存储的必须是大小固定的数据,存取都很不便,开销小;像字符串(可变长),列表和其它领有可变大小的汇合类型数据,存储在堆上。因而计算机须要给这些不确定的数据调配足够大的堆内存空间,这一过程会耗费更多的工夫,并且程序通过指针拜访它们,而不能像栈那样间接拜访。

总结来说,栈内存存取数据疾速,但要求数据大小固定;堆内存尽管存取速度慢些,然而对数据的要求宽松。

垃圾回收

在带有垃圾回收机制的语言中,栈上的数据会在超出作用域范畴时被删除,堆上的数据不再应用后会由垃圾回收器解决,不须要程序员去具体关怀堆栈上产生的事件。

然而对于像 C 这样的语言,要手动治理内存。那些在更高级的语言中轻易就能够简略初始化的列表,在 C 语言中须要手动调配堆内存来初始化,而且数据不必了还须要手动开释这块儿内存,否则就会造成内存透露,而且内存只能被开释一次。

这种手动调配手动开释内存的过程容易出问题。微软证实他们 70% 的破绽都是内存相干的问题导致的。既然手动操作内存的危险这么高,为什么还要应用呢?因为相比垃圾回收机制,它具备更高的控制力和性能,程序不必停下来花工夫查看哪些内存须要被开释。

Rust 的所有权机制就处在二者之间。通过在程序中记录数据的应用并遵循肯定的规定,借用查看器可能判断数据在什么时候可能初始化,什么时候能被开释(在 Rust 中开释被称作 drop),联合了垃圾回收的便当与手动治理的性能,就像一个内嵌在语言中的内存管理器。

在实操中,在所有权机制下咱们能够对数据进行三种操作形式:

  1. 间接将数据的所有权移交进来
  2. 拷贝一份数据,独自将拷贝数据的所有权移交进来
  3. 将数据的援用移交进来,保留数据自身的所有权,让接管方临时“借用”(borrow

应用哪种形式根据场景而定。

借用查看器的其它能力:并发

除了解决内存的调配与开释,借用查看器还能阻止数据竞争,正如 Rust 所谓的“无惧并发”,让你毫无顾虑地进行并发、并行编程。

毛病

美妙的事物总是随同着代价,Rust 的所有权零碎同样也有缺点,事实上如果不是这些缺点,我也不会专门写这篇文章。

比拟难上手,是借用查看机制的一大毛病。Rust 社区中不乏被它折磨的新人,我本人也在把握它下面破费了很多工夫。

举个例子,在借用机制下,共享数据会变得比拟繁琐,尤其是共享数据的同时还要扭转数据的场景。很多其它语言中十分简便就能创立的数据结构,在 Rust 中会比拟麻烦。

然而当你了解了它,编写 Rust 代码会更棘手。我很喜爱社区里的一句话:

借用机制的几条规定,就像拼写查看一样,如果你一点儿都不了解他们,那你写进去的代码根本都是错的。平心静气地了解了它们,才会写出正确的代码。

几条根本规定:

  1. 每当向一个办法传递参数变量(非变量的援用)时,都是将该变量的所有权转移给调用的办法,尔后你就不能再应用它了。
  2. 每当传递变量的援用(即所谓的借用),你能够传递任意多个不可变援用,或者一个可变援用。也就是说可变援用只能有一个。

实际

了解了借用查看机制后,当初实际一下。咱们将应用 Rust 中可变长度的 list:Vec<T> 类型(相似 Python 中的 list 和 JavaScript 中的 Array),可变长度的个性决定了它须要应用堆内存来存储。

这个例子比拟刻意,但它能很好的阐明上述的规定。咱们将创立一个 vector,将它作为参数传递给一个函数进行调用,而后看看在外面会产生什么。

留神:上面这个代码实例不会通过编译

fn hold_my_vec<T>(_: Vec<T>) {}

fn main() {let v = vec![2, 3, 5, 7, 11, 13, 17];
    hold_my_vec(v);
    let element = v.get(3);
    
    println!("I got this element from the vector: {:?}", element);
}

运行后,会失去如下谬误:

error[E0382]: borrow of moved value: `v`
--> src/main.rs:6:19
          |
        4 |     let v = vec![2, 3, 5, 7, 11, 13, 17];
          |         - move occurs because `v` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
        5 |     hold_my_vec(v);
          |                 - value moved here
        6 |     let element = v.get(3);
          |                   ^ value borrowed here after move

这个报错信息通知咱们 Vec<i32> 没有实现 Copy 个性(trait),因而它的所有权是被转移(借用)了,无奈在这之后再拜访它的值。只有能在栈上存储的类型实现了 Copy 个性,而 Vec 类型必须调配在堆内存上,它无奈实现该个性。咱们须要找到另一种伎俩来解决相似状况。

Clone

尽管 Vec 类型变量不能实现 Copy 个性,但它实现了 Clone 个性。在 Rust 中,克隆是另一种复制数据的形式。与 copy 只能对栈上的数据进行拷贝、开销小的特点不同,克隆也能够面向堆数据,并且开销能够很大。

回到下面的例子中,传值给函数的场景,那咱们给它一个向量的克隆也能够小道目标。如下代码能够失常运行:

fn hold_my_vec<T>(_: Vec<T>) {}

fn main() {let v = vec![2, 3, 5, 7, 11, 13, 17];
    hold_my_vec(v.clone());
    let element = v.get(3);

    println!("I got this element from the vector: {:?}", element);
}

但这个代码理论做了很多无用功,hold_my_vec 函数都没应用传入的向量,只是接管的它的所有权。并且例子中的向量十分小,克隆起来没什么累赘,对于刚开始接触 rust 开发的阶段,这样能够不便地看到后果。实际上也有更好的形式,上面就来介绍。

援用

除了间接将变量的值所有权移交给函数,还能够把它“借”进来。咱们须要批改下 hold_my_vec 的函数签名,让它接管的参数从 Vec<T> 更改为 &Vec<T>,即援用类型。调用该函数的形式也须要批改下,让 Rust 编译器晓得只是将向量的援用———— 一个借用值,交给函数应用。这样函数就是会短暂地借用这个值,在之后的代码中依然能够应用它。

fn hold_my_vec<T>(_: &Vec<T>) {}

fn main() {let v = vec![2, 3, 5, 7, 11, 13, 17];
    hold_my_vec(&v);
    let element = v.get(3);

    println!("I got this element from the vector: {:?}", element);
}

总结

这篇文章只是对借用查看机制简短地概览,介绍它会做什么,以及为什么这么做。更多的细节就留给读者本人开掘了。

实际上,随着你的程序代码量扩张,你会遭逢更多辣手的问题,须要围绕所有权和借用机制开展更深刻的思考。甚至为了贴合 Rust 的借用机制,你得从新设计代码的组织构造。

Rust 的学习曲线的确比拟平缓,但只有继续学习,你总能一路向上。

正文完
 0