《Rust编程实战》系列第47篇
上一篇文章中,我们学习了Rust的
Trait Object,了解了dyn Trait、Box<dyn Trait>以及动态分发。本篇继续学习Rust类型系统中另一项非常核心的能力:
Generics
中文通常称为"泛型"。
泛型解决的问题可以简单理解为:
在不知道具体类型的情况下,编写可以复用于多种类型的代码。
假设我们需要分别查找
i32、f64中的最大值,如果不用泛型,可能会写:
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 -
复杂业务泛型重构案例