近日在知乎闲逛时,和某位抱怨《Rust 编程之道》的答主起了争执:她认为此书将 Trait 翻译成“特型”是极糟糕的译法,足以媲美遗臭万年的鲁棒性(Robust),而笔者对此有不同意见。今天借这个话题谈谈笔者的一些思考,希望能起到抛砖引玉的效果。
笔者没读过《Rust 编程之道》,因此本文不涉及对此书的评价。
翻译问题
为什么“鲁棒性”会招致如此多的反对?概因 Robust 本可信达雅地翻译成“稳健性”,译者却偏偏选择了令人费解的“鲁棒性”,平白增加了理解门槛。default 翻译成“缺省”也是同样的问题。相较之下,Trait 的翻译要有争议的多:
首先,Trait 应不应该翻译?这个问题见仁见智。对于评论留言等场合,直接使用英文也无伤大雅;但对于教科书和文章,使用中文更显正式。考虑到 Trait 是一个常用的专有术语,有一个合适的中文表述是非常重要的。
其次,Trait 应该如何翻译?Trait 一词的原意是特质、特性和特征,中文社区似乎倾向于译为“特征”。问题是特征的语感不好,“高阶特征约束”读起来就不怎么顺口。如果一定要从中选一个译名,我推荐学习 Scala 官方文档,将 Trait 译为“特质”。
虽然术语 Trait 来自 Scala,但 Rust 的 Trait 更接近 Haskell 的 Typeclass:如果说 type/class 是对 value/object 的抽象,那么 trait(typeclass)/interface 就是对 type/class 的抽象。类型可以视为一组值的集合,特型则可以视为一组类型的集合。所以 trait 可以用来约束泛型,而类型可以用来约束变量(和函数参数)。具体来说,Rust : 左边是泛型名,右边是它的特型;: 左边是变量名,右边是它的类型。
泛型参数对应的其实是变量或形参,变量 v 是调用时具体值的标识符,泛型 T 是为给调用时确定的具体类型起了一个标识符。例如,调用泛型函数时可以通过 Turbofish 语法指明泛型参数 T 的具体“值”:
| |
foo::<usize>(128) 调用把 usize 传给泛型参数 T,把 128 传给值参数 x。这在类型为一等公民的 Zig 中更明显:
| |
“特型”指一组具有指定特点的类型,“特型”可以理解为“更高维”的类型,注意笔者不是在说 HKT。这样翻译有利于初学者在类型、泛型和 Trait 之间建立联系,还便于理解 Trait Object,因为特型对象就是把特型当成类型用,直接用特型约束变量(对象),这和 Go 的 Interface 类似。
组合优于继承
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 核心编程思想的理解。“特型”这个词,强调了 Trait 是对类型(Type)的抽象和约束,是构建泛型和实现多态的关键。它引导我们从“类型的类型”和“行为的组合”这两个维度去思考,从而更好地运用 Rust 提供的强大工具,编写出灵活、安全且高效的代码。
下次当您在 Rust 代码中看到 : 时,不妨想想它左右两边的关系:无论是 value: Type 还是 T: Trait,都在描绘一种约束和归属。而这,正是类型系统的魅力所在。