15智能指针

指针(pointer)是一个包含内存地址的变量的通用概念。

  • 这个地址,引用或指向(points at)一些其他数据
  • Rust 中最常用的指针是引用(reference)。
    • &符号为标志并借用它们所指向的值。
    • 引用除了借用数据没有任何其他功能,也没有额外开销。

智能指针(smart pointers)是一类数据结构

  • 表现与指针类似;
  • 但拥有额外的元数据和功能;
  • 起源于 C++,在其他语言中也存在;

普通引用与智能指针的一个额外区别:

  • 引用是一类只借用数据的指针;
  • 大部分情况下,智能指针拥有它们指向的数据的所有权

例如,StringVec<T> 都属于智能指针。

智能指针通常使用结构体实现

  • 与常规结构体的区别,实现了 DerefDrop 两个 trait
  • Deref trait 允许智能指针结构体实例像引用一样工作
    • 这样可以编写既用于引用、又用于智能指针的代码;
  • Drop trait 允许我们自定义当智能指针离开作用域时运行的代码;
    • 例如,Box<T> 类型实现了 Drop trait ,box 所指向的堆数据也会被清除。

所以:

智能指针是一类可以像引用一样工作的结构体。

标准库中最常用的智能指针:

  • Box<T>,用于在对上分配值;
  • Rc<T>,一个引用计数类型,其数据可以有多个所有者;
  • Ref<T>RefMut<T>,通过 RefCell<T> 访问,一个在运行时而不是在编译时执行借用规则的类型。

使用Box<T> 指向堆上的数据

box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。

使用场景

  • 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值得时候;
    • 例如,box 允许创建递归类型。
  • 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候;
    • 转移大量数据的所有权可能花费很长的时间,因为数据在栈上进行了拷贝,使用 box 可以将这些数据存储在堆上,然后只有少量指针数据在栈上被拷贝。
  • 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候;
    • 称为 trait对象(trait object),17章 有专门将这个主题。

使用 Box<T> 在堆上存储数据

如何创建和使用 Box<T>?

语法:

创建Box<T> 对象

Box::new(xxx) 要创建的具体类型数据

1
2
let b = Box::new(5); // 使用box 在堆上存储一个 i32 值;
println!("b = {}",b);

当b离开作用域时,它会被释放,box 本身(位于栈上)和它所指向的数据(位于堆上)。

Box 允许创建递归类型

Rust 需要在编译时知道类型占用多少空间。

递归类型(recursive type),其值得一部分可以是相同类型的另一个值。这种值得嵌套理论上可以无限的进行下去。

  • 从名字可以理解为与嵌套函数极为相似。

如果 Rust 中使用这种递归类型,那么 Rust 在编译期是不知道递归类型需要多少空间的。

解决:

box 有一个已知大小,所以可以通过在循环类型定义中插入 box,就可以创建间接存储的递归类型了。

cons list 的更多内容

cons list 是一个来源于 Lisp 语言及其方言的数据结构。

cons 函数(construction function 的缩写)利用两个参数构造一个新的列表,它们通常是一个单独的值和另一个列表。

  • 写法虽然不一样,但是作用与 Java 中的列表list 一样

cons list 的每一项都包含两个元素:

  • 当前项的值;
  • 下一项;
    • 最后一项值包含一个叫做 Nil 的值,并且没有下一项
    • 代表递归的终止条件(base case)的规范名称是 Nil,它表示列表终止(是枚举的一项)

注意:Rust 中需要列表的时候, Vec<T> 是一个更好的选择。

计算非递归类型的大小

Box 指向堆上数据,并且可确定大小 · Rust 程序设计语言(第二版) 简体中文版 (gitbooks.io)

使用 Box<T> 给递归类型一个已知的大小

Rust 无法计算出要为定义为递归的类型分配多少空间。

不同于直接存储一个值,我们可以通过 box 间接存储一个指向值得指针。

  • 因为 Box<T> 是一个指针,我们总是知道它需要多少空间:
    • 指针的大小并不会根据其指向的数据量而改变。
1
2
3
4
5
6
7
8
9
10
11
// 不对的方式:
enum List {
Cons(i32, List),
Nil,
}

// 使用 Box 的方式
enum List {
Cons(i32, Box<List>),
Nil,
}

例如上面例子,将 Box 放入 Cons成员中,而不是直接存放另一个 List值。

Box 会指向另一个位于堆上的 List 值,而不是存放在 Cons 成员中。

1
2
3
4
5
6
7
8
9
10
11
12
13
enum List {
Cons(i32, Box<List>),
Nil,
}

