所有权系统(3)

246 阅读6分钟

智能指针与所有权

独占所有权 Box<T>

智能指针(Samrt Pointer) 是一种结构体,是对指针的封装。 智能指针区别于常规结构体的特征在于,它实现了 Deref 和 Drop。

智能指针和普通引用的区别之一是所有权的不同:智能指针拥有资源的所有权,而普通引用只是对所有权的借用。

智能指针 Box<T> 独占所有权的样例:

```
fn main() {
    let x = Box::new("hello");
    let y = x;
    println!("x = {}", x); // value borrowed here after move
}
```

解引用智能指针 Box<T>

对于 Box<T> 类型 a,b 来说 *a 和 *b 操作相当于 *(a.defref) 和 *(b.deref)。

如果包含的类型 T 属于复制语义,则执行按位复制;如果属于移动语义,则移动所有权。

fn main() {
    let a = Box::new("hello");
    let b = Box::new("Rust".to_string());

    let c = *a;
    let d = *b;   // String 是移动语义,解引用 Box 会导致 String 的转移,转移后 b 不再可用。

    println!("a = {:?}", a);
    // println!("b = {:?}", b); // borrow of moved value: `b`
    println!("d = {:?}", d);
}

这种对 Box<T>使用操作符 (*) 进行解引用而转移所有权的行为,被称作 解引用移动,目前支持此行为的智能指针只有 Box<T>。 (Rc<T> 和 Arc<T> 不支持解引用移动)

use std::rc::Rc;
fn main() {
    let x = Rc::new(45);
    let x1 = *x;    // 解引用复制,允许

    let a = Rc::new("aa".to_string());
    // let b = *a;  // 解引用移动,不允许
    let b = &*a;    // Rc只能 解引用只读借用
}

共享所有权 Rc<T> 和 Weak<T>

引用计数 (reference counting) 是最简单的 GC 算法之一。 Rust 中提供了 Rc<T> 智能指针来支持引用计数,但不同于 GC,Rust是确定性的析构,开发者(编译器)知道资源什么时候会被析构。

Rust 中只有拥有所有权才能释放资源, Rc<T> 可以将多个所有权共享给多个变量。每当共享一个所有权时,计数就会增加一次;当共享变量离开作用域时,计数就减少一次。当计数为零时,该值才会被析构。

Rc<T> 是单线程引用计数指针,不是线程安全的类型,Rust 也不允许它被传递或共享给别的线程。

use std::rc::Rc;
fn main() {
    let x = Rc::new(45);
    let y1 = x.clone();    // 增加引用计数
    let y2 = x.clone();    // 增加引用计数
    println!("x_strong_count = {:?}", Rc::strong_count(&x));

    let w = Rc::downgrade(&x);    // 增加弱引用计数
    println!("{}", Rc::weak_count(&x));

    let y3 = &*x;   // 不增加引用计数。45i32是复制语义,也可以 *x 。
    println!("100 - *x = {}", 100 - y3);

    let x = Rc::new("hello".to_string());
    let y = &*x;
    println!("x = {:?}", y);
}

通过 clone 方法共享的引用所有权被称为 强引用。 通过 downgrade 方法创建了另一种智能指针类型 Weak<T>,也是引用计数指针。它共享的指针没有所有权,所以被称为 弱引用

内部可变性 Cell<T> 和 RefCell<T>

Rust 中的可变或不可变主要是针对一个变量绑定而言的,比如对于结构体来说,可变或不可变只能对其实例进行设置,而不能设置单个成员的可变性。

实际的开发应用中,某个字段是可变而其他字段不可变的情况确实存在。 Rust 提供了 Cell<T> 和 RefCell<T> 来对应这种情况。

它们本质上不属于智能指针,只是可以提供内部可变性的容器。

Cell<T>

内部可变性实际上是 Rust 中的一种设计模式。内部可变性容器是对 Struct 的一种封装,表面不可变,但内部可以通过某种方法来改变里面的值。

use std::{cell::Cell};

fn main() {
    let mut foo1 = Foo {
        x: 3,
        y: Cell::new(7),
    };
    println!("foo1 = {:?}", foo1);
    foo1.x = 5;
    foo1.y = Cell::new(9);
    println!("foo1 = {:?}", foo1);

    let foo2 = Foo {
        x: 5,
        y: Cell::new(9),
    };

    println!("foo2.y = {:?}", foo2.y.get());
    foo2.y.set(10);
    println!("foo2.y = {:?}", foo2.y.get());
}

#[derive(Debug)]
struct Foo {
    x: u32,
    y: Cell<u32>,
}

