所有权

Rust 中,每一个值都有一个生命周期的所有者。当所有者被释放,那么值就会被清除。

{  
let point = Box::new((0.625, 0.5)); // 分配point
let label = format!("{:?}", point); // 分配label assert_eq!(label, "(0.625, 0.5)");
}
// 两者都被清除

如果再复杂一点,那么就是一个所有权树的概念

Rust 正是对这个所有权树做了足够好的分析 ,才可以完成实体关系的可追踪性

但是不能一个对象所有权限制太严格了,所以:

  • 可以把一个所有者转移到另一个所有者
  • 提供基于计数的 Rc 和 Arc ,可以完成出现多个所有者的情况
  • 可以 借用 其 引用。 引用是生命期有限的非所有指针

转移

Rust 中 , 所有权是可以转移的 , 转移后原来的变为未初始化状态。

Python 将指针从s复制到t和u , 更新列表的引用计数 , 不复制堆内存 主要是为了方便垃圾收集 - Java类似

s = ['udon', 'ramen', 'soba'] 
t= s  
u= s

C++ 直接就是完全的深复制,堆存在三份对象

using namespace std;
vector<string> s = { "udon", "ramen", "soba" };
vector<string> t = s;
vector<string> u = s;

Rust是保留一份堆对象,同时赋值会转移所有权,所以栈上只有一个地址指向堆 编译器会认为s 还没有初始化

类似函数参数调用、元祖对象构建都会转移所有权

转移所有权仅仅是复制一下栈的特征值

Copy 类型

对于在堆中存储的对象,会涉及到所有权的转移,那么直接就是栈上的Copy类型数据呢 , 如一个整型数据,就不必要那么麻烦了,直接复制值就行了

Copy类型组合而成的元祖、或者固定大小的数组都是Copy类型

任何在值清除后需要特殊处理的,都不能是Copy 类型

默认情况下 ,struct 和 enum 不是 Copy 类型

但是我们可以显式声明一下

#[derive(Copy, Clone)]
struct Label { number: u32 }

Rc & Arc

如何将三个变量指向一份堆内存,类似Python实现的那个效果?

  • 类似可以解决类似图数据结构多个指向的问题
  • 在多线程中,多个线程可能会持有同一个数据,但是你受限于 Rust 的安全机制,无法同时获取该数据的可变引用

Rc 就可以帮助我们完成 (Arc 仅仅是线程安全的 Rc类型,允许多个线程之间安全共享)

fn main() {
    let s = String::from("hello, world");
    // s在这里被转移给a
    let a = Box::new(s);
    // 报错!此处继续尝试将 s 转移给 b
    let b = Box::new(s);
}
 
// 使用 Ref 可以解决这问题
use std::rc::Rc;
fn main() {
    let a = Rc::new(String::from("hello, world"));
    let b = Rc::clone(&a);
 
    assert_eq!(2, Rc::strong_count(&a));
    assert_eq!(Rc::strong_count(&a), Rc::strong_count(&b))
}

不要被 clone 字样所迷惑,以为所有的 clone 都是深拷贝。这里的 clone 仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据,因此 a 和 b 是共享了底层的字符串 s,这种复制效率是非常高的。当然你也可以使用 a.clone() 的方式来克隆,但是从可读性角度,我们更加推荐 Rc::clone 的方式

use std::rc::Rc;
 
struct Owner {
    name: String,
    // ...其它字段
}
 
struct Gadget {
    id: i32,
    owner: Rc<Owner>,
    // ...其它字段
}
 
