Rust 泛型 Generics详解:编写可复用且类型安全的代码

《Rust编程实战》系列第47篇

上一篇文章中,我们学习了Rust的Trait Object,了解了dyn TraitBox<dyn Trait>以及动态分发。本篇继续学习Rust类型系统中另一项非常核心的能力:

复制代码
Generics

中文通常称为"泛型"。

泛型解决的问题可以简单理解为:

在不知道具体类型的情况下,编写可以复用于多种类型的代码。

假设我们需要分别查找i32f64中的最大值,如果不用泛型,可能会写:

css 复制代码
fn max_i32(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}
fn max_f64(a: f64, b: f64) -> f64 {
    if a > b { a } else { b }
}

两个函数逻辑完全一样,只是类型不同。Rust可以通过泛型把重复代码合并:

less 复制代码
fn max<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

这里:

r 复制代码
T

不是某个具体类型,而是类型参数。调用时可以根据参数自动推断:

ini 复制代码
let a = max(10, 20);
let b = max(10.5, 20.5);

本文将介绍:

  • 什么是泛型

  • 泛型函数

  • 泛型类型参数

  • 多个泛型参数

  • 泛型Struct

  • 泛型Enum

  • 泛型impl

  • Trait Bound

  • 泛型与where

  • 泛型与生命周期

  • 泛型与Trait

  • 泛型与Trait Object

  • 单态化Monomorphization

  • 泛型性能

  • Response、分页、Repository实战

什么是泛型

泛型就是把代码中的具体类型抽象成一个类型参数。

普通函数:

rust 复制代码
fn identity(value: i32) -> i32 {
    value
}

只能处理:

rust 复制代码
i32

如果改成泛型:

r 复制代码
fn identity<T>(value: T) -> T {
    value
}

就可以处理多种类型:

ini 复制代码
fn main() {
    let number = identity(100);
    let text = identity(String::from("Rust"));
    let flag = identity(true);
    println!("{}", number);
    println!("{}", text);
    println!("{}", flag);
}

这里的:

r 复制代码
<T>

表示声明一个泛型类型参数。

T只是习惯名称,也可以写:

csharp 复制代码
fn identity<Value>(value: Value) -> Value {
    value
}

但Rust生态中通常优先使用:

r 复制代码
T
U
V
K
V
E

等简短名称。

泛型函数

最基本的泛型函数:

csharp 复制代码
fn print_value<T>(value: T) {
}

这里函数可以接收任意类型。

但如果我们这样写:

csharp 复制代码
fn print_value<T>(value: T) {
    println!("{}", value);
}

会报错。

原因是Rust并不知道:

r 复制代码
T

一定支持:

复制代码
{}

格式化。

因此泛型不是:

什么操作都可以执行。

而是:

对类型没有提出要求时,只能执行所有类型都允许的通用操作。

如果需要打印,可以添加Trait约束:

rust 复制代码
use std::fmt::Display;
fn print_value<T: Display>(value: T) {
    println!("{}", value);
}

这表示:

T必须实现Display Trait。

泛型类型由调用者确定

例如:

r 复制代码
fn identity<T>(value: T) -> T {
    value
}

调用:

ini 复制代码
let value = identity(100);

编译器会推断:

ini 复制代码
T = i32

调用:

ini 复制代码
let value = identity(String::from("Rust"));

则:

ini 复制代码
T = String

通常不需要显式写具体类型。

如果有需要,也可以使用:

ini 复制代码
let value = identity::<i32>(100);

这里:

ruby 复制代码
::<i32>

通常称为Turbofish语法。

标准库中经常看到:

rust 复制代码
"100".parse::<i32>()

多个泛型参数

一个函数可以声明多个泛型:

sql 复制代码
fn pair<T, U>(
    first: T,
    second: U,
) -> (T, U) {
    (first, second)
}

使用:

rust 复制代码
let value = pair(
    100,
    String::from("Rust"),
);

这里:

ini 复制代码
T = i32
U = String

返回类型:

rust 复制代码
(i32, String)

如果写:

r 复制代码
fn pair<T>(
    first: T,
    second: T,
) -> (T, T)

则要求两个参数必须是同一个具体类型。

同一个T表示相同类型

例如:

