跳转到内容

Rust 泛型

其实你早就在用泛型(generics)了,只是可能没叫出它的名字:Vec<i32> 和 Option<T> 都用到了泛型。这一页讲的是怎样写你自己的泛型函数和泛型结构体。

假设你想写一个函数,从一组数字里找出最大的那个,而且 i32 和 f64 都要支持。没有泛型的话,你得写两遍:

fn largest_i32(numbers: &[i32]) -> i32 {
let mut largest = numbers[0];
for &n in numbers {
if n > largest {
largest = n;
}
}
largest
}
fn largest_f64(numbers: &[f64]) -> f64 {
let mut largest = numbers[0];
for &n in numbers {
if n > largest {
largest = n;
}
}
largest
}

逻辑一模一样,只是每种类型都复制粘贴了一份。有了泛型,写一次就够了。

fn largest<T: PartialOrd + Copy>(numbers: &[T]) -> T {
let mut largest = numbers[0];
for &n in numbers {
if n > largest {
largest = n;
}
}
largest
}
fn main() {
let integers = [10, 25, 3, 47, 8];
let floats = [1.5, 2.8, 0.3];
println!("{}", largest(&integers));
println!("{}", largest(&floats));
}

输出:

47
2.8
▶ 动手试一试

<T: PartialOrd + Copy> 可以读作:“对于任意类型 T,只要它能比较大小(PartialOrd),并且能被复制(Copy)。”T 是一个占位符。你实际用哪种具体类型调用这个函数(i32、f64 等等),Rust 就为那种类型生成一个对应的版本。

结构体也可以是泛型的:

struct Pair<T> {
first: T,
second: T,
}
fn main() {
let numbers = Pair { first: 5, second: 10 };
let words = Pair { first: "你好", second: "世界" };
println!("{} {}", numbers.first, numbers.second);
println!("{} {}", words.first, words.second);
}

输出:

5 10
你好 世界
▶ 动手试一试

同一个 Pair<T> 定义,既能装数字,也能装文本,还能装其他任何类型。numbers 是 Pair<i32>,words 是 Pair<&str>,但结构体你只写了一次。

没有。你实际用到了哪些具体类型,Rust 就为每一种生成一个单独的、专门化的函数或结构体版本(这叫作单态化,monomorphization)。等程序真正运行时,已经没有任何泛型代码了,只剩下普通的、类型完全确定的函数,速度和你亲手为每种类型写一遍完全一样。

和结构体那一页一样,你可以用 impl 块给泛型结构体添加方法。唯一的新东西是,impl 自己也需要一个 <T>:

struct Pair<T> {
first: T,
second: T,
}
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
fn swap(self) -> Pair<T> {
Pair { first: self.second, second: self.first }
}
}
impl Pair<f64> {
fn average(&self) -> f64 {
(self.first + self.second) / 2.0
}
}
fn main() {
let words = Pair::new("左", "右").swap();
println!("{} {}", words.first, words.second);
let scores = Pair::new(7.0, 9.0);
println!("{}", scores.average());
}

输出:

右 左
8
▶ 动手试一试

这里有两种 impl 块:

  • impl<T> Pair<T> 的意思是“这些方法对任意类型的 Pair 都存在”。第一个 <T> 可以读作“对于任意类型 T”。如果少了它,Rust 会以为 T 是某个真实类型的名字,然后报错说找不到这个类型。
  • impl Pair<f64> 没有 <T>。它的方法只对 Pair<f64> 存在,所以 average 才能用 + 和 / 2.0。试着调用 words.average(),编译器会告诉你 Pair<&str> 上没有这个方法。

如果两个字段的类型可能不同,结构体也可以有多个类型参数,比如 struct Pair<T, U> { first: T, second: U }。而当 trait 约束写得很长时,可以把它们挪到 where 子句里,让函数签名更好读。这些内容在高级 trait那一页里讲。

写一个 fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T,返回最小的那个元素。分别用 [8, 3, 5] 和字符 ['r', 'u', 's', 't'] 测试它。提示:就是这一页的 largest 函数,把一个比较反过来。

查看答案
fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut smallest = items[0];
for &item in items {
if item < smallest {
smallest = item;
}
}
smallest
}
fn main() {
println!("{}", smallest(&[8, 3, 5]));
println!("{}", smallest(&['r', 'u', 's', 't']));
}

输出:

3
r
▶ 动手试一试

给 Pair<T> 添加一个方法 larger(&self) -> &T,返回较大那个字段的引用。分别用一个装数字的 Pair 和一个装单词的 Pair 测试它。提示:比较大小需要 PartialOrd,所以把约束直接写在 impl 上:impl<T: PartialOrd> Pair<T>。

查看答案

字符串按字典序(逐字节)比较,所以这里的单词保持英文,结果一眼就能看懂:"pear" 以 p 开头,比以 a 开头的 "apple" 大。

struct Pair<T> {
first: T,
second: T,
}
impl<T: PartialOrd> Pair<T> {
fn larger(&self) -> &T {
if self.first > self.second {
&self.first
} else {
&self.second
}
}
}
fn main() {
let numbers = Pair { first: 3, second: 9 };
let words = Pair { first: "pear", second: "apple" };
println!("{}", numbers.larger());
println!("{}", words.larger());
}

输出:

9
pear
▶ 动手试一试
  • 泛型让你只写一个函数或结构体,就能用于多种类型,不用重复写代码。
  • <T> 是占位的类型;trait 约束(T: PartialOrd)限制了 T 可以是哪些类型。
  • impl<T> Pair<T> 为所有 T 添加方法;impl Pair<f64> 只为某一个具体类型添加方法。
  • 泛型代码在编译时会按类型生成专门的版本,运行时没有任何性能开销。

小测验

1. 泛型解决的是什么问题?

2. 在 fn largest<T: PartialOrd + Copy>(...) 中,PartialOrd + Copy 起什么作用?

3. 和手写的代码相比,使用泛型在运行时有额外开销吗?

4. 只写一个 struct Pair<T> 定义,能让你做到什么?

得分: 0 / 4