跟着谷歌安卓团队学习Rust – Rust难点内存管理

大家好,我是梦兽编程。欢迎回来与梦兽编程一起刷Rust的系列。

这是由 Google 的 Android开发团队的分享Rust课程。本课程涵盖了 Rust 的方方面面,从基本语法到泛型和错误处理等高级主题。

该课程的最新版本可以在 https://google.github.io/comprehensive-rust/[1]找到。如果您正在其他地方阅读,请检查那里的更新。

如果你喜欢看梦兽编程的版本可以订阅跟着谷歌安卓团队学Rust订阅最新内容,梦兽编程也期待大家关注我的个人网站。

加入梦兽编程微信群,公众号回复111即可加入交流群与梦兽进行交流。

内存管理的回顾

工作多年后,使用很久的高级语言估计都已经忘记了计算机的内存机制。

如果你一直从事c/c++工作的话.这些概念会显得非常清新。

在日常程序开发过程中,我们可以把内存分为两块内存:

堆栈(Stack)是一种线性数据结构,它以“后进先出”(Last-In-First-Out,LIFO)的方式管理数据。它的操作只限于栈顶元素,也就是最后一个插入的元素。当你调用一个函数时,函数的局部变量和参数会被存储在堆栈中,每当一个函数调用结束时,它所占用的内存会被释放。这种自动管理内存的方式使得堆栈非常高效,但它的容量较小,通常有限制。

堆(Heap)是一种动态分配的内存区域,用于存储动态分配的对象。堆中的内存可以在任何时候分配和释放,没有固定的顺序或规则。在堆中分配的内存需要手动管理,即在程序中显式地分配和释放内存。由于堆的灵活性,它的容量相对较大,但分配和释放内存的过程可能会比较复杂。

总结一下:

  • 堆栈:局部变量的连续内存区域。
  1. 值具有编译时已知的固定大小。
  2. 速度极快:只需移动堆栈指针即可。
  3. 易于管理:遵循函数调用。
  4. 很棒的记忆地方。
  • 堆:在函数调用之外存储值。
  1. 值具有在运行时确定的动态大小。
  2. 比堆栈稍慢:需要一些簿记。
  3. 不保证内存局部性。

在Rust语言中,我们的内存走向如图所示:

fn main() {
    let s1 = String::from("Hello");
}

跟着谷歌安卓团队学习Rust–Rust难点内存管理

file

在栈中存放变量的定义,值都存放在堆上。

内存的管理方法

在没学Rust之前,我们市面上的编程的语言都可以分类两派:

  • c/c++ 这一类的让程序员完全手动管理的语言。
  • java 这一类让GC垃圾回收器进行管理的语言

** 但是rust给我们提供了一种新的组合 **

通过编译时强制执行正确的内存实现完全控制和安全性 管理。

Rust的所有权和借用模型可以在许多情况下以零成本的方式达到与C语言相似的性能,其中内存分配和释放操作精确地发生在需要的地方。它还提供类似于C++智能指针的工具。在需要时,还可以使用其他选项,如引用计数,并且还有第三方库可用于支持运行时垃圾回收(本课程不涉及)。

Rust的所有权和借用模型是一种内存管理机制,旨在确保程序在编译时具有内存安全性和线程安全性,并且不会出现数据竞争和内存泄漏等问题。在Rust中,每个值都有一个所有者,只能有一个所有者。当所有者超出范围时,其所拥有的值将被自动释放。这种所有权模型消除了传统的垃圾回收机制的需要,因为内存的分配和释放都是在编译时静态确定的,不需要运行时开销。

借用是Rust中另一个重要的概念,它允许临时地借用某个值的引用而不获取其所有权。这样可以避免不必要的所有权转移,提高代码的灵活性和性能。借用规则确保了在同一时间内,对于特定的数据,要么只有一个可变引用,要么有任意数量的不可变引用,但不能同时存在可变引用和不可变引用。

只有一个可变引用,这里要画重点。要么你在编码的过程很难理解。

Rust还提供了智能指针类型,如Box、Rc和Arc,它们类似于C++中的智能指针,提供了更灵活的内存管理方式。Box是用于在堆上分配的指针,Rc是引用计数指针,Arc是原子引用计数指针。这些智能指针类型允许多个所有者或共享所有权,并在不同情况下提供了不同的权衡。

