Rust 泛型
其实你早就在用泛型(generics)了,只是可能没叫出它的名字:Vec<i32> 和 Option<T> 都用到了泛型。这一页讲的是怎样写你自己的泛型函数和泛型结构体。
泛型解决什么问题
Section titled “泛型解决什么问题”假设你想写一个函数,从一组数字里找出最大的那个,而且 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));}输出:
472.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>,但结构体你只写了一次。
泛型在运行时有开销吗?
Section titled “泛型在运行时有开销吗?”没有。你实际用到了哪些具体类型,Rust 就为每一种生成一个单独的、专门化的函数或结构体版本(这叫作单态化,monomorphization)。等程序真正运行时,已经没有任何泛型代码了,只剩下普通的、类型完全确定的函数,速度和你亲手为每种类型写一遍完全一样。
泛型结构体的方法
Section titled “泛型结构体的方法”和结构体那一页一样,你可以用 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那一页里讲。
1. 泛型版的最小值
Section titled “1. 泛型版的最小值”写一个 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']));}输出:
3r2. Pair 中较大的那一半
Section titled “2. Pair 中较大的那一半”给 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());}输出:
9pear- 泛型让你只写一个函数或结构体,就能用于多种类型,不用重复写代码。
<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
- 泛型数据类型 — Rust 程序设计语言:函数、结构体、枚举和方法中的泛型,包括
largest这个例子。 - 泛型 — 通过例子学 Rust:泛型函数、泛型结构体及其语法的简短可运行示例。
- 实现 — 通过例子学 Rust:更多关于
impl<T>块,以及只为某个具体类型实现方法的内容。 - 约束 — 通过例子学 Rust:trait 约束怎样限制泛型类型,以及为什么需要它。
- Generic parameters — The Rust Reference(英文):精确的规则,想了解全部细节时再看。