《Rust 编程第一课》 学习笔记 Day 18

138 阅读3分钟

大家好,我是砸锅。一个摸鱼八年的后端开发。熟悉 Go、Lua。第十八天还是继续和大家一起学习 Rust😊

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情

Rust 对可变引用的限制:

  1. 在一个作用域里面,仅允许一个活跃的可变引用。所谓活跃是指真正被用来修改数据的可变引用,只是定义没有使用或者当作可读引用来使用都不算
  2. 在一个作用域里面,活跃的可变引用(写)和可读引用(读)是互斥的,不能同时存在。和别的语言里面的 RwLock 类似,并发下读写访问规则

Rust 处理很多问题的思路是:编译时,处理大部份使用场景,保证安全性和效率;运行时处理无法编译时处理的场景,虽然会牺牲一定的效率,但是提高了灵活性

Rust 在运行时做动态检查是利用了引用计数的智能指针:Rc(Reference counter) 和 Arc (Atomic reference counter)

Rc

对某个数据结构 T,可以创建引用计数 Rc,让它拥有更多的所有者。Rc 会把对应的数据结构创建在堆上,如果要对数据创建更多的所有者,可以使用 clone(), clone 操作不会将其内部数据复制,只会增加引用计数

当一个 Rc 结构离开作用域被 drop() 时,就会减少引用计数,直到引用计数为零,就会真正清除对应的内存

use std::rc::Rc;

fn main() {
    let a = Rc::new(1);
    let b = a.clone();
    let c = a.clone();

    // a: 0x6000034111b0, b: 0x6000034111b0, c:0x6000034111b0
    println!("a: {:p}, b: {:p}, c:{:p}", a, b, c); 
}

Box::leak() 机制

Box 是 Rust 下的智能指针,可以强制将任何数据结构创建在堆上,然后在栈上放一个指针指向这个数据结构

Box::leak() 创建的对象,从堆内存里面泄漏出去,不受栈内存控制,生命周期可以大到和整个进程的生命周期一致的对象

实现一个 DAG

use std::rc::Rc;

#[derive(Debug)]
struct Node {
    id: usize,
    downstream: Option<Rc<Node>>,
}

impl Node {
    pub fn new(id: usize) -> Self {
        Self {
            id,
            downstream: None,
        }
    }

    pub fn update_downstream(&mut self, downstream: Rc<Node>) {
        self.downstream = Some(downstream);
    }

    pub fn get_downstream(&self) -> Option<Rc<Node>> {
        self.downstream.as_ref().map(|v| v.clone())
    }
}

fn main() {
    let mut node1 = Node::new(1);
    let mut node2 = Node::new(2);
    let mut node3 = Node::new(3);
    let node4 = Node::new(4);
    node3.update_downstream(Rc::new(node4));

    node1.update_downstream(Rc::new(node3));
    node2.update_downstream(node1.get_downstream().unwrap());
    println!("node1: {:?}, node2: {:?}", node1, node2);
}

内部可变性

例如 let mut 声明一个可变的值,或者是 &mut 声明一个可变引用时,编译器可以在编译时进行严格的检查,确保只有可变的值和可变的引用才可以修改值内部的数据,这个被称为外部可变性

如果想在编译器检查时这个值是只读的,然后在运行时可以变成可变借用,修改内部的数据,这时候就需要用到 RefCell

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(1);
    { // 单独加花括号是为了分开两个不同的作用域,因为在所有权规则里,同一个作用域下不能同时有活跃的可变借用和不可变借用
		    let mut v = data.borrow_mut();
        *v += 1;
		}
    println!("data: {:?}", data.borrow());
}

Arc

如果是多个线程访问同一块内存的时候,就不能用 Rc了,因为 Rc 为了性能并没有使用线程安全的引用计数器。这时候需要使用 Arc,因为它实现了线程安全的引用计数器

内部引用计数使用了 Atomic Usize,利用 CPU 的特殊指令,来保证多线程下安全

RefCell 也不是线程安全的,如果在多线程的场景下,需要使用 Mutex 和 RwLock。Mutex 是互斥量,获取互斥量的线程对数据独占访问。RwLock 是读写锁,获得写锁的线程对数据独占访问,同时没有写锁的时候,允许有多个读锁