r 复制代码
fn compare<T>(
    first: T,
    second: T,
) {
}

可以:

scss 复制代码
compare(10, 20);

因为两个参数都是i32

但下面通常不行:

scss 复制代码
compare(10, 20.5);

因为:

rust 复制代码
i32 != f64

如果希望类型不同:

csharp 复制代码
fn compare<T, U>(
    first: T,
    second: U,
) {
}

这也是理解泛型签名时非常重要的一点:

相同泛型参数名代表相同具体类型,不同泛型参数允许不同类型。

泛型Struct

Struct也可以使用泛型。

例如:

r 复制代码
#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

创建整数坐标:

ini 复制代码
let first = Point {
    x: 10,
    y: 20,
};

这里:

ini 复制代码
T = i32

创建浮点坐标:

ini 复制代码
let second = Point {
    x: 10.5,
    y: 20.8,
};

这里:

ini 复制代码
T = f64

同一个Struct定义可以复用到多种类型。

Struct多个泛型参数

如果两个字段允许不同类型:

r 复制代码
struct Point<T, U> {
    x: T,
    y: U,
}

可以创建:

ini 复制代码
let point = Point {
    x: 10,
    y: 20.5,
};

此时:

ini 复制代码
T = i32
U = f64

如果只写:

r 复制代码
struct Point<T> {
    x: T,
    y: T,
}

则x和y必须是同一个具体类型。

泛型Enum

Enum同样支持泛型。

其实我们之前已经使用过很多次:

r 复制代码
Option<T>

概念上类似:

scss 复制代码
enum Option<T> {
    Some(T),
    None,
}

所以:

scss 复制代码
Some(100)

可以理解为:

rust 复制代码
Option<i32>

而:

less 复制代码
Some(String::from("Rust"))

就是:

rust 复制代码
Option<String>

另一个经典例子是:

r 复制代码
Result<T, E>

概念类似:

scss 复制代码
enum Result<T, E> {
    Ok(T),
    Err(E),
}

其中:

ini 复制代码
T = 成功结果类型
E = 错误类型

例如:

rust 复制代码
Result<User, String>

自定义泛型Enum

例如API结果:

scss 复制代码
#[derive(Debug)]
enum ApiResult<T> {
    Success(T),
    Error(String),
}

可以返回User:

css 复制代码
let result = ApiResult::Success(
    User {
        name: String::from("Tom"),
    }
);

也可以返回商品:

css 复制代码
let result = ApiResult::Success(
    Product {
        name: String::from("Rust Book"),
    }
);

无需分别创建:

复制代码
UserResult
ProductResult
OrderResult

泛型可以极大减少重复类型。

泛型impl

泛型Struct的方法实现也需要声明泛型。

例如:

rust 复制代码
struct Point<T> {
    x: T,
    y: T,
}
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
    fn y(&self) -> &T {
        &self.y
    }
}

这里:

r 复制代码
impl<T>

声明:

接下来为任意T的Point实现方法。

调用:

arduino 复制代码
let point = Point {
    x: 10,
    y: 20,
};
println!("{}", point.x());

为特定泛型类型实现方法

并不是所有方法都必须对任意T开放。

例如:

rust 复制代码
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (
            self.x.powi(2)
                + self.y.powi(2)
        ).sqrt()
    }
}

这里的方法只属于:

rust 复制代码
Point<f64>

因此:

yaml 复制代码
Point {
    x: 1.0,
    y: 2.0,
}

可以调用。

但:

yaml 复制代码
Point {
    x: 1,
    y: 2,
}

不能直接调用这个方法。

这种设计非常灵活:

可以为所有泛型类型提供通用方法,也可以为特定具体类型提供专属方法。

impl中的新泛型参数

方法本身也可以定义新的泛型。

例如:

csharp 复制代码
struct Pair<T> {
    first: T,
    second: T,
}
impl<T> Pair<T> {
    fn mix<U>(
        self,
        value: U,
    ) -> (T, U) {
        (self.first, value)
    }
}

这里:

r 复制代码
T

来自Struct。

而:

复制代码
U

只属于mix()方法。

泛型为什么经常需要Trait Bound

假设:

