万能模板:泛型
学习目标
学完本章后,你将能够:
- 理解泛型的核心概念:一份代码适用多种类型
- 在函数中使用泛型参数
- 在结构体中使用泛型
- 在枚举中使用泛型
- 理解为什么泛型能减少重复代码
一、问题:为什么需要泛型?
假设你想写一个函数,找出两个数中较大的那个:
fn largest_i32(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}这个函数只能用于 i32!如果你的需求是:
- 找最大的 i32 → 写一个函数
- 找最大的 f64 → 再写一个?
- 找最大的 char → 再写一个?
这三个函数逻辑完全一样,只是类型不同。太重复了!
泛型(Generics) 就是解决这个问题的:“用一个模板,套不同的类型。“
二、最简单的泛型函数
fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("最大数字:{}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("最大字符:{}", largest(&chars));
}输出:
最大数字:100
最大字符:y
关键在这行:
fn largest<T: PartialOrd>(list: &[T]) -> &T
↑ ↑
泛型参数 约束:"T 必须能比较大小"拆解:
<T>— 声明”这个函数要用一个泛型类型,代号叫 T”T: PartialOrd— “T 必须是可以比较大小的类型”list: &[T]— 参数是”T 类型的切片”-> &T— 返回”T 类型的引用”
你可以把 T 想成一个插槽。调用时,传什么类型进去,T 就变成什么类型。
三、泛型结构体
3.1 最简单的泛型结构体
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.5, y: 4.3 };
println!("整数点:({}, {})", integer_point.x, integer_point.y);
println!("浮点数点:({}, {})", float_point.x, float_point.y);
}Point<T> 中的 T 表示”x 和 y 是相同的类型,但具体是什么类型,创建时再决定”。
3.2 两个泛型参数
如果需要 x 和 y 是不同类型:
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let p1 = Point { x: 5, y: 3.14 }; // i32 + f64
let p2 = Point { x: 'A', y: "hello" }; // char + &str
println!("p1: ({}, {})", p1.x, p1.y);
println!("p2: ({}, {})", p2.x, p2.y);
}T和U只是名字,可以叫任何名字。常见名字:T,U,V,K(键),V(值),E(错误)- 但名字必须是大写字母开头的驼峰命名。
3.3 为泛型结构体添加方法
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
fn y(&self) -> &T {
&self.y
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("x = {}", p.x());
println!("y = {}", p.y());
}注意:impl<T> 后面的 <T> 不能少!它在告诉 Rust:“接下来的方法要用到泛型 T”。
3.4 针对特定类型的方法
你还可以只给特定类型的泛型结构体添加特殊方法:
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let p = Point { x: 3.0, y: 4.0 };
println!("到原点的距离:{}", p.distance_from_origin());
// 只有 Point<f64> 才有这个方法
}四、泛型枚举
其实我们早就用过泛型枚举了!
// 标准库中的 Option<T>
enum Option<T> {
Some(T),
None,
}
// 标准库中的 Result<T, E>
enum Result<T, E> {
Ok(T),
Err(E),
}现在你明白 Option<i32> 是什么意思了:T 被替换成了 i32,所以变成了 Some(i32) 和 None。
fn main() {
let x: Option<i32> = Some(5); // T = i32
let y: Option<String> = Some(String::from("hi")); // T = String
let z: Option<f64> = None; // T = f64
}五、泛型的方法实现
让我们看一个更实用的例子:
struct Pair<T> {
first: T,
second: T,
}
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
fn swap(&mut self) {
std::mem::swap(&mut self.first, &mut self.second);
}
}
fn main() {
let mut p = Pair::new("小明", "小红");
println!("{} 和 {}", p.first, p.second); // 小明 和 小红
p.swap();
println!("{} 和 {}", p.first, p.second); // 小红 和 小明
}Pair<T> 能用于任何类型 — 字符串、数字、甚至你自己定义的结构体!
六、多泛型参数与 trait 约束组合
use std::fmt::Display;
// T 必须能打印
fn print_pair<T: Display>(a: T, b: T) {
println!("({}, {})", a, b);
}
// 不同泛型类型,各自有约束
fn compare_and_print<T: Display + PartialOrd>(a: T, b: T) {
if a > b {
println!("{} 大于 {}", a, b);
} else if a < b {
println!("{} 小于 {}", a, b);
} else {
println!("{} 等于 {}", a, b);
}
}
fn main() {
print_pair(10, 20);
print_pair("你好", "世界");
compare_and_print(10, 20);
compare_and_print(3.14, 2.71);
}T: Display + PartialOrd 意思是”T 必须同时满足能打印和能比较大小”。
七、泛型性能:零成本抽象
有一个常见的担心:“泛型会不会让程序变慢?”
答案:不会。 Rust 泛型是编译时展开的。
fn largest<T: PartialOrd>(list: &[T]) -> &T { ... }
fn main() {
largest(&[1, 2, 3]); // 编译时生成 largest_i32
largest(&[1.0, 2.0, 3.0]); // 编译时生成 largest_f64
}编译器会为每种用到的具体类型生成一份专属代码,就像你手动写了两个函数一样。这叫做单态化(monomorphization),性能没有损失。
八、泛型实战:一个迷你缓存
struct Cache<T> {
value: Option<T>,
}
impl<T: Clone> Cache<T> {
fn new() -> Self {
Cache { value: None }
}
fn get_or_compute<F>(&mut self, compute: F) -> T
where
F: FnOnce() -> T,
{
match &self.value {
Some(v) => v.clone(),
None => {
let v = compute();
self.value = Some(v.clone());
v
}
}
}
}
fn main() {
let mut cache = Cache::new();
let v1 = cache.get_or_compute(|| {
println!("计算中...");
42
});
let v2 = cache.get_or_compute(|| {
println!("计算中..."); // 不会打印,因为缓存了
100
});
println!("v1 = {}, v2 = {}", v1, v2);
}输出:
计算中...
v1 = 42, v2 = 42
本章小结
泛型让你写”一份代码,适配多种类型”:
<T>声明泛型类型参数- 函数、结构体、枚举、方法都可以使用泛型
T: Trait是类型约束,限制 T 必须支持某些功能Option<T>和Result<T, E>就是泛型枚举- 泛型是”零成本抽象”:编译时展开,不影响运行时性能
- 常见命名:
T(任意类型)、K(键)、V(值)、E(错误)
泛型的另一半是 trait。下一章我们深入 trait。
章节考查
总分100分:概念考查40分 + 判断正误20分 + 代码分析15分 + 编程大题15分 + 填空题5分 + 代码补全5分
一、概念考查(每题4分,共40分)
1. 泛型的主要目的是?
- A. 让代码运行更快
- B. 一份代码适用多种类型,减少重复
- C. 替代所有类型标注
- D. 让程序更安全
点击查看答案
B。泛型的核心目的是代码重用 — 用一个通用的定义适配多种不同的具体类型。
2. fn foo<T>(x: T) -> T 中 <T> 的作用是?
- A. 返回一个元组
- B. 声明泛型类型参数 T
- C. 调用一个宏
- D. 声明变量 T
点击查看答案
B。<T> 声明了泛型类型参数,告诉编译器这个函数将使用一个类型占位符 T。
3. Option<T> 是一个?
- A. 函数
- B. 结构体
- C. 泛型枚举
- D. 模块
点击查看答案
C。Option<T> 是一个泛型枚举,T 可以是任何类型。
4. T: Display 的含义是?
- A. T 必须叫 Display
- B. T 必须实现 Display trait
- C. T 可以显示
- D. T 是一个字符串
点击查看答案
B。T: Display 是 trait 约束,表示 T 必须实现了 Display trait(可以被格式化为文本)。
5. struct Point<T, U> { x: T, y: U } 中 x 和 y 的类型?
- A. 必须是相同类型
- B. 可以是任意类型,且可以不同
- C. 必须是数字类型
- D. T 和 U 必须不同
点击查看答案
B。T 和 U 是两个独立的泛型参数,可以是不同类型(也可以是相同类型)。
6. 泛型的”单态化”是什么意思?
- A. 将所有类型合并为一个
- B. 编译时为每种用到的具体类型生成专属代码
- C. 运行时动态决定类型
- D. 把泛型删除
点击查看答案
B。单态化(monomorphization)是 Rust 编译时用具体类型替换泛型参数的过程,保证零运行时开销。
7. 以下哪个是合法的泛型方法定义?
- A.
impl<T> Point<T> { fn get(&self) -> &T { &self.x } } - B.
impl Point<T> { fn get(&self) -> &T { &self.x } }(缺少<T>) - C.
impl Point { fn get(&self) -> T { self.x } } - D. 以上都合法
点击查看答案
A。impl<T> Point<T> 是正确的泛型方法声明。B 缺少 impl<T> 中的 <T>。
8. Vec<T> 中的 T 是指?
- A. 向量的维度
- B. 元素的类型
- C. 容量
- D. 索引类型
点击查看答案
B。Vec<T> 中的 T 是向量所存储元素的类型。
9. T: PartialOrd 约束表示什么?
- A. T 可以打印
- B. T 可以比较大小
- C. T 可以复制
- D. T 是浮点数
点击查看答案
B。PartialOrd trait 提供部分比较的功能(<, >, <=, >=)。
10. 泛型代码和手写多个具体类型代码的运行时性能相比?
- A. 泛型更慢
- B. 泛型更快
- C. 一样快(零成本抽象)
- D. 不确定
点击查看答案
C。Rust 泛型是零成本抽象,编译后和手写具体类型代码性能完全相同。
二、判断正误(每题2分,共20分)
1. 泛型只能在函数中使用。
点击查看答案
错误。泛型可用于函数、结构体、枚举、方法、trait 等。
2. Result<T, E> 有两个泛型参数。
点击查看答案
正确。Result<T, E> 有两个参数:T(成功类型)和 E(错误类型)。
3. 泛型参数名必须以 T 开头。
点击查看答案
错误。可以用任何名(如 T, U, K, V, E),但必须是大写驼峰。
4. impl<T> Point<T> 中的 <T> 必须写两次。
点击查看答案
正确。impl<T> 声明泛型参数,Point<T> 使用它。两者缺一不可。
5. 所有泛型参数都必须有 trait 约束。
点击查看答案
错误。trait 约束是可选的。fn foo<T>(x: T) 中的 T 可以在没有任何约束时使用(但只能做非常有限的事,如移动它)。
6. Option<i32> 等价于一个枚举,其变体为 Some(i32) 和 None。
点击查看答案
正确。Option<i32> 将泛型 T 替换为 i32。
7. struct Pair<T> { a: T, b: i32 } 中 a 和 b 类型相同。
点击查看答案
错误。a 是 T 类型,b 固定为 i32。只有 a 使用了泛型 T。
8. 泛型在运行时可能产生额外开销。
点击查看答案
错误。Rust 泛型是编译时处理的,运行时无额外开销。
9. Point<T> 和 Point<U> 是不同的类型(当 T 和 U 不同时)。
点击查看答案
正确。Point<i32> 和 Point<f64> 是两种不同的具体类型。
10. 可以为 Point<f64> 单独添加方法。
点击查看答案
正确。impl Point<f64> { ... } 只为 f64 版本添加方法。
三、代码分析(每题3分,共15分)
1. 下面代码的输出是什么?
struct Wrapper<T> {
value: T,
}
impl<T> Wrapper<T> {
fn new(value: T) -> Self {
Wrapper { value }
}
}
fn main() {
let w = Wrapper::new(42);
println!("{}", w.value);
}- A. 编译错误
- B. 42
- C. Wrapper { value: 42 }
- D. 0
点击查看答案
B。Wrapper::new(42) 创建了 Wrapper<i32>,w.value 是 42。
2. 下面代码能否编译通过?
fn add<T>(a: T, b: T) -> T {
a + b
}- A. 能
- B. 不能,T 没有
Addtrait 约束 - C. 能,但运行时可能出错
- D. 不能,泛型不能用于 + 运算符
点击查看答案
B。+ 运算符需要 T 实现 std::ops::Add trait,但这里没有约束 T,编译器不知道 T 是否能相加。
3. 下面代码的输出是什么?
fn identity<T>(x: T) -> T {
x
}
fn main() {
let a = identity(5);
let b = identity("hello");
println!("{} {}", a, b);
}- A. 5 hello
- B. 编译错误
- C. 5 5
- D. hello hello
点击查看答案
A。每次调用 identity 时编译器生成对应的具体版本,分别用于 i32 和 &str。
4. 下面代码有什么问题?
struct Container<T> {
item: T,
}
impl Container<T> {
fn get(&self) -> &T {
&self.item
}
}- A. 没有问题
- B.
impl缺了<T>声明 - C. 返回类型不对
- D. 结构体定义错误
点击查看答案
B。应该是 impl<T> Container<T>,缺少了 impl<T> 声明。
5. 下面代码的输出是什么?
enum Either<L, R> {
Left(L),
Right(R),
}
fn main() {
let x: Either<i32, f64> = Either::Left(42);
let y: Either<i32, f64> = Either::Right(3.14);
match x {
Either::Left(n) => println!("整数:{}", n),
Either::Right(n) => println!("浮点:{}", n),
}
match y {
Either::Left(n) => println!("整数:{}", n),
Either::Right(n) => println!("浮点:{}", n),
}
}- A. 整数:42(两行)
- B. 整数:42 然后 浮点:3.14
- C. 编译错误
- D. 两个 match 都输出”浮点”
点击查看答案
B。第一个 match x 匹配 Left(42),输出”整数:42”;第二个 match y 匹配 Right(3.14),输出”浮点:3.14”。
四、编程大题(15分)
题目: 设计一个泛型 Stack<T>(栈)结构体:
- 内部用
Vec<T>存数据 - 方法
new:创建空栈 - 方法
push:入栈 - 方法
pop:出栈,返回Option<T> - 方法
peek:查看栈顶,返回Option<&T> - 方法
is_empty:判断栈是否为空 - 方法
len:返回栈中元素个数 - 在 main 中分别用 i32 和 String 类型测试
点击查看答案
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
Stack { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn peek(&self) -> Option<&T> {
self.items.last()
}
fn is_empty(&self) -> bool {
self.items.is_empty()
}
fn len(&self) -> usize {
self.items.len()
}
}
fn main() {
// 用 i32 测试
let mut int_stack = Stack::new();
int_stack.push(1);
int_stack.push(2);
int_stack.push(3);
println!("长度:{},栈顶:{:?}", int_stack.len(), int_stack.peek());
while let Some(val) = int_stack.pop() {
println!("弹出:{}", val);
}
println!("空了?{}", int_stack.is_empty());
// 用 String 测试
let mut str_stack = Stack::new();
str_stack.push(String::from("你好"));
str_stack.push(String::from("世界"));
println!("栈顶:{:?}", str_stack.peek());
}评分标准:
- 结构体定义(2分)
- new(1分)
- push(2分)
- pop(2分)
- peek(2分)
- is_empty(1分)
- len(1分)
- main 测试代码(4分)
五、填空题(每题1分,共5分)
1. 泛型参数写在函数名后面的 ______ 括号中。
点击查看答案
<>(尖括号)。fn foo<T>(x: T)。
2. Option<______> 表示一个可能不存在的整数。
点击查看答案
i32(或任何整数类型)。Option<i32> 可能是 Some(42) 或 None。
3. ______ 是 trait 约束,表示 T 必须实现特定的功能。
点击查看答案
T: Trait。如 T: Display,T: PartialOrd。
4. 泛型结构体 struct Pair<T> { ... } 的 impl 块必须写成 impl<______> Pair<T> { ... }。
点击查看答案
T。impl<T> Pair<T>。
5. Result<______, ______> 有两个泛型参数,分别表示成功和错误的类型。
点击查看答案
T 和 E。Result<T, E> 中 T 是成功类型,E 是错误类型。
六、代码补全(共5分)
1. 补全泛型函数定义(2分)
fn first<______>(list: &[T]) -> &______ {
&list[0]
}点击查看答案
fn first<T>(list: &[T]) -> &T {2. 补全泛型结构体(2分)
struct Pair<______> {
first: T,
second: T,
}点击查看答案
struct Pair<T> {3. 补全 impl 声明(1分)
impl<______> Pair<T> {
fn swap(&mut self) {
std::mem::swap(&mut self.first, &mut self.second);
}
}点击查看答案
impl<T> Pair<T> {计分:概念40 + 判断20 + 代码分析15 + 编程15 + 填空5 + 补全5 = 总分100分
练习
练习
| 题号 | 题目 | 链接 | 知识点 |
|---|---|---|---|
| P1001 | A+B Problem | https://www.luogu.com.cn/problem/P1001 | 输入输出 |
| P1008 | 全排列 | https://www.luogu.com.cn/problem/P1008 | 递归、枚举 |
| P1010 | 幂次方 | https://www.luogu.com.cn/problem/P1010 | 函数、递归 |
| P1012 | 拼数 | https://www.luogu.com.cn/problem/P1012 | 字符串、排序 |