use crate::List::{Cons, Nil};

fn main() {
let list = Cons(1,
Box::new(Cons(2,
Box::new(Cons(3,
Box::new(Nil))))));
}

box 只提供了间接存储和堆分配;

  • Box<T> 实现了 Deref trait ,所以它允许 Box<T> 值被当做引用对待;
  • Box<T>实现了 Drop trait,所以当 Box<T> 值离开作用域时,box 所指向的堆数据也会被清除。

通过Deref trait 将智能指针当作常规引用处理

实现 Deref trait 允许我们重载解引用运算符(dereference operator)*(与乘法运算符或通配符相区别)。

通过这种方式实现 Deref trait 的智能指针可以被当做常规引用来对待,可以编写操作应用的代码并用于智能指针。

通过解引用运算符追踪指针的值

常规引用,是一个指针类型,

  • 可以看成指向存储在其他地方值得箭头。

解引用运算符(*) 的作用:

  • 追踪引用所指向的数据

这个知识点记住就可以了。

1
2
3
4
5
6
7
8
fn main() {
let x = 5;
let y = &x;
let y = Box::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
}

通过实现 Deref trait 将某类型像引用一样处理

Deref trait 由标准库提供,要求实现名为 deref 的方法,其借用 self 并返回一个内部数据的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::ops::Deref;

# struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
// 定义了用于此 triat 的关联类型。见19章
type Target = T;

// 提供了*运算符访问的值的引用
fn deref(&self) -> &T {
&self.0
}
}

没有 Deref trait 的话,编译器只会解引用 & 引用类型。

deref 方法向编译器提供了获取任何实现了 Deref trait 的类型的值,并且调用这个类型的 deref 方法来获取一个它知道如何解引用的 & 引用的能力。

  • 上面这句话拆分一下,能做两件事
    1. deref 方法让编译器可以获得任何实现了 Deref trait 的类型的值;
    2. 编译器可以调用这些类型的 deref 方法获得一个&引用类型(这样它就知道如何解引用了)。

示例 15-9

1
assert_eq!(5, *y);

上面这段代码,Rust 事实上在底层运行了如下代码:

1
*(y.deref())

– 我理解就是任何类型先调用它实现的 Deref trait 的 deref方法来获得一个 &引用类型,然后使用 *解引用运算符解引用,获取引用指向的数据。

官方解释:

Rust 将 * 运算符替换为先调用 deref 方法再进行普通解引用的操作,如此我们便不用担心是否还需手动调用 deref 方法了。Rust 的这个特性可以让我们写出行为一致的代码,无论是面对的是常规引用还是实现了 Deref 的类型。

deref 方法返回值的引用,以及 *(y.deref()) 括号外边的普通解引用仍为必须的原因在于所有权。如果 deref 方法直接返回值而不是值的引用,其值(的所有权)将被移出 self。在这里以及大部分使用解引用运算符的情况下我们并不希望获取 MyBox<T> 内部值的所有权。

注意,每次当我们在代码中使用 * 时, * 运算符都被替换成了先调用 deref 方法再接着使用 * 解引用的操作,且只会发生一次,不会对 * 操作符无限递归替换,解引用出上面 i32 类型的值就停止了,这个值与示例 15-9 中 assert_eq!5 相匹配

函数和方法的隐式解引用强制多态

解引用强制多态deref coercions)是 Rust 在函数或方法传参上的一种便利。

其将实现了 Deref 的类型的引用转换为原始类型通过 Deref 所能够转换的类型的引用。(有些拗口)

当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时,解引用强制多态将自动发生。这时会有一系列的 deref 方法被调用,把我们提供的类型转换成了参数所需的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
fn hello(name: &str) {}

// 解引用强制多态
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}

// 没有解引用强制多态的写法
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}

注:解引用强制多态分析发生在编译时,所以并没有运行时开销。

解引用强制多态如何与可变性交互

类似于如何使用 Deref trait 重载不可变引用的 * 运算符,Rust 提供了 DerefMut trait 用于重载可变引用的 * 运算符。

Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态:

  • T: Deref<Target=U> 时从 &T&U
  • T: DerefMut<Target=U> 时从 &mut T&mut U
  • T: Deref<Target=U> 时从 &mut T&U

前两种情况除了可变性之外是相同的:

  • 第一种情况,表明如果有一个 &T ,而 T 实现了返回 U 类型的 Deref ,则可以直接得到&U
  • 第二种情况表明对于可变引用也有着相同的行为。
  • 第三种情况有些微妙:Rust 也会将可变引用强转为不可变引用。但是反之不可能的。
    • 转换不能打破借用规则
    • 将不可变引用转换为可变引用则需要数据只能有一个不可变引用,而借用规则无法保证这一点。