Cell<T> 提供的 set/get 方法像极了 OOP的 setter/getter 方法。实际上,Cell<T> 包裹的 T 本身合法的避开了借用检查。

RefCell<T>

RefCell<T> 适用的范围个更广,对类型 T 并没有 Copy 的限制。

use std::{borrow::Borrow, cell::RefCell};

fn main() {
    let x = RefCell::new(vec![1, 2, 3, 4]);
    println!("x.borrow() = {:?}", x.borrow());

    x.borrow_mut().push(5);
    println!("x.borrow() = {:?}", x.borrow());
}

Cell<T> 和 RefCell<T> 使用最多的场景就是配合只读引用来使用,如 Rc<RefCell<T>>。

Cell<T> 和 RefCell<T> 的区别

  • Cell<T> 使用 set/get 方法直接操作包裹的值,RefCell<T> 通过 borrow/borrow_mut 返回包装过的引用 Ref<T> 和 RefMut<T> 来操作包裹的值。

  • Cell<T> 一般适合复制语义类型(实现了 Copy),RefCell<T> 一般适合移动语义类型(未实现 Copy)。

  • Cell<T> 无运行时开销,并且永远不会在运行时引发 panic 错误。RefCell<T>需要在运行时执行借用检查,发现违反借用的情况,会引发现场 panic 而退出当前线程。

写时复制 Cow<T>

写时复制 (Copy on Write) 技术是一种程序中的优化策略,应用于多种场景。比如 Linux 中父进程创建子进程时,此种 “拖延” 策略减少了开销。

Rust 根据这种思维,提供了 Cow<T> 容器。Cow<T> 是一个枚举体的智能指针,包括两个可选值:

  • Borrowed,用于包裹引用。
  • Owned,用于包裹所有者。

Cow<T> 提供的功能是,以不可变的方式访问借用内容,以及在需要可变借用或所有权的时候再 克隆(clone)一份数据。

  • Cow<T> 实现了 Deref,这意味着可以直接调用其包含数据的不可变的方法。
  • Cow<T> 旨在减少复制操作,提高性能,一般用于读多写少的场景。
use std::borrow::Cow;

fn main() {
    // 没有可变需求,也不会克隆
    let s1 = [1, 2, 3];
    let mut cow1 = Cow::from(&s1[..]);
    abs_all(&mut cow1);
    println!("in_01 = {:?}", s1);
    println!("ot_01 = {:?}", cow1);

    // 有可变需求,所以会克隆。
    // 注意:借用数据被克隆成了新的对象
    // s2 != cow2 ,s2 不可变,也不会被改变
    let s2 = [1, 2, 3, -45, 5];
    let mut cow2 = Cow::from(&s2[..]);
    abs_all(&mut cow2);
    println!("in_02 = {:?}", s2);
    println!("ot_02 = {:?}", cow2);

    // 这里不会克隆,因为数据本身拥有所有权
    // 注意: v1 本身就是可变的
    let mut v1 = Cow::from(vec![1, 2, -3, 4]);
    abs_all(&mut v1);
    println!("v1 = {:?}", v1);

    // 没有可变需求,所以没有克隆
    let s3 = [1, 3, 5, 6];
    let sum1 = abs_sum(&s3[..]);
    println!("sum1 = {}", sum1);

    // 这里有可变需求,因此发生了克隆
    let s4 = [1, -3, 5, -6];
    let sum2 = abs_sum(&s4[..]);
    println!("sum2 = {}", sum2);
}

fn abs_all(input: &mut Cow<[i32]>) {
    for i in 0..input.len() {
        let v = input[i];
        if v < 0 {
            input.to_mut()[i] = v * (-1);
        }
    }
}

fn abs_sum(slice: &[i32]) -> i32 {
    let mut cow = Cow::from(slice);
    abs_all(&mut cow);
    let sum = cow.iter().fold(0, |acc, &n| acc + n);
    return sum;
}

在使用Cow的时候,注意一下几个要点:

  • Cow<T> 实现了 Deref,所以可以直接调用 T 的不可变方法。

  • 在需要修改 T 时,可以使用 to_mut 方法来获取可变借用。改方法会产生克隆,但仅克隆一次,如果多次调用,则只会使用第一次的克隆对象。如果 T 本身拥有所有权,则此时调用 to_mut 不会发生克隆。

  • 在需求修改 T 时,也可以使用 into_owned 方法来获取一个拥有所有权的对象。如果 T 是借用类型,这个过程会发生克隆,并创建新的所有权对象。如果 T 是所有权对象,则会将所有权转移到新的克隆对象。

Cow<T> 的另一个用处是统一实现规范。// 比如?