Appearance
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 才能编译通过。