使用 Drop Trait 运行清理代码

Drop trait 允许我们在值要离开作用域时执行一些代码。

  • 是对于智能指针模式第二重要的 trait

例如,Box<T> 自定义了Drop 用来释放 box 所指向的堆空间。

在 Rust 中,通过实现 Drop trait ,可以指定每当值离开作用域时被执行的代码,编译器会自动插入这些代码。

  • 这样系统就不会因为忘记清理或释放资源导致负载过重而崩溃。

注意:这个特性并不需要为每个类型实现,只有在你的类型需要自己的析构函数(destructor )逻辑时才需要实现它。

实现 Drop trait:

  • 要实现 drop 方法,它获取一个 self 的可变引用。

Drop trait 包含在 prelude 中,所以使用时无需导入它。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ToDrop;

impl Drop for ToDrop {
fn drop(&mut self) {
println!("ToDrop is being dropped");
}
}

fn main() {
let x = ToDrop;
println!("Made a ToDrop!");
}

通过 std::mem::drop 提早丢弃值

Drop 是Rust 自动调用的,Rust 不允许我们主动调用 Drop trait 的 drop 方法。

  • 我们不能禁用当值离开作用域时自动插入的drop,并且不能显示调用drop

当我们希望在作用域结束之前就强制释放变量的话,应该使用由标准库提供的 std::mem::drop

  • 例如,当使用智能指针管理锁时,你可能希望强制运行drop 方法来释放锁以便作用域中的其他代码可以获取锁;

std::mem::drop 也在 prelude 中。

1
2
3
4
5
6
fn main() {
let c = CustomSmartPointer { data: String::from("some data") };
println!("CustomSmartPointer created.");
drop(c); // 这里调用了 std::mem::drop 方法。
println!("CustomSmartPointer dropped before the end of main.");
}

Rc<T> 引用计数智能指针

背景:大部分情况下所有权是非常明确的,可以准确的世道哪个变量拥有某个值。但是,有些情况单个值可能会有多个所有者。例如,图数据结构中,点与边的关系。

  • 点从概念上讲为所有指向它的边所拥有。节点直到没有任何边指向它之前都不应该被清理;

  • 问题:Rust中所有权规则规定,一个值仅能有一个所有者,这里为什么会说有多个所有者?多个值使用不可以使用引用吗?一个所有权拥有者,其他使用引用。–问题是类似点与边的关系中,编译时无法知道谁最后离开,把所有权给谁?只有在运行时才能知道,最后一个离开后才能清除数据;

    • 所以我理解多所有权即谁都有资格拥有它,这样最后一个离开的人只要确定没有多余引用了,就可以将数据清除。
  • 例如,看电视,一人打开后,可以多人一起看,当最后一个人离开房间时,他关掉电视因为它不再被使用了。如果某人在其他人还在看的时候就关掉了电视,正在看电视的人肯定会抓狂的!

Rc<T> 类型,为引用计数(reference counting)的缩写。

  • 引用计数意味着通过记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代码没有任何有效引用并可以清理。

使用场景:

Rc<T> 用于当我们希望在堆上分配一些内存供程序的多个部分读取,但是无法在编译时确定程序的哪一个部分会最后结束使用它的时候。

  • 如果确定知道哪部分是最后一个结束使用的话,就可以令其成为数据所有者,正常的所有权规则就可以在编译时生效。
  • 多所有权,可以理解为是共享所有权。
  • 涉及生命周期问题

注意,Rc<T> 只能用于单线程场景;

多线程有自己的引用计数;

使用方式:

  1. 创建Rc<T>

    Rc::new();

  2. 增加 Rc 实例的引用计数;每次被其他变量使用时都只用下面这个方法

    例如:Rc::clone(&a)

  3. 查看当前Rc的强引用数

    Rc::strong_count(&a)

  4. Rc<T> 在与 RefCell<T>结合使用时,Rust 实现了自动解引用获取其中RefCell<T>

    1
    2
    3
    4
    let value = Rc::new(RefCell::new(5));
    *value.borrow_mut() += 10;
    // 1.首先,Rust 自动解引用,获取了Rc<T> 内部的 RefCell<T>
    // 2.然后,RefCell<T> 的 borrow_mut() 返回的是 RefMut<T> ,所以使用 * 解引用运算符后获取数值5

使用 Rc<T> 共享数据

1
2
3
4
5
6
7
8
9
10
11
12
13
enum List {
Cons(i32, Rc<List>),
Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}

