万能模板:泛型

学习目标

学完本章后,你将能够:

  • 理解泛型的核心概念:一份代码适用多种类型
  • 在函数中使用泛型参数
  • 在结构体中使用泛型
  • 在枚举中使用泛型
  • 理解为什么泛型能减少重复代码

一、问题:为什么需要泛型?

假设你想写一个函数,找出两个数中较大的那个:

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);
}
  • TU 只是名字,可以叫任何名字。常见名字: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。

12-共享行为: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. 模块
点击查看答案

COption<T> 是一个泛型枚举,T 可以是任何类型。

4. T: Display 的含义是?

  • A. T 必须叫 Display
  • B. T 必须实现 Display trait
  • C. T 可以显示
  • D. T 是一个字符串
点击查看答案

BT: 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. 以上都合法
点击查看答案

Aimpl<T> Point<T> 是正确的泛型方法声明。B 缺少 impl<T> 中的 <T>

8. Vec<T> 中的 T 是指?

  • A. 向量的维度
  • B. 元素的类型
  • C. 容量
  • D. 索引类型
点击查看答案

BVec<T> 中的 T 是向量所存储元素的类型。

9. T: PartialOrd 约束表示什么?

  • A. T 可以打印
  • B. T 可以比较大小
  • C. T 可以复制
  • D. T 是浮点数
点击查看答案

BPartialOrd 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
点击查看答案

BWrapper::new(42) 创建了 Wrapper<i32>w.value 是 42。

2. 下面代码能否编译通过?

fn add<T>(a: T, b: T) -> T {
    a + b
}
  • A. 能
  • B. 不能,T 没有 Add trait 约束
  • 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>(栈)结构体:

  1. 内部用 Vec<T> 存数据
  2. 方法 new:创建空栈
  3. 方法 push:入栈
  4. 方法 pop:出栈,返回 Option<T>
  5. 方法 peek:查看栈顶,返回 Option<&T>
  6. 方法 is_empty:判断栈是否为空
  7. 方法 len:返回栈中元素个数
  8. 在 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: DisplayT: PartialOrd

4. 泛型结构体 struct Pair<T> { ... } 的 impl 块必须写成 impl<______> Pair<T> { ... }

点击查看答案

Timpl<T> Pair<T>

5. Result<______, ______> 有两个泛型参数,分别表示成功和错误的类型。

点击查看答案

TEResult<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分


练习

练习

题号题目链接知识点
P1001A+B Problemhttps://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字符串、排序