《Rust编程实战》系列第47篇
上一篇文章中,我们学习了Rust的Trait Object,了解了dyn Trait、Box<dyn Trait>以及动态分发。本篇继续学习Rust类型系统中另一项非常核心的能力:
1Generics 2
中文通常称为“泛型”。
泛型解决的问题可以简单理解为:
在不知道具体类型的情况下,编写可以复用于多种类型的代码。
假设我们需要分别查找i32、f64中的最大值,如果不用泛型,可能会写:
1fn max_i32(a: i32, b: i32) -> i32 { 2 if a > b { a } else { b } 3} 4fn max_f64(a: f64, b: f64) -> f64 { 5 if a > b { a } else { b } 6} 7
两个函数逻辑完全一样,只是类型不同。Rust可以通过泛型把重复代码合并:
1fn max<T: PartialOrd>(a: T, b: T) -> T { 2 if a > b { a } else { b } 3} 4
这里:
1T 2
不是某个具体类型,而是类型参数。调用时可以根据参数自动推断:
1let a = max(10, 20); 2let b = max(10.5, 20.5); 3
本文将介绍:
- 什么是泛型
- 泛型函数
- 泛型类型参数
- 多个泛型参数
- 泛型Struct
- 泛型Enum
- 泛型impl
- Trait Bound
- 泛型与where
- 泛型与生命周期
- 泛型与Trait
- 泛型与Trait Object
- 单态化Monomorphization
- 泛型性能
- Response、分页、Repository实战
什么是泛型
泛型就是把代码中的具体类型抽象成一个类型参数。
普通函数:
1fn identity(value: i32) -> i32 { 2 value 3} 4
只能处理:
1i32 2
如果改成泛型:
1fn identity<T>(value: T) -> T { 2 value 3} 4
就可以处理多种类型:
1fn main() { 2 let number = identity(100); 3 let text = identity(String::from("Rust")); 4 let flag = identity(true); 5 println!("{}", number); 6 println!("{}", text); 7 println!("{}", flag); 8} 9
这里的:
1<T> 2
表示声明一个泛型类型参数。
T只是习惯名称,也可以写:
1fn identity<Value>(value: Value) -> Value { 2 value 3} 4
但Rust生态中通常优先使用:
1T 2U 3V 4K 5V 6E 7
等简短名称。
泛型函数
最基本的泛型函数:
1fn print_value<T>(value: T) { 2} 3
这里函数可以接收任意类型。
但如果我们这样写:
1fn print_value<T>(value: T) { 2 println!("{}", value); 3} 4
会报错。
原因是Rust并不知道:
1T 2
一定支持:
1{} 2
格式化。
因此泛型不是:
什么操作都可以执行。
而是:
对类型没有提出要求时,只能执行所有类型都允许的通用操作。
如果需要打印,可以添加Trait约束:
1use std::fmt::Display; 2fn print_value<T: Display>(value: T) { 3 println!("{}", value); 4} 5
这表示:
T必须实现Display Trait。
泛型类型由调用者确定
例如:
1fn identity<T>(value: T) -> T { 2 value 3} 4
调用:
1let value = identity(100); 2
编译器会推断:
1T = i32 2
调用:
1let value = identity(String::from("Rust")); 2
则:
1T = String 2
通常不需要显式写具体类型。
如果有需要,也可以使用:
1let value = identity::<i32>(100); 2
这里:
1::<i32> 2
通常称为Turbofish语法。
标准库中经常看到:
1"100".parse::<i32>() 2
多个泛型参数
一个函数可以声明多个泛型:
1fn pair<T, U>( 2 first: T, 3 second: U, 4) -> (T, U) { 5 (first, second) 6} 7
使用:
1let value = pair( 2 100, 3 String::from("Rust"), 4); 5
这里:
1T = i32 2U = String 3
返回类型:
1(i32, String) 2
如果写:
1fn pair<T>( 2 first: T, 3 second: T, 4) -> (T, T) 5
则要求两个参数必须是同一个具体类型。
同一个T表示相同类型
例如:
1fn compare<T>( 2 first: T, 3 second: T, 4) { 5} 6
可以:
1compare(10, 20); 2
因为两个参数都是i32。
但下面通常不行:
1compare(10, 20.5); 2
因为:
1i32 != f64 2
如果希望类型不同:
1fn compare<T, U>( 2 first: T, 3 second: U, 4) { 5} 6
这也是理解泛型签名时非常重要的一点:
相同泛型参数名代表相同具体类型,不同泛型参数允许不同类型。
泛型Struct
Struct也可以使用泛型。
例如:
1#[derive(Debug)] 2struct Point<T> { 3 x: T, 4 y: T, 5} 6
创建整数坐标:
1let first = Point { 2 x: 10, 3 y: 20, 4}; 5
这里:
1T = i32 2
创建浮点坐标:
1let second = Point { 2 x: 10.5, 3 y: 20.8, 4}; 5
这里:
1T = f64 2
同一个Struct定义可以复用到多种类型。
Struct多个泛型参数
如果两个字段允许不同类型:
1struct Point<T, U> { 2 x: T, 3 y: U, 4} 5
可以创建:
1let point = Point { 2 x: 10, 3 y: 20.5, 4}; 5
此时:
1T = i32 2U = f64 3
如果只写:
1struct Point<T> { 2 x: T, 3 y: T, 4} 5
则x和y必须是同一个具体类型。
泛型Enum
Enum同样支持泛型。
其实我们之前已经使用过很多次:
1Option<T> 2
概念上类似:
1enum Option<T> { 2 Some(T), 3 None, 4} 5
所以:
1Some(100) 2
可以理解为:
1Option<i32> 2
而:
1Some(String::from("Rust")) 2
就是:
1Option<String> 2
另一个经典例子是:
1Result<T, E> 2
概念类似:
1enum Result<T, E> { 2 Ok(T), 3 Err(E), 4} 5
其中:
1T = 成功结果类型 2E = 错误类型 3
例如:
1Result<User, String> 2
自定义泛型Enum
例如API结果:
1#[derive(Debug)] 2enum ApiResult<T> { 3 Success(T), 4 Error(String), 5} 6
可以返回User:
1let result = ApiResult::Success( 2 User { 3 name: String::from("Tom"), 4 } 5); 6
也可以返回商品:
1let result = ApiResult::Success( 2 Product { 3 name: String::from("Rust Book"), 4 } 5); 6
无需分别创建:
1UserResult 2ProductResult 3OrderResult 4
泛型可以极大减少重复类型。
泛型impl
泛型Struct的方法实现也需要声明泛型。
例如:
1struct Point<T> { 2 x: T, 3 y: T, 4} 5impl<T> Point<T> { 6 fn x(&self) -> &T { 7 &self.x 8 } 9 fn y(&self) -> &T { 10 &self.y 11 } 12} 13
这里:
1impl<T> 2
声明:
接下来为任意T的Point实现方法。
调用:
1let point = Point { 2 x: 10, 3 y: 20, 4}; 5println!("{}", point.x()); 6
为特定泛型类型实现方法
并不是所有方法都必须对任意T开放。
例如:
1impl Point<f64> { 2 fn distance_from_origin(&self) -> f64 { 3 ( 4 self.x.powi(2) 5 + self.y.powi(2) 6 ).sqrt() 7 } 8} 9
这里的方法只属于:
1Point<f64> 2
因此:
1Point { 2 x: 1.0, 3 y: 2.0, 4} 5
可以调用。
但:
1Point { 2 x: 1, 3 y: 2, 4} 5
不能直接调用这个方法。
这种设计非常灵活:
可以为所有泛型类型提供通用方法,也可以为特定具体类型提供专属方法。
impl中的新泛型参数
方法本身也可以定义新的泛型。
例如:
1struct Pair<T> { 2 first: T, 3 second: T, 4} 5impl<T> Pair<T> { 6 fn mix<U>( 7 self, 8 value: U, 9 ) -> (T, U) { 10 (self.first, value) 11 } 12} 13
这里:
1T 2
来自Struct。
而:
1U 2
只属于mix()方法。
泛型为什么经常需要Trait Bound
假设:
1fn largest<T>( 2 first: T, 3 second: T, 4) -> T { 5 if first > second { 6 first 7 } else { 8 second 9 } 10} 11
这会报错,因为不是所有类型都支持:
1> 2
Rust需要明确知道:
1T 2
具备比较能力。
可以写:
1fn largest<T: PartialOrd>( 2 first: T, 3 second: T, 4) -> T { 5 if first > second { 6 first 7 } else { 8 second 9 } 10} 11
这里:
1T: PartialOrd 2
表示:
T必须实现PartialOrd。
这就是:
1Trait Bound 2Trait约束 3
多个Trait Bound
如果还需要打印:
1use std::fmt::Display; 2fn largest<T>( 3 first: T, 4 second: T, 5) -> T 6where 7 T: PartialOrd + Display, 8{ 9 println!( 10 "{} 和 {}", 11 first, 12 second 13 ); 14 if first > second { 15 first 16 } else { 17 second 18 } 19} 20
一个泛型可以同时要求实现多个Trait:
1T: PartialOrd + Display 2
以后还会经常看到:
1T: Clone + Debug + Send + Sync 2
泛型与where
简单约束:
1fn show<T: Display>(value: T) 2
非常直观。
但泛型多起来后:
1fn process< 2 T: Display + Clone, 3 U: Debug + PartialEq, 4>( 5 first: T, 6 second: U, 7) 8
会比较难读。
这时可以使用:
1fn process<T, U>( 2 first: T, 3 second: U, 4) 5where 6 T: Display + Clone, 7 U: Debug + PartialEq, 8{ 9} 10
where不会改变泛型能力,只是让复杂约束更加清晰。
下一篇会专门详细讲解它。
泛型与生命周期
泛型和生命周期可以同时出现:
1fn longest<'a, T>( 2 first: &'a T, 3 second: &'a T, 4) -> &'a T 5where 6 T: PartialOrd, 7{ 8 if first > second { 9 first 10 } else { 11 second 12 } 13} 14
这里:
1'a 2
描述引用生命周期。
1T 2
描述数据类型。
1T: PartialOrd 2
描述类型必须具备的能力。
可以简单理解:
1生命周期回答:引用多久有效 2泛型回答:这是什么类型 3Trait Bound回答:这个类型必须能做什么 4
泛型与Trait
泛型经常与Trait结合。
例如:
1trait Summary { 2 fn summary(&self) -> String; 3} 4fn print<T: Summary>( 5 value: &T, 6) { 7 println!("{}", value.summary()); 8} 9
泛型负责:
1允许不同具体类型进入函数 2
Trait负责:
1限制这些类型必须具备Summary能力 2
因此Rust中的泛型很少完全脱离Trait存在。
泛型与impl Trait
下面:
1fn show<T: Summary>( 2 value: &T, 3) 4
和:
1fn show( 2 value: &impl Summary, 3) 4
在简单情况下用途类似。
impl Trait更加简洁。
泛型写法更适合:
- 多个参数需要保持同一类型
- 需要多个泛型参数
- 复杂Trait Bound
- 返回类型与输入类型存在关系
例如:
1fn same<T: Summary>( 2 first: &T, 3 second: &T, 4) 5
明确要求两个参数具体类型相同。
泛型与Trait Object
泛型:
1fn run<T: Plugin>( 2 plugin: &T, 3) 4
通常使用:
1静态分发 2
Trait Object:
1fn run( 2 plugin: &dyn Plugin, 3) 4
使用:
1动态分发 2
泛型适合:
- 编译时知道具体类型
- 追求静态优化
- 类型数量固定在编译阶段
Trait Object适合: - 运行时选择类型
Vec<Box<dyn Trait>>- 插件系统
- 动态组件
二者都是多态,只是实现方式不同。
泛型的单态化
Rust泛型性能非常重要的一点是:
1Monomorphization 2单态化 3
例如:
1fn identity<T>(value: T) -> T { 2 value 3} 4fn main() { 5 identity(10); 6 identity(20.5); 7} 8
编译器会根据实际使用的类型,生成类似:
1fn identity_i32( 2 value: i32, 3) -> i32 { 4 value 5} 6fn identity_f64( 7 value: f64, 8) -> f64 { 9 value 10} 11
真正代码当然由编译器自动完成。
所以Rust泛型并不是运行时不断判断:
1T到底是什么? 2
而是在编译阶段生成对应的具体版本。
泛型是不是有运行时开销
由于单态化,很多泛型代码可以做到:
1Zero-cost Abstraction 2零成本抽象 3
例如:
1Vec<i32> 2Vec<String> 3
在编译后会针对具体类型生成对应代码。
通常不会因为使用泛型本身产生Java式运行时类型判断开销。
但代价可能是:
使用大量不同泛型实例时,最终生成的机器代码会增加。
这可能影响:
1编译时间 2二进制体积 3
因此静态泛型和动态Trait Object各有适用场景。
泛型实战:API响应
Web项目经常需要统一返回结构:
1#[derive(Debug)] 2struct Response<T> { 3 code: u16, 4 message: String, 5 data: Option<T>, 6} 7
成功响应:
1impl<T> Response<T> { 2 fn success(data: T) -> Self { 3 Self { 4 code: 200, 5 message: String::from("success"), 6 data: Some(data), 7 } 8 } 9 fn error( 10 message: &str, 11 ) -> Self { 12 Self { 13 code: 500, 14 message: message.to_string(), 15 data: None, 16 } 17 } 18} 19
用户接口:
1let response = 2 Response::success(User { 3 name: String::from("Tom"), 4 }); 5
商品接口:
1let response = 2 Response::success(Product { 3 name: String::from("Rust Book"), 4 }); 5
统一使用:
1Response<T> 2
无需为每种业务类型定义重复响应结构。
泛型实战:分页数据
分页接口通常可以定义:
1#[derive(Debug)] 2struct Page<T> { 3 page: u32, 4 page_size: u32, 5 total: u64, 6 items: Vec<T>, 7} 8
用户分页:
1Page<User> 2
商品分页:
1Page<Product> 2
订单分页:
1Page<Order> 2
这就是泛型在Web后端中非常典型的实际用途。
泛型实战:Repository
假设我们设计通用数据仓储接口:
1trait Repository<T> { 2 fn find( 3 &self, 4 id: u64, 5 ) -> Option<T>; 6 fn save( 7 &mut self, 8 value: T, 9 ); 10} 11
用户仓储:
1struct UserRepository { 2 users: Vec<User>, 3} 4impl Repository<User> 5 for UserRepository 6{ 7 fn find( 8 &self, 9 id: u64, 10 ) -> Option<User> { 11 self.users 12 .iter() 13 .find(|user| user.id == id) 14 .cloned() 15 } 16 fn save( 17 &mut self, 18 value: User, 19 ) { 20 self.users.push(value); 21 } 22} 23
这里:
1Repository<T> 2
定义了一套通用数据访问能力,而具体实现决定:
1T到底是User、Product还是Order 2
泛型实战:容器类型
例如设计一个简单容器:
1#[derive(Debug)] 2struct Container<T> { 3 value: T, 4} 5impl<T> Container<T> { 6 fn new(value: T) -> Self { 7 Self { 8 value, 9 } 10 } 11 fn value(&self) -> &T { 12 &self.value 13 } 14 fn into_inner(self) -> T { 15 self.value 16 } 17} 18
使用:
1let first = 2 Container::new(100); 3let second = 4 Container::new(String::from("Rust")); 5
同一个Container可以存储任意类型。
常见错误
认为T可以执行任何操作
错误:
1fn add<T>( 2 a: T, 3 b: T, 4) -> T { 5 a + b 6} 7
Rust不知道T一定支持+。
需要添加对应Trait Bound。
相同T传入不同类型
例如:
1fn pair<T>( 2 a: T, 3 b: T, 4) 5
不能随意传:
1i32 + String 2
需要改成:
1fn pair<T, U>( 2 a: T, 3 b: U, 4) 5
泛型参数定义了却没有使用
例如:
1struct User<T> { 2 name: String, 3} 4
如果T没有出现在字段中,编译器无法知道T如何参与类型结构,通常会报未使用类型参数相关问题。除非使用:
1PhantomData<T> 2
等高级技术。
Trait Bound太多
如果方法只需要打印,不应该要求:
1T: Display + Clone + Debug + Send + Sync 2
只添加真正需要的约束。
为简单业务过度泛型化
例如一个函数永远只处理:
1User 2
却设计十几个泛型参数,会增加代码理解成本。
泛型的目标是减少重复,而不是让类型签名变复杂。
泛型设计最佳实践
真正存在重复逻辑时再抽象
如果多个类型拥有完全相同的数据处理流程,可以考虑泛型。
类型之间需要共同能力时配合Trait
例如:
1T: Display 2T: Clone 3T: Repository 4
简单Trait约束直接写在<>
例如:
1fn show<T: Display>( 2 value: T, 3) 4
复杂约束使用where
例如:
1fn process<T, U>( 2 first: T, 3 second: U, 4) 5where 6 T: Display + Clone, 7 U: Debug, 8{ 9} 10
API响应和容器非常适合泛型
例如:
1Response<T> 2Page<T> 3Result<T,E> 4Option<T> 5Container<T> 6
不要为了“高级”而泛型
如果代码只有一个明确类型,直接使用具体类型往往更加清晰。
泛型与Trait Object按场景选择
编译阶段确定类型:
1T: Trait 2
运行时多类型:
1dyn Trait 2
本章小结
泛型是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- 复杂业务泛型重构案例
《Rust 泛型 Generics详解:编写可复用且类型安全的代码》 是转载文章,点击查看原文。
