23、Trait Bounds(Traits约束)
Trait Bounds(Traits约束)
我们已经看到了两种traits的用例:
- 解锁“内置”行为(例如:运算符重载)
- 为已经存在的类型添加新的行为(例如:为u32类型添加
is_even
方法)
现在是第三种用例:泛型编程
引入问题
到目前为止,我们的所有函数和方法都是和具体类型一起工作的。
基于具体类型编写的代码往往更容易阅读和理解,但是它的可复用性也受到了限制。
例如,我们可以想象一下,编写一个函数,如果整数是偶数,就返回true
。如果我们使用具体类型编程,那么我们必须为每个整数类型编写一个单独的函数!
一共有多少个整数类型呢??
这很显然,作为程序员,怎么能干这种事情。
如果不用泛型解决这个问题,似乎确实有好一点的方案?
我们可以编写一个拓展trait,然后为每个整数类型编写不同的实现:
trait IsEven {
fn is_even(&self) -> bool;
}
impl IsEven for i32 {
fn is_even(&self) -> bool {
self % 2 == 0
}
}
impl IsEven for i64 {
fn is_even(&self) -> bool {
self % 2 == 0
}
}
// Etc.
但是,这也好不到哪去吧~
重复仍然存在
泛型编程
使用泛型可以做的更好
泛型允许我们编写使用类型参数,而不是具体类型的代码:
fn print_if_even<T>(n: T)
where
T: IsEven + Debug
{
if n.is_even() {
println!("{n:?} is even");
}
}
print_if_even
就是一个泛型函数。
它不与特定类型绑定,而是可以与任意类型T一起使用:
- 这个类型实现了
IsEven
trait - 这个类型实现了
Debug
trait
以上的这个约束由trait bound
表示:T:IsEven+Debug
。+操作符
用于要求T实现多个trait。
T:IsEven+Debug
相当于T实现IsEven和Debug
Traits约束
Traits约束在print_if_even中的作用就是
如果我们尝试删掉Traits约束,代码将无法通过编译:
fn print_if_even<T>(n: T) {
if n.is_even() {
println!("{n:?} is even");
}
}
没有traits约束,编译器就不知道T类型可以做什么,他不知道T有is_even
方法,也不知道如何对T类型进行格式化打印。从编译器的角度看,裸露的T泛型根本就没有行为。
traits约束通过确保函数体所需行为的存在,限制了可以使用的类型集。
语法:内连traits约束
上面的例子都使用where
子句指定traits约束。
fn print_if_even<T>(n: T)
where
T: IsEven + Debug
// ^^^^^^^^^^^^^^^^^
// This is a `where` clause
{
// [...]
}
如果traits边界比较简单,可以将它们内联到类型参数旁边
fn print_if_even<T: IsEven + Debug>(n: T) {
// ^^^^^^^^^^^^^^^^^
// This is an inline trait bound
// [...]
}
语法:有意义的名称
上面的例子使用到了T作为类型参数。这是一般情况下,函数只有一个类型参数时的常见的约定。
我们可以使用其他的有意义的名称作为类型参数:
fn print_if_even<Number: IsEven + Debug>(n: Number) {
// [...]
}
当涉及到多个类型参数时,最好使用更加有意义的名称,在命名类型参数时,要尽可能提高清晰度和可读性。
注意遵守Rust的编码规范:使用驼峰命名法命名类型参数。
练习
这次的题目,是让我们补全上面泛型函数的traits约束,实现比较功能,查阅资料,寻找支持比较的traits即可,这边我发现
就是这个PartialOrd
实现了比较的功能,所以我们导入这个trait即可
pub fn min<T:PartialOrd>(left: T, right: T) -> T {
if left <= right {
left
} else {
right
}
}
也是顺利通过了。
这一节的学习就先到这里了。