LOADING

加载过慢请开启缓存 浏览器默认开启

Rust 系统编程(二):动态分发 - 从 Box 说起

Rust 语言提供了前所未有的保护机制,其类型检查严格堪称举世无双,开发人员在使用 Rust 语言的时候能够得到优雅的可读性,但是也受到 Rust 高度抽象的烦恼。Rust 系统编程系列文章旨在深入探索 Rust 的特性,从 Rust 本身的设计哲学的视角中去理解 Rust 的细节。

本文延续上一节的内容,但将目光移向 rust 的核心库和标准库,从 Box 的定义初步认识 Rust 动态分发的优雅。

Rust 系统编程系列:

请注意本文不是 Rust 动态分发的教程,而是探讨一些其中的细节问题。


在 Rust 中,不像 C++,new 已经不再是关键字了。而动态内存分配则使用了一个全新的类型 Box<T,A>,它将堆上分配的内存指针进行封装,形成一种类似智能指针的类型,有点像 C++ 后来提出的 unique_ptr。它的完整定义应该是:

struct Box<T: ?Sized, A: Allocator = Global>(Unique<T>, A);

在这里,Unique<T> 代表了一个独占所有权的指针,这保证了 Box 确实是拥有了变量 T 的。而这个 Unique 的完整定义是:

struct Unique<T: ?Sized>{
    pointer: NonNull<T>,
    _marker: PhantomData<T>,
}

很有趣。这个 Unique 仍然不是一个裸指针定义的,它里面还套了娃 NonNull<T>NonNull 实际上保证的是指针不为空,毕竟众所周知空指针十分危险,并且在 C/C++ 当中似乎并没有严格对它进行约束,而是由程序员来保证指针使用安全,这是不符合 rust 设计哲学的,因而又定义了专门的类型 NonNull 来对空指针进行约束,形成了一种繁琐而不失规整的、属于 rust 独有的浪漫与优雅。当然这里面还涉及到了幽灵数据 PhantomData<T>,笔者此时仍然没有完全搞清楚它的使用场合,此处不做讨论,但是明确一点:幽灵数据不占用空间,这是一种零成本抽象

struct NonNull<T: ?Sized>{
    pointer: *const T,
}

芜湖!我们的裸指针终于出现了,这个时候我们知道其实 Unique 就是一个指针,没有什么特别的,只不过它有着一些方法和特质来保证所指对象存在并且掌握所有权。

嗯……虽然我们的主题是动态分发,但是我在这里说明 Box 的定义主要是为了引出下面这一个神奇的语法:

struct StrA;
trait TrtB;
impl TrtB for StrA;
let box: Box<dyn TrtB> = Box::new(StrA);

我们可以坚信 StrAdyn TrtB 绝不是同一个类型,并且你绝不会能直接定义出具有所有权的 dyn TrtB 的 trait 对象,例如,下面的定义是过不了编译的:

let a: dyn TrtB = StrA as dyn TrtB;
// rust analyzing error:
//   cast to unsized type: `dyn TrtB`
//   cast to unsized type: `StrA` as `dyn TrtB`
//   consider using a box or reference as appropriate

这就奇怪了,凭什么 Box<dyn TrtB> 是可通过编译的?也许有人一下子就看出来了,笔者在前面说了那么多的 Box 内部结构,就是为此做了铺垫。Box 内部保存的并不是 dyn TrtB 对象本身,而是一个裸指针,这意味着,在泛型封装下,看起来像是 StrA as dyn TrtB 的一个隐式类型转换,实际上内部实现的却是 *const StrA as *const dyn TrtB。是的,Box 也没有那么神秘,但是用了一种很优雅的方式将 dyn TrtB 封装了起来并且让它看上去就是一个 trait 对象本身一样。通过这种封装,我们可以通过定义不同的接口来保证裸指针的使用正确,让所有不安全的性质在封装下得以安全,这就是 rust 的设计哲学。

不过,Rust 的类型安全设计并没有那么简单,它使用了一系列的 trait 来保证各种场景下的类型转换安全,我们将在后续的文章中看到,这种转换不是编译器内部规定的,而是 Rust 核心库定义的,当然我们说这里面有编译器的参与,但是作为核心库中的一种 trait,它让我们可以去自定义类型的隐式转换。这里十分建议去阅读核心库的代码,里面的注释也十分的详细(或者看文档也行,和注释的内容是一样的)。后续的讨论中会包括这些隐式转换或者强制转换在核心库中的注释解释,旨在深入地理解这些 trait 的含义、原理和用法。注意,Rust 将这一系列的安全保证抽象为了各种运算符,这些运算符都是 trait,通过实现 trait 来做到运算符的重载。也就是说,这些转换被认为是一种 operation,因而它的代码在 core::ops 当中。