sql 复制代码
fn largest<T>(
    first: T,
    second: T,
) -> T {
    if first > second {
        first
    } else {
        second
    }
}

这会报错,因为不是所有类型都支持:

markdown 复制代码
>

Rust需要明确知道:

r 复制代码
T

具备比较能力。

可以写:

sql 复制代码
fn largest<T: PartialOrd>(
    first: T,
    second: T,
) -> T {
    if first > second {
        first
    } else {
        second
    }
}

这里:

makefile 复制代码
T: PartialOrd

表示:

T必须实现PartialOrd。

这就是:

复制代码
Trait Bound
Trait约束

多个Trait Bound

如果还需要打印:

sql 复制代码
use std::fmt::Display;
fn largest<T>(
    first: T,
    second: T,
) -> T
where
    T: PartialOrd + Display,
{
    println!(
        "{} 和 {}",
        first,
        second
    );
    if first > second {
        first
    } else {
        second
    }
}

一个泛型可以同时要求实现多个Trait:

makefile 复制代码
T: PartialOrd + Display

以后还会经常看到:

rust 复制代码
T: Clone + Debug + Send + Sync

泛型与where

简单约束:

php 复制代码
fn show<T: Display>(value: T)

非常直观。

但泛型多起来后:

rust 复制代码
fn process<
    T: Display + Clone,
    U: Debug + PartialEq,
>(
    first: T,
    second: U,
)

会比较难读。

这时可以使用:

rust 复制代码
fn process<T, U>(
    first: T,
    second: U,
)
where
    T: Display + Clone,
    U: Debug + PartialEq,
{
}

where不会改变泛型能力,只是让复杂约束更加清晰。

下一篇会专门详细讲解它。

泛型与生命周期

泛型和生命周期可以同时出现:

rust 复制代码
fn longest<'a, T>(
    first: &'a T,
    second: &'a T,
) -> &'a T
where
    T: PartialOrd,
{
    if first > second {
        first
    } else {
        second
    }
}

这里:

css 复制代码
'a

描述引用生命周期。

r 复制代码
T

描述数据类型。

makefile 复制代码
T: PartialOrd

描述类型必须具备的能力。

可以简单理解:

复制代码
生命周期回答:引用多久有效
泛型回答:这是什么类型
Trait Bound回答:这个类型必须能做什么

泛型与Trait

泛型经常与Trait结合。

例如:

rust 复制代码
trait Summary {
    fn summary(&self) -> String;
}
fn print<T: Summary>(
    value: &T,
) {
    println!("{}", value.summary());
}

泛型负责:

复制代码
允许不同具体类型进入函数

Trait负责:

css 复制代码
限制这些类型必须具备Summary能力

因此Rust中的泛型很少完全脱离Trait存在。

泛型与impl Trait

下面:

less 复制代码
fn show<T: Summary>(
    value: &T,
)

和:

php 复制代码
fn show(
    value: &impl Summary,
)

在简单情况下用途类似。

impl Trait更加简洁。

泛型写法更适合:

  • 多个参数需要保持同一类型

  • 需要多个泛型参数

  • 复杂Trait Bound

  • 返回类型与输入类型存在关系

    例如:

less 复制代码
fn same<T: Summary>(
    first: &T,
    second: &T,
)

明确要求两个参数具体类型相同。

泛型与Trait Object

泛型:

php 复制代码
fn run<T: Plugin>(
    plugin: &T,
)

通常使用:

复制代码
静态分发

Trait Object:

php 复制代码
fn run(
    plugin: &dyn Plugin,
)

使用:

复制代码
动态分发

泛型适合:

  • 编译时知道具体类型

  • 追求静态优化

  • 类型数量固定在编译阶段

    Trait Object适合:

  • 运行时选择类型

  • Vec<Box<dyn Trait>>

  • 插件系统

  • 动态组件

    二者都是多态,只是实现方式不同。

泛型的单态化

Rust泛型性能非常重要的一点是:

复制代码
Monomorphization
单态化

例如:

scss 复制代码
fn identity<T>(value: T) -> T {
    value
}
fn main() {
    identity(10);
    identity(20.5);
}

编译器会根据实际使用的类型,生成类似:

rust 复制代码
fn identity_i32(
    value: i32,
) -> i32 {
    value
}
fn identity_f64(
    value: f64,
) -> f64 {
    value
}

