Skip to content

5.3 扩展方法

我们还可以利用 trait 给其他的类型添加成员方法,哪怕这个类型不是我们自己写的。比如,我们可以为内置类型 i32 添加一个方法:

rust
trait Double {
    fn double(&self) -> Self;
}

impl Double for i32 {
    fn double(&self) -> i32 { *self * 2 }
}

fn main() {
    // 可以像成员方法一样调用
    let x : i32 = 10.double();
    println!("{}", x);
}

这个扩展的功能就像 C# 里面的“扩展方法”一样。哪怕这个类型不是在当前的项目中声明的,我们依然可以为它增加一些成员方法。但我们也不是随随便便就可以这么做的,Rust 对此有一个规定。

在声明 trait 和 impl trait 的时候,Rust 规定了一个 Coherence Rule(一致性规则)或称为 Orphan Rule(孤儿规则):impl 块要么与 trait 的声明在同一个的 crate 中,要么与类型的声明在同一个 crate 中。

也就是说,如果 trait 来自于外部 crate,而且类型也来自于外部 crate,编译器不允许你为这个类型 impl 这个 trait。它们之中必须至少有一个是在当前 crate 中定义的。因为在其他的 crate 中,一个类型没有实现一个 trait,很可能是有意的设计。如果我们在使用其他的 crate 的时候,强行把它们“拉郎配”,是会制造出 bug 的。比如说,我们写了一个程序,引用了外部库 lib1 和 lib2,lib1 中声明了一个 trait T,lib2 中声明了一个 struct S,我们不能在自己的程序中针对 S 实现 T。 这也意味着,上游开发者在给别人写库的时候,尤其要注意,一些比较常见的标准库中的 trait,如 Display Debug ToString Default 等,应该尽可能地提供好。否则,使用这个库的下游开发者是没办法帮我们把这些 trait 实现的。

同理,如果是匿名 impl,那么这个 impl 块必须与类型本身存在于同一个 crate 中。

更多关于“一致性规则”的解释,可以参见编译器的详细错误说明:

rust
rustc --explain E0117
rustc --explain E0210

当类型和 trait 涉及泛型参数的时候,一致性规则实际上是很复杂的,用户如果需要了解所有的细节,还需要参考对应的 RFC 文档

许多初学者会用自带 GC 的语言中的“Interface”、抽象基类来理解 trait 这个概念,但是实际上它们有很大的不同。

Rust 是一种用户可以对内存有精确控制能力的强类型语言。我们可以自由指定一个变量是在栈里面,还是在堆里面,变量和指针也是不同的类型。类型是有大小(Size)的。有些类型的大小是在编译阶段可以确定的,有些类型的大小是编译阶段无法确定的。目前版本的 Rust 规定,在函数参数传递、返回值传递等地方,都要求这个类型在编译阶段有确定的大小。否则,编译器就不知道该如何生成代码了。

而 trait 本身既不是具体类型,也不是指针类型,它只是定义了针对类型的、抽象的“约束”。不同的类型可以实现同一个 trait,满足同一个 trait 的类型可能具有不同的大小。因此,trait 在编译阶段没有固定大小,目前不能直接使用 trait 作为实例变量、参数、返回值

有一些初学者特别喜欢写这样的代码:

rust
let x: Shape = Circle::new(); // Shape 不能做局部变量的类型
fn use_shape(arg : Shape) {}  // Shape 不能直接做参数的类型
fn ret_shape() -> Shape {}    // Shape 不能直接做返回值的类型

这样的写法是错误的。请一定要记住,trait 的大小在编译阶段是不固定的。那怎样写才是对的呢?后面我们讲到泛型的时候再说。

Released under the MIT License