如上的例子中,如果使用 Box<T>,则会报错,因为Box<a> 时会发生所有权的转移,

这里使用 Rc::clone(),只会增加 a 的引用计数;

Rc::clone() 区别于大部分类型的 clone

Rc::clone() 只会增加引用计数,不会花费多少时间;

而普通的 clone 则是深拷贝,则会花费很长时间;

所以当查找代码中的性能时,只需考虑深拷贝的clone,而无需考虑Rc::clone调用。

克隆 Rc<T> 会增加引用计数

Rc<T> 离开作用域时,因为实现了 Drop trait 所以会自动减少引用计数;

当引用计数为0时,会同时清理Rc

注意:通过不可变引用,Rc<T> 允许在程序的多个部分之间只读地共享数据,

  • 如果 Rc<T> 也允许多个可变引用,则会违反借用规则(相同位置的多个可变借用可能造成数据竞争和不一致)。

RefCell 与内部可变性模式

内部可变性Interior mutability)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 unsafe 代码来模糊 Rust 通常的可变性和借用规则。

通过 RefCell<T> 在运行时检查借用规则

对于引用和 Box<T> ,借用规则的不可变性作用于编译时;

  • 违反借用规则,得到一个编译错误;

对于RefCell<T>,借用规则的不可变性作用于运行时

  • 违反则会运行时触发panic并退出;

Rust 的编译器是天生保守的,为了保证安全,会拒绝编译器无法理解的代码,无论是否正确。

所以RefCell<T> 适用于,开发者确信代码遵守借用规则,而编译器不能理解和确定的时候。

Box<T>,Rc<T> 和 RefCell<T> 选择依据

Box<T> Rc<T> RefCell<T>
所有权 单一所有权 多所有权 单一所有权
检查时期 编译时 编译时 运行时
借用可变性 可变或不可变借用 不可变借用 可变或不可变借用

内部可变性 mock

测试替身(test double)是一个通用编程概念,它代表一个在测试中替代某个类型的类型。

mock 对象是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的。

RefCell<T> 在运行时记录借用

当创建不可变和可变引用时,我们分别使用 &&mut 语法。

对于 RefCell<T> 来说,则是 borrowborrow_mut 方法,这属于 RefCell<T> 安全 API 的一部分。

  • borrow 方法返回 Ref 类型的智能指针,
  • borrow_mut 方法返回 RefMut 类型的智能指针。
  • 这两个类型都实现了 Deref,所以可以当作常规引用对待。

RefCell<T> 记录当前有多少个活动的 Ref<T>RefMut<T> 智能指针。

每次调用 borrowRefCell<T> 将活动的不可变借用计数加一。当 Ref 值离开作用域时,不可变借用计数减一。

就像编译时借用规则一样,RefCell<T> 在任何时候只允许有多个不可变借用或一个可变借用。

结合 Rc<T>RefCell<T> 来拥有多个可变数据所有者

RefCell<T> 的一个常见用法是Rc<T> 结合

1
Rc<RefCell<i32>>  // 这样就可以得到有多个所有者并且可以修改的值了。

注意,因为 Rc<T> 只存放不可变值,所以一旦创建值后就不能修改。

加入 RefCell<T> 来获得修改其中值的能力。

注意: Rc<T>RefCell<T> 结合使用时,Rust会自动解引用获取其内部的值

引用循环与内存泄漏是安全的

内存泄漏(memory leak)是指永远也不会被清理的内存部分

Rc<T>RefCell<T> 结合很灵活但是也可以创建出引用循环的可能。

  • 这会造成内存泄漏,因为每一项的引用计数永远也到不了0,其值也永远也不会被丢弃。

只有所有权关系才能影响值是否可以被丢弃。

避免引用循环:将Rc<T> 变为 Weak<T>

弱引用(weak reference),无需计数为 0 就能使Rc 实例被清理。

强引用代表如何共享 Rc<T> 实例的所有权,但弱引用并不属于所有权关系。

  • 弱引用不会造成引用循环。因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断。

如何用 Weak<T> ?

调用Rc::downgrade 时,会得到Weak<T>类型的智能指针,Rc<T> 实例的弱引用计数(weak_count)会加1。

为了使用Weak<T>所指向的值,要保证其值仍然有效。可以调用 Rc::upgrade,会返回Option<Rc<T>> ,如果Rc<T> 的值还未被丢弃,则返回Some;如果已经被丢弃,则返回None

  • 因为返回是Option<T>,Rust 会处理 SomeNone 的情况,不会返回非法指针。
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信