fn main() {
    // 创建一个基于引用计数的 `Owner`.
    let gadget_owner: Rc<Owner> = Rc::new(Owner {
        name: "Gadget Man".to_string(),
    });
 
    // 创建两个不同的工具,它们属于同一个主人
    let gadget1 = Gadget {
        id: 1,
        owner: Rc::clone(&gadget_owner),
    };
    let gadget2 = Gadget {
        id: 2,
        owner: Rc::clone(&gadget_owner),
    };
 
    // 释放掉第一个 `Rc<Owner>`
    drop(gadget_owner);
 
    // 尽管在上面我们释放了 gadget_owner,但是依然可以在这里使用 owner 的信息
    // 原因是在 drop 之前,存在三个指向 Gadget Man 的智能指针引用,上面仅仅
    // drop 掉其中一个智能指针引用,而不是 drop 掉 owner 数据,外面还有两个
    // 引用指向底层的 owner 数据,引用计数尚未清零
    // 因此 owner 数据依然可以被使用
    println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name);
    println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name);
 
    // 在函数最后,`gadget1` 和 `gadget2` 也被释放,最终引用计数归零,随后底层
    // 数据也被清理释放
}
  • ! Rc/Arc 是不可变引用,你无法修改它指向的值,只能进行读取,如果要修改,需要配合内部可变性 RefCell 或互斥锁 Mutex
  • 一旦最后一个拥有者消失,则资源会自动被回收,这个生命周期是在编译期就确定下来的
  • Rc 只能用于同一线程内部,想要用于线程之间的对象共享,你需要使用 Arc
  • Rc<T> 是一个智能指针,实现了 Deref 特征,因此你无需先解开 Rc 指针,再使用里面的 T,而是可以直接使用 T,例如上例中的 gadget1.owner.name

上述的所有权转移和引用计数指针都是为了缓和所有权树过于严苛的方式

Cell 和 RefCell

 Rust 提供了 Cell 和 RefCell 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用)

内部可变性的实现是因为 Rust 使用了 unsafe 来做到这一点,但是对于使用者来说,这些都是透明的,因为这些不安全代码都被封装到了安全的 API 中

Cell 和 RefCell 在功能上没有区别,区别在于 Cell<T> 适用于 T 实现 Copy 的情况:

use std::cell::Cell;
fn main() {
  let c = Cell::new("asdf");
  let one = c.get();
  c.set("qwer");
  let two = c.get();
  println!("{},{}", one, two);
}
 
let c = Cell::new(String::from("asdf"));
// 编译器会立刻报错,因为 `String` 没有实现 `Copy` 特征
| pub struct String {
| ----------------- doesn't satisfy `String: Copy`
|
= note: the following trait bounds were not satisfied:
        `String: Copy`
 

Rc/Arc 和 RefCell 合在一起,解决了 Rust 中严苛的所有权和借用规则带来的某些场景下难使用的问题。但是它们并不是银弹,例如 RefCell 实际上并没有解决可变引用和引用可以共存的问题,只是将报错从编译期推迟到运行时,从编译器错误变成了 panic 异常:

use std::cell::RefCell;

fn main() {
    let s = RefCell::new(String::from("hello, world"));
    let s1 = s.borrow();
    let s2 = s.borrow_mut();

    println!("{},{}", s1, s2);
}

依然会因为违背了借用规则导致了运行期 panic

RefCell 正是用于你确信代码是正确的,而编译器却发生了误判时

对于大型的复杂程序,也可以选择使用 RefCell 来让事情简化。例如在 Rust 编译器的ctxt结构体中有大量的 RefCell 类型的 map 字段,主要的原因是:这些 map 会被分散在各个地方的代码片段所广泛使用或修改。由于这种分散在各处的使用方式,导致了管理可变和不可变成为一件非常复杂的任务(甚至不可能),你很容易就碰到编译器抛出来的各种错误。而且 RefCell 的运行时错误在这种情况下也变得非常可爱:一旦有人做了不正确的使用,代码会 panic,然后告诉我们哪些借用冲突了。

总之,当你确信编译器误报但不知道该如何解决时,或者你有一个引用类型,需要被四处使用和修改然后导致借用关系难以管理时,都可以优先考虑使用 RefCell

use std::cell::RefCell;
use std::rc::Rc;
fn main() {
    let s = Rc::new(RefCell::new("我很善变,还拥有多个主人".to_string()));
 
    let s1 = s.clone();
    let s2 = s.clone();
    // let mut s2 = s.borrow_mut();
    s2.borrow_mut().push_str(", oh yeah!");
 
    println!("{:?}\n{:?}\n{:?}", s, s1, s2);
}
  • Rust 所有权 复习 (@2024-01-24)