Skip to content

21.4 泛型参数约束

Rust 的泛型和 C++的 template 非常相似,但也有很大不同。它们的最大区别在于执行类型检查的时机。在 C++里面,模板的类型检查是延迟到实例化的时候做的。而在 Rust 里面,泛型的类型检查是当场完成的。示例如下:


rust
// C++ template 示例
template <class T>
const T& max (const T& a, const T& b) {
    return (a<b)?b:a;
}

void instantiateInt() {
    int m = max(1, 2);    // 实例化 1
}

struct T {
    int value;
};

void instantiateT() {
    T t1 { value: 1};
    T t2 { value: 2};
    //T m = max(t1, t2);  // 实例化 2
}

int main() {

    instantiateInt();
    instantiateT();
    return 0;
}

在这个例子中:如果我们把“实例化 2”处的代码先注释掉,使用g++ -std=c++11 test.cpp命令编译,可以通过;如果取消注释,则编译不通过。编译错误为:

error: no match for 'operator<' (operand types are 'const T' and 'const T')

出现编译错误的原因是我们没有给 T 类型提供比较运算符重载。此处的关键在于,如果我们用 int 类型来实例化 max 函数,它就可以通过;如果我们用自定义的 T 类型来实例化 max 函数,它就通不过。max 函数本身一直都是没有问题的。也就是说,编译器在处理 max 函数的时候,根本不去管 a<b 是不是一个合理的运算,而是将这个检查留给后面实例化的时候再分析。

Rust采取了不同的策略,它会在分析泛型函数的时候当场检查类型的合法性。这个检查是怎样做的呢?它要求用户提供合理的“泛型约束”。在Rust中,trait可以用于作为“泛型约束”。在这一点上,Rust跟C#的设计是类似的。上例用Rust来写,大致是这样的逻辑:

rust
fn max<T>(a: T, b: T) -> T {
    if a < b {
        b
    } else {
        a
    }
}

fn main() {
    let m = max(1, 2);
}

编译,出现编译错误:

error[E0369]: binary operation `<` cannot be applied to type `T`
--> test.rs:2:8
    |
  2 |     if a < b {
    |        ^^^^^
    |
    = note: an implementation of `std::cmp::PartialOrd` might be missing for `T`

这个编译错误说得很清楚了,由于泛型参数 T 没有任何约束,因此编译器认为 a<b 这个表达式是不合理的,因为它只能作用于支持比较运算符的类型。在 Rust 中,只有 impl 了 PartialOrd 的类型,才能支持比较运算符。修复方案为泛型类型 T 添加泛型约束。

泛型参数约束有两种语法:

(1)在泛型参数声明的时候使用冒号:指定;

(2)使用where从句指定。

rust
use std::cmp::PartialOrd;

// 第一种写法:在泛型参数后面用冒号约束
fn max<T: PartialOrd>(a: T, b: T) -> T {

// 第二种写法,在后面单独用 where 从句指定
fn max<T>(a: T, b: T) -> T
    where T: PartialOrd

在上面的示例中,这两种写法达到的目的是一样的。但是,在某些情况下(比如存在下文要讲解的关联类型的时候),where 从句比参数声明中的冒号约束(Trait Bounds)具有更强的表达能力,但它在泛型参数列表中是无法表达的。我们以 Iterator trait 中的函数为例:


rust
trait Iterator {
    type Item; // Item 是一个关联类型

    // 此处的 where 从句没办法在声明泛型参数的时候写出来
    fn max(self) -> Option<Self::Item>
        where Self: Sized, Self::Item: Ord
    {
        ...
    }
    ...
}

它要求 Self 类型满足 Sized 约束,同时关联类型 Self::Item 要满足 Ord 约束,这是用冒号语法写不出来的。在声明的时候使用冒号约束的地方,一定都能换作 where 从句来写,但是反过来不成立。另外,对于比较复杂的约束条件,where 子句的可读性明显更好。

在有了“泛型约束”之后,编译器不仅会在声明泛型的地方做类型检查,还会在实例化泛型的地方做类型检查。接上例,如果向我们上面实现的那个 max 函数传递自定义类型参数:


rust
struct T {
    value: i32
}

fn main() {
    let t1 = T { value: 1};
    let t2 = T { value: 2};
    let m = max(t1, t2);
}

编译,可见编译错误:


rust
error[E0277]: the trait bound `T: std::cmp::PartialOrd` is not satisfied
    --> test.rs:20:13
     |
  20 |     let m = max(t1, t2);
     |             ^^^ can't compare `T` with `T`
     |
     = help: the trait `std::cmp::PartialOrd` is not implemented for `T`
     = note: required by `max`

这说明,我们在调用 max 函数的时候也要让参数符合“泛型约束”。因此我们需要 impl PartialOrd for T。完整代码如下:


rust
use std::cmp::PartialOrd;
use std::cmp::Ordering;

fn max<T>(a: T, b: T) -> T
    where T: PartialOrd
{
    if a < b {
        b
    } else {
        a
    }
}

struct T {
    value: i32
}

impl PartialOrd for T {
    fn partial_cmp(&self, other: &T) -> Option<Ordering> {
        self.value.partial_cmp(&other.value)
    }
}

impl PartialEq for T {
    fn eq(&self, other: &T) -> bool {
        self.value == other.value
    }
}

fn main() {
    let t1 = T { value: 1};
    let t2 = T { value: 2};
    let m = max(t1, t2);
}

由于标准库中的 PartialOrd 继承了 PartialEq,因此单独实现 PartialOrd 还是会产生编译错误,必须同时实现 PartialEq 才能编译通过。

Released under the MIT License