和其他语言一样,Rust也具有泛型(Generics)能力。泛型是指把类型抽象成一种“参数”,数据和算法都针对这种抽象的类型参数来实现,而不针对具体类型。当我们需要真正使用的时候,再具体化、实例化类型参数。使用不同类型参数将泛型类型具体化后,获得的是完全不同的具体类型。
基本语法
泛型使用<>
声明,且必须先声明后使用,泛型可以被用于数据结构、函数和方法中。
泛型在数据结构中的用法
对于复合数据类型,我们可以使用泛型来提高复合数据类型的通用性。之前学过的复合数据类型有tuple
、struct
、tuple struct
和enum
四种,因为tuple
类型没有类型名,故其无法使用泛型,剩余三种都可以。我们熟悉的Option
就是一个泛型enum
类型,泛型声明放在类型名之后大括号之前,如下面代码所示。
pub enum Option<T> {
/// No value
None,
/// Some value `T`
Some(T),
}
在这个Option
内部,Some(T)
是一个tuple struct
,包含一个类型为T
的元素,T
在使用时指定具体类型。例如:
let x: Option<i32> = Some(42);
let y: Option<f64> = None;
在这里,x
是Option<i32>
类型的值,y
是Option<f64>
类型的值,它们是完全不同的类型。
泛型在函数中的用法
泛型声明放在函数名之后括号之前,如下面代码所示。compare_option
函数的两个参数均为Option
,但可以具有不同的类型。
fn compare_option<T1, T2>(fir: Option<T1>, sec: Option<T2>) -> bool {
match (fir, sec) {
(Some(_), Some(_)) => true,
(None, None) => true,
_ => false,
}
}
fn main() {
let x: Option<i32> = Some(42);
let y: Option<i32> = None;
let option = compare_option(x, y);
println!("{}", option);
}
如main
函数中所示,一般情况下,调用泛型函数可以不指定泛型参数类型,编译器可以通过类型推导自动判断。如果需要手动指定泛型参数类型,需要使用compare_option::<i32, i32>(x, y)
形式的语法。
泛型在方法中的用法
在结构体上下文中被定义的函数称为方法,对方法也可以使用泛型,泛型声明放在impl
之后类型名之前,如下面代码所示。
impl<T> Data<T> {
fn compare_option<P>(&self, other: &Data<P>) -> bool {
let fir: &Option<T> = &self.data;
let sec: &Option<P> = &other.data;
match (fir, sec) {
(Some(_), Some(_)) => true,
(None, None) => true,
_ => false,
}
}
}
fn main() {
let data1 = Data { data: Some(10) };
let data2 = Data { data: Some(10) };
let res = data1.compare_option(&data2);
println!("{}", res);
}
当为结构体实现trait的时候也可以使用泛型,在impl <Trait> for <Type>
这个语法中,泛型参数既可以出现在<Trait>
位置也可以出现在<Type>
位置。下面是标准库中的Into
trait。From
和Into
是一对功能互逆的trait,如果A:Into<B>
,则意味着B:From<A
。下面代码的意思是,针对所有类型T
,只要满足U: From<T>
,那么就为该类型impl Into<U>
。有了这样一个impl块之后,我们如果想为自己的两个类型提供互相转换的功能,那么只需impl From
这一个trait就可以了。
impl<T, U> Into<U> for T
where
U: From<T>,
{
fn into(self) -> U {
U::from(self)
}
}
impl
后面的泛型参数声明需要满足三个条件之一:
- 出现在被实现的类型中,例如,
impl<T> Foo<T>
- 出现在被实现的trait中,例如,
impl<T> SomeTrait<T> for Foo
- 出现在关联类型的绑定中,例如,
impl<T, U> SomeTrait for T where T: AnotherTrait<AssocType=U>
泛型约束
我们在使用泛型参数的时候,一般不是对所有类型都进行操作,而是只对满足某种类型条件的类型进行操作,这称为泛型的约束,约束一般以trait的形式表达,有两种语法:
- 在泛型参数声明的时候使用冒号
:
指定 - 使用
where
子句指定
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;
在上面的示例中,两种写法的效果相同,都是要求T
类型必须实现了PartialOrd
这个trait。对于复杂的约束条件,使用where
子句的可读性会更好。泛型约束会在编译期执行检查,检查被实例化的泛型类型是否满足了泛型声明时的约束。
trait的关联类型
trait中不仅可以包含方法、常量,还可以包含“类型”,例如常用的迭代器Iterator
这个trait,它里面就有一个Item
类型。
pub trait Iterator {
type Item;
...
}
这样在trait中声明的类型被称为关联类型(associated type),关联类型也同样是这个trait的“泛型参数”。只有指定了所有的泛型参数和关联类型,这个trait才能真正的具体化。可以看到,我们希望参数是一个泛型迭代器,可以在约束条件中写ITER: Iterator<Item = ITEM>
,且其关联类型Item
必须实现了Debug
这个trait。跟普通泛型参数比起来,关联类型参数必须使用名字赋值的方式。
fn use_iter<ITEM, ITER>(mut iter: ITER)
where
ITER: Iterator<Item = ITEM>,
ITEM: Debug,
{
while let Some(i) = iter.next() {
println!("{:?}", i);
}
}
目前来看,关联类型和普通泛型参数的功能一致,那它存在的意义是什么呢?其实,关联类型更加强调了“关联性”,即强调了所声明的泛型参数和该trait的相关性,从而能够达到封装的效果。例如,下面这个例子中,假如我们想设计一个泛型的“图”类型,它包含“顶点”和“边”两个泛型参数,如果使用普通泛型参数,如下所示:
trait Graph<N, E> {
fn has_edge(&self, n1: &N, n2: &N);
// ...
}
fn distance<N, E, G: Graph<N, E>>(graph: &G, start: &N, end: &N) -> i32 {
// ...
}
distance
函数用来计算一个图中两个顶点之间的距离,它的函数签名里会有很多的泛型参数。但实际上,对于确定的Graph
类型,它的顶点和边的类型应该是固定的,所以在函数签名中再写一遍没有什么意义,关联类型就解决了这个问题,如下面代码所示:
trait Graph {
type N;
type E;
fn has_edge(&self, n1: &Self::N, n2: &Self::N);
// ...
}
fn distance<G>(graph: &G, start: &G::N, end: &G::N) -> i32
where
G: Graph,
{
// ...
}
除了关注关联类型的优点,我们也得关注其缺点。如果某个trait使用了关联类型,则不能为某个类型实现多个该trait,如下面的代码所示:
trait ConvertTo {
type DEST;
fn convert(&self) -> Self::DEST;
}
impl ConvertTo for i32 {
type DEST = i32;
fn convert(&self) -> i32 {
*self as i32
}
}
// error[E0119]: conflicting implementations of trait `ConvertTo` for type `i32`:
impl ConvertTo for i32 {
type DEST = f64;
fn convert(&self) -> f64 {
*self as f64
}
}
但如果改用普通泛型参数,则无此问题,可以正常编译通过:
trait ConvertTo<DEST> {
fn convert(&self) -> DEST;
}
impl ConvertTo<i32> for i32 {
fn convert(&self) -> i32 {
*self as i32
}
}
impl ConvertTo<f64> for i32 {
fn convert(&self) -> f64 {
*self as f64
}
}
由此可见,在编译器眼里,如果trait有类型参数,那么给定不同的类型参数,它们就已经是不同的trait,可以针对同一个类型impl
。如果trait没有类型参数,只有关联类型,给关联类型指定不同的类型参数是不能用它们针对同一个类型impl
的。
对于普通泛型参数和关联类型,它们各有利弊,我们该如何选择呢?一句话,对于需要在impl阶段就确定下来的,选择关联类型;对于在函数调用阶段才确定下来的,选择普通泛型参数。在标准库中,何时使用普通泛型参数、何时使用关联类型,实际上有非常好的示例。
-
普通泛型参数:对于标准库中的
AsRef
,实现这个trait的类型可能会想转化为很多类型的引用,具体的类型由用户在函数调用阶段选择,所以必须使用普通泛型函数。pub trait AsRef<T: ?Sized> { /// Performs the conversion. fn as_ref(&self) -> &T; }
-
关联类型:对于标准库中的
Deref
,实现这个trait的类型解引用的目标类型是唯一固定的,需要在impl阶段就确定下来,所以必须使用关联类型。pub trait Deref { /// The resulting type after dereferencing. type Target: ?Sized; /// Dereferences the value. fn deref(&self) -> &Self::Target; }
参考文献
- 《Rust编程之道》张汉东
- 《深入浅出Rust》范长春