近日在知乎闲逛时,和某位抱怨《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 是传入值的标识符,泛型参数 T 是 v 类型的标识符,函数调用时可以通过 Turbofish 语法指明 T 的具体「值」:
| |
foo::<usize>(128) 调用把 usize 传给泛型参数 T,把 128 传给值参数 x。这在 Zig 中更加清晰明了:
| |
Zig 视类型为一等公民,允许类型像值一样被传递。从这个角度看,Zig 的语法一致性比 Rust 好,泛型的实现方式也更优雅。
组合优于继承
C++、Java 的继承,实际上是类 (Class) 或类型 (Type) 的继承:子类继承了父类的数据和方法。Rust 倾向于另一种抽象方式:为类型实现不同的特质。这是一种组合而非继承的思路:我们不再创建一颗「动物」继承树,而是为猫、狗、鸟等类型分别实现发声、行走、飞行等特质。
| |
通过组合特质,我们可以自由地为任何类型添加行为,而无需将它们塞进一个僵硬的继承体系中。鸭子会飞会叫,汽车只会鸣笛,这在基于继承的系统中很难优雅地建模。总不能让 Car 继承 Animal 吧?
组合解决了继承的两个经典问题:
- 脆弱基类问题 (Fragile Base Class Problem):在深度继承体系中,修改基类(父类)的一个实现细节,可能会意外地破坏其所有子类的行为。而 Trait 的实现是附加到类型上的,类型与 Trait 之间是松耦合的,修改 Trait 的默认实现或具体类型的实现,影响范围更可控。
- 钻石问题 (Diamond Problem):当一个类试图从两个拥有共同基类的父类进行多重继承时,就会产生歧义。例如,扫描仪和打印机都继承自 USB 设备,那么一个多功能一体机同时继承扫描仪和打印机时,它应该继承哪一份 USB 设备的状态和行为?Rust 的 Trait 从根本上避免了这个问题,因为 Trait 只定义行为,不包含数据(状态),类型可以实现任意多个 Trait 而不会产生冲突。
当然,Trait 也提供了默认实现,这让我们可以在多个类型间共享代码,获得部分继承的好处,同时又保持了组合的灵活性。
| |
结论
理解 Trait 的关键,在于将它和类型、值参数、泛型参数联系起来,从「类型的类型」和「行为的组合」这两个维度去思考。下次当你在 Rust 代码中看到 : 时,不妨想想它左右两边的关系:无论是 variable: type 还是 Generics: Trait,都在描绘一种约束和归属,这正是类型系统的魅力所在。