LOADING

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

Rust 系统编程(四):self 语法糖 - Receiver

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

本文继续研究 Rust 的动态分发,探讨如何自定义一个封装类型的 self 语法糖,体现 Rust 的优雅与灵活的兼并。

Rust 系统编程系列:

如果不想深入探讨 Rust 的设计美学,可以跳转到实例代码快速理解:


我相信许多人对 self 语法糖并不陌生,特别是对编写类脚本语言(Python、ECMAScript等)的开发者来说(我在这里说是“类脚本语言”实际上并不是某个通用的说法,只是我不敢确定这些语言是否是脚本语言罢了)。因而 Rust 引入了这个语法糖并不奇怪,我们确实能尝到些许“甜头”!但在类脚本语言中,由于类型限制不是那么的强,通常我们也不关心这个 self 实际对应什么类型,然而,在 Rust 中这是不可能的,因为 Rust 可以说是有史以来的最强类型语言。实际上,相当一部分开发者接触过这些常见的 self 语法糖:

struct S;
impl S {
    fn foo1(self: Self);
    fn foo2(self: &Self);
    fn foo3(self: &mut Self);
    fn foo4(self);
    fn foo5(&self);
    fn foo6(&mut self);
}

实际上,在我大量的查阅一些 Rust 语言参考的过程中,发现这个语法糖实际上有个较为正式的名称:receiver。也许我们可以把它翻译为接收器。为什么叫这个名字呢?笔者认为这和 Rust 的语法特点有关:在 Rust 中,结构体的数据定义和方法定义一定是分开的,这个语法糖就好像是一个方法接受到了一个对象一样,只不过这个对象的类型是 Self 而已。

也许我们平时并不在意这个语法糖的使用细节,因为它用起来是那么地自然,就如同我们吃糖一样甜味自然而然地就进嘴了。但是,你是否使用过下面的 self 语法糖呢?

struct SS;
impl SS {
    fn bar1(self: Box<Self>);
    fn bar2(self: Rc<Self>);
    fn bar3(self: Arc<Self>);
    fn bar4(self: Pin<&Self>);
}

很神奇的语法对吧。第一眼看过去你甚至不知道怎么使用这些方法。实际上,SS 是无法调用这些方法的。嗯?不对吧,我们明明是将这些方法写在 impl SS 当中的,如果 SS 对象不能调用,那还有谁能调用呢?对咯,bar1 需要 Box<SS> 才能调用,bar2 需要 Rc<Self> 才能调用,剩下的类似。诶?这不就相当于我们给 Box<T> 实现了一个泛型特化 impl Box<SS> 吗?从结果来看,确实如此。这种语法糖让我们可以给 Box 这种封装器实现外部的泛型特化,但又保留原来的 Box 定义不变,这种语法确实很神奇。

更神奇的是,这种语法糖并不是上述的 BoxRcArcPin 才能做到,实际上有一个内建的 trait Receiver 用于允许封装器能够以这样的语法糖调用,并且这个 trait 被视为一种运算方法(类似加减乘除也有对应的 trait),相当于以运算符重载的方式来让这种语法糖生效。

例(1) Receiver 的使用

对于 Receiver,在核心库注释中给出的解释是:用于表示一个结构体可以被用于一个方法的 receiver,也就是说,一个类型可以用这个类型作为 self 的类型,就像这样:

use core::ops::Receiver;

struct SmartPointer<T>(T);

impl<T> Receiver for SmartPointer<T> {
    type Target = T;
}

struct MyContainedType;

impl MyContainedType {
    fn method(self: SmartPointer<Self>) {
        // ...
    }
}

fn main() {
    let ptr = SmartPointer(MyContainedType);
    ptr.method();
}

不过,使用 Receiver 有一个前提,需要开启一个不稳定的 feature:arbitrary_self_types。实际上,参阅核心库时,你会发现一个相似的 trait LegacyReceiver,它们之间唯一的区别是,是否存在关联类型 Target。不过 LegacyReceiver 会在将来被弃用,而 arbitrary_self_types 也在将来会变得稳定,因而 Receiver 显然更为安全。

关于 Receiver 中的关联类型 Target,笔者目前没有发现它的用处,也许你能在核心库提到的相关问题 44874 的讨论中得到答案,这里不再深入了。(下面的例(2)解释了 Target 的用处) 不过经过笔者实验可以肯定的是,这个 Target 并不会将自定义类型解引用成 T,这意味着它不具有类似于 Deref<T> 的行为,但是实际上实现 Deref<T> 的结构体会自动实现它。核心库的注释中也提到了:所有实现了 Deref 的结构体都被空白地实现了这个 trait,因此你很少会使用它(因为使用 Deref 就够了)。如果你需要实现一个不能实现 Deref 的智能指针时,你才会去使用它;这种情况也许是因为你遇到了其他编程语言并且不能保证引用可以遵守 Rust 的别名规则。

例(2) Receiver 的链式解释

需要注意的一点是,核心库的注释最后还提到了编译器对 Receiver 的解释方法。这里必须要区别解引用 Deref,因为看起来 Receiver 是将一个类型解释为关联类型 Target,但实际上并不会发生编译期的解引用,也就是说,你不能看做是一种别名来调用 Target 的方法,你调用的仍然是外部的封装类型。但这个 Target 会被链式地检查,即关联类型 Target 也是一个实现了 Receiver 的类型的话会递归地进行解释直至出现最末尾的没有实现的 Receiver 的 Target 为止,最后解释出来的类型会代替最开始的封装类型而检查合法性。最后解释出来的类型必须是 Self 或者 Self 的引用或指针(如果是指针的话,需要开启 arbitrary_self_types_pointers 特性),这意味着,实际上 Box<T> 也是如此工作的,最后会被解释为 &T,从而符合 self 语法糖最起初的规则。核心库中也给了这种链式解释的例子:

use std::boxed::Box;
use std::rc::Rc;

// Both `Box` and `Rc` (indirectly) implement Receiver
struct MyContainedType;

fn main() {
    let t = Rc::new(Box::new(MyContainedType));
    t.method_a();
    t.method_b();
    t.method_c();
}

impl MyContainedType {
    fn method_a(&self) {}
    fn method_b(self: &Box<Self>) {}
    fn method_c(self: &Rc<Box<Self>>) {}
}

结语

概括一下 Receiver 的作用:这个 trait 提供一种标记,让编译器认为这种类型作为其他类型方法的 receiver 是合法的。 这里我们可以看出 receiver 这种语法比起传统面向对象语言的对象调用方法更具有灵活性,它使得一个类型的方法可以拥有外部实现,而 Receiver 则告诉编译器这种外部实现是合法的。