总的来说,Rust的所有权和借用模型提供了一种高效且安全的内存管理机制,使得开发者能够编写高性能的代码,并且在编译时就能够捕获和修复潜在的内存错误。

所有权

所有变量绑定都有一个有效的范围,并且使用其作用域之外的变量:

struct Point(i32, i32);

fn main() {
    {
        let p = Point(3, 4);
        println!("x: {}", p.0);
    }
    println!("y: {}", p.1);
}

在这一段代码中 println!("y: {}", p.1);这一行会告诉你编译错误。原因在于Rust在作用域的末尾,将删除变量并释放数据。所以在main的作用域中p变量函数实际在内存中并不存在。

一个析构函数也可以在这里运行以释放资源。

所有权的转移

fn main() {
    let s1: String = String::from("Hello!");
    let s2: String = s1;
    println!("s2: {s2}");
    // println!("s1: {s1}");
}

在Rust中,当将一个值分配给另一个变量时,所有权会转移。这意味着原始变量将失去对该值的所有权,而新的变量将获得该值的所有权。这个过程称为所有权转移。

这事一件很滑稽的事情,Rust为了保存自己能在编译过程中能知道你的内存走向。所以一个内存指向只能一个变量堆栈上的内存指针拥有这个值。

现在我们来解释这段描述中的内容:

  1. s1将所有权转移给s2:这意味着原始的字符串值由变量s1拥有,但当将其赋给变量s2时,所有权从s1转移到s2。这意味着s1不再拥有该字符串值,而是s2拥有。
  2. 当s1超出其作用域时,什么都不会发生:这意味着当s1不再可见时,也就是它的作用域结束时,对该字符串值没有任何特殊处理。因为所有权已经转移到s2,所以s1不再拥有该值,也不会执行任何释放操作。
  3. 当s2超出其作用域时,字符串数据将被释放:这意味着当s2的作用域结束时,也就是它不再可见时,对该字符串值会执行释放操作。在Rust中,当变量拥有数据的所有权,并且该变量超出其作用域时,该数据会自动被释放。这确保了内存的正确管理,避免了内存泄漏。

Clone

为了解决这个问题,我们我在使用 s2 = s1这种场景,最优解就是对s1进行clone之后再赋值给s2

#[derive(Default)]
struct Backends {
    hostnames: Vec<String>,
    weights: Vec<f64>,
}

impl Backends {
    fn set_hostnames(&mut self, hostnames: &Vec<String>) {
        self.hostnames = hostnames.clone();
        self.weights = hostnames.iter().map(|_| 1.0).collect();
    }
}

比如我们在使用面向对象的时候,如果传入的是带&的这种引用类型,因为基础类型所有语言都是进行clone操作。如果不进行clone操作,回到调用地方时候,hostnames 就已经把所有权转移给 self.hostnames,对于新手玩家来讲将会非常懵逼,为什么我的hostnames变量无法使用了?

每次都需要自己写clone吗?

现在你对转移有了一定的了解,你会发现难道我们每次都需要自己写这个clone吗?那不是很麻烦? 在结构体中,提供了Type的clone只需要在derive中注入就会进行自动Clone操作。

#[derive(Copy, Clone, Debug)]
struct Point(i32, i32);

fn main() {
    let p1 = Point(3, 4);
    let p2 = p1;
    println!("p1: {p1:?}");
    println!("p2: {p2:?}");
}

内存释放时需要做的回调

如果你想对内存释放的时候进行一些操作,只需要实现Drop接口中的drop方法即可。可以理解成前端开发Vue,React中的销毁操作。

struct Droppable {
    name: &'static str,
}

impl Drop for Droppable {
    fn drop(&mut self) {
        println!("Dropping {}", self.name);
    }
}

fn main() {
    let a = Droppable { name: "a" };
    {
        let b = Droppable { name: "b" };
        {
            let c = Droppable { name: "c" };
            let d = Droppable { name: "d" };
            println!("Exiting block B");
        }
        println!("Exiting block A");
    }
    drop(a);
    println!("Exiting main");
}

在Rust中如果你想手动销毁就类似上面 drop(a)即可,但一般我们都很少做出来主函数的作用域,其他的作用域根本存活不到,因为超出作用域它就已经被释放。你最多就在drop生命周期中监听一下事情。哈哈