从 Trait 的翻译说起

日在知乎闲逛时,和某位抱怨《Rust 编程之道》的答主起了争执:她认为此书将 Trait 翻译成「特型」是极糟糕的译法,足以和鲁棒性 (Robust) 相提并论。今天笔者打算借题发挥,谈谈对 Trait 的看法,希望能起到抛砖引玉的效果。

笔者没读过《Rust 编程之道》,本文不涉及对此书的评价。

翻译问题

为什么「鲁棒性」会招致如此多的反对?概因 Robust 本可信达雅地翻译成「稳健性」,译者却偏偏选择了令人费解的「鲁棒性」,平白增加了理解门槛,default 翻译成「缺省」也是同样的问题。相较之下,Trait 的翻译要有争议得多:

首先,Trait 应不应该翻译?这个问题见仁见智。对于知乎回答、评论随笔等场合,直接使用英文似乎更好;但考虑到 Trait 是一个常用的专有术语,对于教科书和中文期刊,有一个合适的中文表述是非常重要的。

其次,Trait 应该如何翻译?Trait 一词的原意是特质、特性和特征,Rust 社区则倾向于译为「特征」。问题是,特征的语感不好,「高阶特征约束」读起来有些拗口,笔者更推荐 Scala 社区的译法⸺「特质」。

虽然术语 Trait 最早来自 Scala,但 Rust 的 Trait 更接近 Haskell 的 Typeclass。如果说 type/class 是对 value/object 的抽象,那么 trait/interface 就是对 type/class 的抽象:类型是一组值的共有属性,特质则是一组类型的共有属性。所以类型可以用来约束变量和形参,而特质可以用来约束泛型。具体到 Rust 语法,若 : 左边是变量名,右边是它必须满足的类型;若 : 左边是泛型名,右边是它必须实现的特质。

这样翻译有利于初学者在类型、泛型和 Trait 之间建立联系,还便于理解 Trait Object,因为特质对象就是把特质当成类型,直接用特质约束变量。

泛型

想必你已经发现,泛型就像一个变量或形参,只不过它对应的是类型而非值。为了区分,我们把泛型名称为泛型参数,把形参称为值参数。下例中,值参数 v 是传入值的标识符,泛型参数 Tv 类型的标识符,函数调用时可以通过 Turbofish 语法指明 T 的具体「值」:

1
2
3
4
5
6
7
8
fn foo<T: Clone>(v: T) -> T {
    let y = v.clone();
    y
}

fn main() {
    println!("{}", foo::<usize>(128));
}

foo::<usize>(128) 调用把 usize 传给泛型参数 T,把 128 传给值参数 x。这在 Zig 中更加清晰明了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const std = @import("std");

fn foo(comptime T: type, v: T) T {
    const y = v;
    return y;
}

pub fn main() !void {
    // 函数调用时传入的两个实参为 usize 和 128
    const result = foo(usize, 128);
    std.debug.print("{d}\n", .{result});
}

Zig 视类型为一等公民,允许类型像值一样被传递。从这个角度看,Zig 的语法一致性比 Rust 好,泛型的实现方式也更优雅。

组合优于继承

C++、Java 的继承,实际上是类 (Class) 或类型 (Type) 的继承:子类继承了父类的数据和方法。Rust 倾向于另一种抽象方式:为类型实现不同的特质。这是一种组合而非继承的思路:我们不再创建一颗「动物」继承树,而是为猫、狗、鸟等类型分别实现发声、行走、飞行等特质。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 发声特质
trait MakesSound {
    fn make_sound(&self) -> String;
}

// 飞行特质
trait CanFly {
    fn fly(&self);
}

struct Duck;

impl MakesSound for Duck {
    fn make_sound(&self) -> String {
        "quack".to_string()
    }
}

impl CanFly for Duck {
    fn fly(&self) {
        println!("The duck is flying!");
    }
}

struct Car;

// 汽车不会飞,但可以鸣笛
impl MakesSound for Car {
    fn make_sound(&self) -> String {
        "honk".to_string()
    }
}

通过组合特质,我们可以自由地为任何类型添加行为,而无需将它们塞进一个僵硬的继承体系中。鸭子会飞会叫,汽车只会鸣笛,这在基于继承的系统中很难优雅地建模。总不能让 Car 继承 Animal 吧?

组合解决了继承的两个经典问题:

  1. 脆弱基类问题 (Fragile Base Class Problem):在深度继承体系中,修改基类(父类)的一个实现细节,可能会意外地破坏其所有子类的行为。而 Trait 的实现是附加到类型上的,类型与 Trait 之间是松耦合的,修改 Trait 的默认实现或具体类型的实现,影响范围更可控。
  2. 钻石问题 (Diamond Problem):当一个类试图从两个拥有共同基类的父类进行多重继承时,就会产生歧义。例如,扫描仪和打印机都继承自 USB 设备,那么一个多功能一体机同时继承扫描仪和打印机时,它应该继承哪一份 USB 设备的状态和行为?Rust 的 Trait 从根本上避免了这个问题,因为 Trait 只定义行为,不包含数据(状态),类型可以实现任意多个 Trait 而不会产生冲突。

当然,Trait 也提供了默认实现,这让我们可以在多个类型间共享代码,获得部分继承的好处,同时又保持了组合的灵活性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
trait Greet {
    // 拥有默认实现的方法
    fn greet(&self) {
        println!("Hello!");
    }
}

struct Person;
impl Greet for Person {} // 使用默认实现

struct Robot;
impl Greet for Robot {
    // 也可以重写默认实现
    fn greet(&self) {
        println!("01001000 01100101 01101100 01101100 01101111 00100001");
    }
}

结论

理解 Trait 的关键,在于将它和类型、值参数、泛型参数联系起来,从「类型的类型」和「行为的组合」这两个维度去思考。下次当你在 Rust 代码中看到 : 时,不妨想想它左右两边的关系:无论是 variable: type 还是 Generics: Trait,都在描绘一种约束和归属,这正是类型系统的魅力所在。

点击刷新