Appearance
21.2 函数中的泛型
泛型可以使用在函数中,语法类似:
rust
fn compare_option<T>(first: Option<T>, second: Option<T>) -> bool
{
match(first, second) {
(Some(..), Some(..)) => true,
(None, None) => true,
_ => false
}
}
在上面这个例子中,函数compare_option
有一个泛型参数 T,两个形参类型均为Option<T>
。这意味着这两个参数必须是完全一致的类型。如果我们在参数中传入两个不同的 Option,会导致编译错误:
rust
fn main() {
println!("{}", compare_option(Some(1i32), Some(1.0f32))); // 类型不匹配编译错误
}
编译器在看到这个函数调用的时候,会进行类型检查:first
的形参类型是Option<T>
、实参类型是Option<i32>
,second
的形参类型是Option<T>
、实参类型是Option<f32>
。 这时编译器的类型推导功能会进行一个类似解方程组的操作:由Option<T> == Option<i32>
可得T == i32
,而由Option<T> == Option<f32>
又可得T == f32
。这两个结论产生了矛盾,因此该方程组无解,出现编译错误。
如果我们希望参数可以接受两个不同的类型,那么需要使用两个泛型参数:
rust
fn compare_option<T1, T2>(first: Option<T1>, second: Option<T2>) -> bool { ... }
一般情况下,调用泛型函数可以不指定泛型参数类型,编译器可以通过类型推导自动判断。某些时候,如果确实需要手动指定泛型参数类型,则需要使用function_name::<type params>(function params)
的语法:
rust
compare_option::<i32, f32>(Some(1), Some(1.0));
泛型函数在很大程度上实现了 C++ 的“函数重载”功能。比如,str 类型有一个contains
方法,使用示例如下:
rust
fn main() {
let s = "hello";
println!("{}", s.contains('a'));
println!("{}", s.contains("abc"));
println!("{}", s.contains(&['H'] as &[char]));
println!("{}", s.contains(|c : char| c.len_utf8() > 2));
}
我们可以看到,这个contains
方法可以接受很多种不同的参数类型,使用起来很方便。那么它是怎么实现的呢?主要技术就是泛型。它的签名如下:
rust
fn contains<'a, P: Pattern<'a>>(&'a self, pat: P) -> bool
可见,它的第二个参数不是某个具体类型,而是一个泛型类型,而且这个泛型参数满足 Pattern trait 的约束。这意味着,所有实现了 Pattern trait 的类型,都可以作为参数使用。我们希望这个参数接受哪些类型,就针对这个类型实现这个 trait 即可。
Rust 没有 C++ 那种无限制的 ad hoc 式的函数重载功能。现在没有,将来也不会有。主要原因是,这种随意的函数重载对于代码的维护和可读性是一种伤害。通过泛型来实现类似的功能是更好的选择。 如果说,不同的参数类型,没有办法用 trait 统一起来,利用一个函数体来统一实现功能,那么它们就没必要共用同一个函数名。它们的区别已经足够大,所以理应使用不同的名字。强行使用同一个函数名来表示区别非常大的不同函数逻辑,并不是好的设计。
我们还有另外一种方案,可以把不同的类型统一起来,那就是 enum。通过 enum 的不同成员来携带不同的类型信息,也可以做到类似“函数重载”的功能。但这种做法跟“函数重载”有本质区别,因为它是有运行时开销的。 enum 会在执行阶段判断当前成员是哪个变体,而“函数重载”以及泛型函数都是在编译阶段静态分派的。同样,Rust 也不鼓励大家仅仅为了省去命名的麻烦,而强行把不同类型用 enum 统一起来用一个函数来实现。如果一定要这么做,那么最好是有一个好的理由,而不仅是因为懒得给函数命名而已。