本文作者刘祺,程序员,曾参与多篇外文学术文献的翻译工作。目前是图形图像程序员、科普作家、数学达人。
写这篇文章可谓机缘巧合——笔者前段时间参与了“肥蟹书”(《Rust程序设计(第2版)》)的中文译本审读。在这本书出版后,我留意到一部分读者对@雪狼(汪志成老师)把 trait 翻译成“特型”表示不理解。我想到 trait 不仅是在翻译上有一些争议,而且在用法上有一种未被普遍接受的“奇技淫巧”。以下就这个点跟各位交流一下。
我对 trait 翻译成“特型”的看法
索绪尔的《普通语言学教程》中某处引用了辉尼特的观点,即把语言看作一种社会制度,跟其他一切社会制度一样。索绪尔认为辉尼特的观点太绝对了,他长篇大论地说明了语言并不是在任何一点上都跟其他社会制度相同的社会制度。笔者认为,将 trait 按原样保留,已经成为了 Rust 开发者之间的共识。不过这些并不是我们这篇分享的重头戏,笔者就不在这里展开了。有兴趣的读者可以参考索绪尔的书。
就笔者简单粗暴的观点来说,应当参考 Rust API 文档中的关键字(Keywords)那一章节中对关键字的解释来给关键字对应的东西起名。如果这一节的对于中文使用者的帮助甚微,应当考虑依原文形式保留。可是这种观点亦有很大的问题,那就是笔者会管结构体叫 struct,管枚举叫 enum。虽然这样前后的翻译逻辑也是一致的,然而对于更多的读者来说,这可能比把 trait 翻译成“特型”更难以接受。于是在“肥蟹书”的审阅过程中,我接受汪老师站在初学者的立场上去思考从而将 trait 翻译成“特型”这一观点。
什么是重载,以及把 trait 用于运算符重载
和 C++ 不同的是,一提到 Rust 的重载,大家普遍想到的就是运算符重载。 以至于“肥蟹书”似乎默认知道大家已经了解了这一点,而只讲解了如何进行运算符重载的语法。
首先重载本身就是一种多态性的体现。说得更通俗一点,程序设计的过程中经常会遇到方法的标识符相同而参数的类型不同或者参数的个数不同的情况,这时候就是重载上场的时候。而我们又知道方法实际上也是一个函数,所以在 C++ 那类语言中,重载让人更多联想到的是函数重载,而非单指运算符重载。如果读者使用过 Clojure(一种在 JVM 上的 Lisp 方言)或者其他函数式语言,可能更容易理解实际上运算符也是一个函数这回事儿。因此,运算符重载实际上就是函数重载的一个特殊情况。
运算符重载的例子
为了方便演示为什么需要重载,让我们假设自己是一个科学计算库的作者。我们正在着手实现复数这一类型,那么我们要怎么做呢?首先可以定义一个 struct:
// 把实数定义为单精度浮点数是一种偷懒的写法,
// 真正的科学计算库通常不会这么做,这里笔者
// 疲于引入泛型或额外的 struct
type Real = f32;
#[derive(Copy, Clone)]
struct Complex {
real: Real,
imaginary: Real,
}
impl Complex {
fn new(real: impl Into<Real>, imaginary: impl Into<Real>) -> Self {
Complex {
real: real.into(),
imaginary: imaginary.into(),
}
}
}// 如果你不理解以下两段代码请阅读“肥蟹书”第220~221页
impl From<Real> for Complex {
fn from(real: Real) -> Complex {
Complex::new(real, Real::default())
}
}
impl std::fmt::Debug for Complex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let real_default = Real::default();
if self.imaginary == real_default {
write!(f, "{}", self.real)
} else if self.real == real_default {
write!(f, "{}i", self.imaginary)
} else if self.imaginary < real_default {
write!(f, "({} - {}i)", self.real, self.imaginary.abs())
} else {
write!(f, "({} + {}i)", self.real, self.imaginary)
}
}
}如果没有运算符重载这回事儿的话,我们可能要把复数加法写成:
impl Complex {
fn add(&self, other: impl Into<Self>) -> Self {
let temp = other.into();
Complex {
real: self.real + temp.real,
imaginary: self.imaginary + temp.imaginary,
}
}
}每次调用复数加法的时候就会像:
let foo = Complex::new(1.0, 2.0);
println!("{:?}", foo.add(1.0));笔者认为这将产生一种歧义,即我们到底是给 foo 增加了 1.0,还是把它们相加的结果作为一个新值返回了,这完全不明确。如果能够使用 + 和 += 这两个没有歧义的运算符就好了。所以,我们可以这样写:
impl<T> std::ops::Add<T> for Complex
where
T: Into<Complex>,
{
type Output = Self;
fn add(self, other: T) -> Self {
let temp = other.into();
Complex {
real: self.real + temp.real,
imaginary: self.imaginary + temp.imaginary,
}
}
}
impl<T> std::ops::AddAssign<T> for Complex
where
T: Into<Complex>,
{
fn add_assign(&mut self, other: T) {
let temp = other.into();
*self = Complex {
real: self.real + temp.real,
imaginary: self.imaginary + temp.imaginary,
};
}
}这样一来我们就可以美滋滋地使用运算符了:
let foo = Complex::new(1.0, -2.0);
println!("{:?}", foo + 1.0);
let mut bar = Complex::new(2.0, 2.0);
// 已经给 Complex 实现了 Copy 和 Clone
bar += foo;
println!("{:?}", bar);如果有读者朋友认为这里讲得有点儿快,可以自行阅读“肥蟹书”的第 12 章。书中的内容更为系统和详尽。限于篇幅,这篇文章很多细节没有展开。让我们进入用 trait 实现函数重载这一奇技淫巧的部分吧!
函数重载的奇技淫巧
讲解了 Rust 中的运算符重载,我们就可以理解重载原来就是允许函数接受不同类型的参数这回事儿了。事实上,重载还应该允许接受不同数量的参数;而 Rust 中只允许宏接受不同数量(甚至是无限个)的参数——甚至有人由此误以为宏就是为了实现函数重载而设计的。对于这一部分有疑问的读者可以阅读“肥蟹书”第 21 章。
笔者在这一部分来解释这样两个问题:
1. 在其他语言中,以 JavaScript 为例,函数接受不同数量的参数是如何导致代码可读性降低的;
2. 如果一定要在 Rust 中实现函数接受不同数量的参数,怎么使用 trait 来达成这一点。
函数重载导致代码的可读性降低
如果读者熟悉 JavaScript 中的 splice 函数就会明白很多开发者深受其害。因为它有太多种使用方法而
function MyArray() {
this.__array__ = [];
}
MyArray.prototype.add = function(item, index) {
if(typeof index === 'number'){
this.__array__.splice(index, 0, item);
} else {
this.__array__.push(item);
}
}这里笔者使用了还没有 class 时代的 JavaScript 语法,目的是让各位读者找找那些“痛苦的回忆”。毕竟大家现在已经普遍使用各种完善而复杂的框架,可能已经把 JavaScript 的本来面目忘得一干二净了。对于 splice 这个函数,笔者没有使用它的常规用法——即从指定的位置从数组中删去若干个元素,而是在指定位置删除 0 个元素,然后在这个位置插入目标元素。虽然笔者看起来好心地设计了一个抽象 add 方法,事实上这个方法又接受一个可选的参数。如果算上 splice的其他奇技淫巧,另外还有人使用 push 返回更新后的数组长度这个特性来用数组长度作为循环变量——这些行为都会使代码的可读性严重下降,其他语言的程序员很容易对这些用法感到困惑,而一般他们收到的建议是:如果函数有不同的参数数量,那么就应该写成至少两个函数。
可是有时候也会有默认值这种需求,而你偷懒不想写 Option 类型。因为你可能更懒得每次调用的时候都重复写很多 None——这时候就可以使用混合了元组的 trait 实现这一需求,这次来扩展 Vec 类型:
trait Add<U> {
fn add(&mut self, args: U);
}
impl<T> Add<(T,)> for Vec<T> {
fn add(&mut self, args: (T,)) {
self.push(args.0);
}
}
impl<T> Add<(T, usize)> for Vec<T> {
fn add(&mut self, args: (T, usize)) {
self.insert(args.1, args.0);
}
}
fn main() {
let mut foo = vec![0, 1, 2];
foo.add((3,));
println!("{:?}", foo);
foo.add((4, 0));
println!("{:?}", foo);
}我们可以看到 Rust 自己实现的插入函数是 insert。而非使用函数重载的方法,因为这样的方法是不被建议的。不过我们以后也可以留意使用两个括弧来调用函数的场景,函数的作者实际上是使用了元组。
最后,祝各位秀发浓密!

本文讨论了Rust编程语言中的trait概念,特别关注了将其翻译为“特型”的争议,以及trait在运算符重载中的应用,强调了重载的多态性本质。作者通过实例展示了如何使用trait实现函数重载,避免代码可读性降低的问题。
10

被折叠的 条评论
为什么被折叠?



