理解 Rust 泛型特征 (Generic Trait)

在 Rust 中,特征(trait)也可以是泛型。引入泛型特征,一是希望特征不受具体类别的限制,二是希望特征具有更广泛的约束性。如果你不明白就点进来看吧。

不受具体类别的限制

在 Rust 中,“转型”(cast)实际上就是一种特征。在没有泛型特征的世界里,类型转换应该怎么办?

我们可以先写一个叫做CastFromI32的特征。所有实现CastFromI32特征的类型都可以从i32类转型。

struct MyType {}

pub trait CastFromI32 {
  fn from(_: i32) -> Self;
}

impl CastFromI32 for MyType {
  fn from(origin: i32) -> Self {
    // -- cast code --
  }
}

这远远不够。现在,我还希望能从i64/u32/u64/f8/f32/…等其他类型转型。这意味着我们需要分别写出CastFromI64/CastFromU32/…等一系列特征,然后期待开发者一个个实现他们。

struct MyType {}

pub trait CastFromI32 {
  fn from(_: i32) -> Self;
}

pub trait CastFromI64 {
  fn from(_: i64) -> Self;
}

pub trait CastFromU32 {
  fn from(_: u32) -> Self;
}

// pub trait CastFrom ...
// damn, so much!

这无疑是无趣的。但如果有了泛型特征,我们就能这样做:

struct MyType {}

pub trait CastFrom<T> {
  fn from(_: T) -> Self;
}

泛型特征使得“特征不受具体类别的限制”。这样,方便我们写出更加简洁的代码。开发者也可以利用宏减少工作量。

更广泛的约束性

特征类似于其他语言的接口,具有约束性。在面向对象程序的编写中,我们经常写下FlyableEatable这种接口,让鸭子和披萨区别于其他类。在 Rust 里特征也是如此。

“运算符重载”也通过特征实现。首先来看看加法特征长什么样。

pub trait Add<Rhs = Self> {
    /// The resulting type after applying the `+` operator.
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}

其中,Rhs 是泛型,指定被加数的类型,默认和加数类型一致(Self)。Output是关联类型,指定加法结果的类型。关联类型是泛型的特殊形式。

通过实现Add特征,编写复数类的加法。我会这样写:

struct Complex {
    real: f64,
    imag: f64,
}

impl Add for Complex {
    type Output = Complex;

    fn add(self, rhs: Self) -> Self::Output {
        Complex {
            real: self.real + rhs.real,
            imag: self.imag + rhs.imag,
        }
    }
}

可作为一个优秀的库作者,你还希望Complex底层的实部、虚部支持更多的数据种类,例如i64/u32/u64/f8/f32/…等。这意味着我们需要把Complex写成泛型,大概是这样:

struct Complex<T> {
    real: T,
    imag: T,
}

impl<T> Add for Complex<T> {
    type Output = Complex<T>;

    fn add(self, rhs: Self) -> Self::Output {
        Complex {
            real: self.real + rhs.real, // compile error ❌️ here!
            imag: self.imag + rhs.imag,
        }
    }
}

编译……错误!这是因为,我们使用的泛型T可能是任何类型,例如鸭子或者披萨,它们是没有加法的。编译器非常聪明地检查到了这一点。所以,T需要被约束。约束的条件是“T 具有加法,被加数类型和结果类型也为 T”,翻译成 Rust 就是T: Add<(T, )Output=T>

Add<T, Output=T>的第一个T指的是Rhs,被加数的类型,在Add特征里已经说过默认值是 Self,所以我们这里省略也可以。第二个T是关联类型Output,结果的类型,需要指明是T

所以正确的写法是:

struct Complex<T> {
    real: T,
    imag: T,
}

impl<T> Add for Complex<T>
    where T: Add<Output=T>   // or, `where T: Add<T, Output=T>`
{
    type Output = Complex<T>;

    fn add(self, rhs: Self) -> Self::Output {
        Complex {
            real: self.real + rhs.real,
            imag: self.imag + rhs.imag,
        }
    }
}

回顾我们的改动:将原来不顾一切的类型T加上了约束。这个约束条件“T 具有加法,被加数类型和结果类型也为 T”较为抽象。我们通过调整Add泛型特征里的具体类型使得约束成立。现在,你大概能理解为什么我说“泛型特征具有更广泛的约束性”。

结语

撰写这篇文章时,是我接触 Rust 的第二天。文章内容肯定有很多不足之处,请斧正。

如果你觉得有帮助,可以继续关注我的博客,下方链接处有 RSS 订阅地址。

非经允许,请勿转载。