真正代码当然由编译器自动完成。

所以Rust泛型并不是运行时不断判断:

r 复制代码
T到底是什么?

而是在编译阶段生成对应的具体版本。

泛型是不是有运行时开销

由于单态化,很多泛型代码可以做到:

复制代码
Zero-cost Abstraction
零成本抽象

例如:

rust 复制代码
Vec<i32>
Vec<String>

在编译后会针对具体类型生成对应代码。

通常不会因为使用泛型本身产生Java式运行时类型判断开销。

但代价可能是:

使用大量不同泛型实例时,最终生成的机器代码会增加。

这可能影响:

复制代码
编译时间
二进制体积

因此静态泛型和动态Trait Object各有适用场景。

泛型实战:API响应

Web项目经常需要统一返回结构:

rust 复制代码
#[derive(Debug)]
struct Response<T> {
    code: u16,
    message: String,
    data: Option<T>,
}

成功响应:

rust 复制代码
impl<T> Response<T> {
    fn success(data: T) -> Self {
        Self {
            code: 200,
            message: String::from("success"),
            data: Some(data),
        }
    }
    fn error(
        message: &str,
    ) -> Self {
        Self {
            code: 500,
            message: message.to_string(),
            data: None,
        }
    }
}

用户接口:

css 复制代码
let response =
    Response::success(User {
        name: String::from("Tom"),
    });

商品接口:

css 复制代码
let response =
    Response::success(Product {
        name: String::from("Rust Book"),
    });

统一使用:

r 复制代码
Response<T>

无需为每种业务类型定义重复响应结构。

泛型实战:分页数据

分页接口通常可以定义:

rust 复制代码
#[derive(Debug)]
struct Page<T> {
    page: u32,
    page_size: u32,
    total: u64,
    items: Vec<T>,
}

用户分页:

sql 复制代码
Page<User>

商品分页:

swift 复制代码
Page<Product>

订单分页:

css 复制代码
Page<Order>

这就是泛型在Web后端中非常典型的实际用途。

泛型实战:Repository

假设我们设计通用数据仓储接口:

rust 复制代码
trait Repository<T> {
    fn find(
        &self,
        id: u64,
    ) -> Option<T>;
    fn save(
        &mut self,
        value: T,
    );
}

用户仓储:

rust 复制代码
struct UserRepository {
    users: Vec<User>,
}
impl Repository<User>
    for UserRepository
{
    fn find(
        &self,
        id: u64,
    ) -> Option<User> {
        self.users
            .iter()
            .find(|user| user.id == id)
            .cloned()
    }
    fn save(
        &mut self,
        value: User,
    ) {
        self.users.push(value);
    }
}

这里:

r 复制代码
Repository<T>

定义了一套通用数据访问能力,而具体实现决定:

sql 复制代码
T到底是User、Product还是Order

泛型实战:容器类型

例如设计一个简单容器:

rust 复制代码
#[derive(Debug)]
struct Container<T> {
    value: T,
}
impl<T> Container<T> {
    fn new(value: T) -> Self {
        Self {
            value,
        }
    }
    fn value(&self) -> &T {
        &self.value
    }
    fn into_inner(self) -> T {
        self.value
    }
}

使用:

php 复制代码
let first =
    Container::new(100);
let second =
    Container::new(String::from("Rust"));

同一个Container可以存储任意类型。

常见错误

认为T可以执行任何操作

错误:

css 复制代码
fn add<T>(
    a: T,
    b: T,
) -> T {
    a + b
}

Rust不知道T一定支持+

需要添加对应Trait Bound。

相同T传入不同类型

例如:

r 复制代码
fn pair<T>(
    a: T,
    b: T,
)

不能随意传:

rust 复制代码
i32 + String

需要改成:

less 复制代码
fn pair<T, U>(
    a: T,
    b: U,
)

泛型参数定义了却没有使用

例如:

arduino 复制代码
struct User<T> {
    name: String,
}

如果T没有出现在字段中,编译器无法知道T如何参与类型结构,通常会报未使用类型参数相关问题。除非使用:

r 复制代码
PhantomData<T>

等高级技术。

Trait Bound太多

如果方法只需要打印,不应该要求:

rust 复制代码
T: Display + Clone + Debug + Send + Sync

只添加真正需要的约束。

为简单业务过度泛型化

例如一个函数永远只处理:

sql 复制代码
User

却设计十几个泛型参数,会增加代码理解成本。

泛型的目标是减少重复,而不是让类型签名变复杂。

泛型设计最佳实践

真正存在重复逻辑时再抽象

如果多个类型拥有完全相同的数据处理流程,可以考虑泛型。

类型之间需要共同能力时配合Trait

例如:

makefile 复制代码
T: Display
T: Clone
T: Repository

简单Trait约束直接写在<>

例如:

php 复制代码
fn show<T: Display>(
    value: T,
)

复杂约束使用where

例如:

rust 复制代码
fn process<T, U>(
    first: T,
    second: U,
)
where
    T: Display + Clone,
    U: Debug,
{
}

API响应和容器非常适合泛型

例如:

r 复制代码
Response<T>
Page<T>
Result<T,E>
Option<T>
Container<T>

不要为了"高级"而泛型

如果代码只有一个明确类型,直接使用具体类型往往更加清晰。

泛型与Trait Object按场景选择

编译阶段确定类型:

makefile 复制代码
T: Trait

运行时多类型:

rust 复制代码
dyn Trait

本章小结

泛型是Rust实现代码复用、类型抽象和静态多态的重要工具。

本文学习了:

  • 使用<T>声明泛型类型参数

  • 泛型函数可以处理多种具体类型

  • 多个参数使用同一个T时必须是相同具体类型

  • 使用<T,U>可以表示不同类型

  • Struct和Enum都支持泛型

  • Option<T>Result<T,E>本身就是泛型类型

  • 泛型Struct可以使用impl<T>定义方法

  • 可以为特定具体泛型类型实现专属方法

  • 泛型通常需要通过Trait Bound限制能力

  • 多个Trait使用+组合

  • 复杂Trait约束可以使用where

  • 泛型可以和生命周期、Trait一起使用

  • 泛型通常使用静态分发

  • Trait Object使用动态分发

  • Rust通过单态化生成具体类型代码

  • 泛型通常可以实现零成本抽象

  • Response<T>Page<T>Repository<T>是典型工程应用

    可以记住:

泛型解决"同一套代码如何适配不同类型"。

Trait Bound解决"这些不同类型必须具备什么能力"。

生命周期解决"这些引用能够安全存在多久"。

泛型不是让类型变得模糊,而是在保持严格类型安全的同时减少重复代码。

下一篇预告

下一篇我们将专门深入泛型代码中非常常见的约束语法:

Rust where详解:让泛型与Trait约束更加清晰

内容包括:

  • 什么是where

  • 为什么需要where

  • T: Trait与where区别

  • 多个泛型约束

  • 多个Trait Bound

  • 生命周期约束

  • T: 'a

  • 'a: 'b

  • 泛型Struct中的where

  • impl中的where

  • Trait中的where

  • 关联类型约束

  • where Self: Sized

  • 复杂业务泛型重构案例

相关推荐
小满zs1 小时前
Go语言第九章(错误处理)
后端·go
桦说编程1 小时前
深入理解 FutureTask 状态机——从契约到实现
java·后端·性能优化
程序员爱钓鱼1 小时前
Go 编程实战:指针 Pointer——理解地址、取址与解引用
后端·面试·go
To_OC10 小时前
从一行入口代码啃透 NestJS:工厂模式、装饰器和依赖注入是怎么串起来的
后端·设计模式·nestjs
Python私教10 小时前
多个项目怎么安全合并?先适配,再切换
后端·python·架构
黄敬峰10 小时前
告别 Vibe Coding!SDD 规范驱动开发实战:从文档到代码的 AI 开发新范式
面试
Python私教12 小时前
从表格到管理系统:别先写页面,先补齐权限、流程和审计
数据库·后端·架构
北斗落凡尘12 小时前
LangGraph 入门实战(12)--使用MCP
后端·python·langchain
嗯哼python12 小时前
从 0 到 1 构建 AI 模拟面试系统:RAG + LangGraph 的工程实践
人工智能·面试·职场和发展