《Rust编程实战》系列第49篇
上一篇文章中,我们学习了Rust的where语法,知道了如何把复杂的泛型、Trait Bound和生命周期约束写得更加清晰。本篇继续深入Rust类型系统中的一个重要概念:
Associated Type
中文通常称为“关联类型”。
关联类型主要出现在Trait中,它允许我们在Trait内部声明一个“暂时未知的类型”,由具体实现决定这个类型到底是什么。例如:
trait Container {
type Item;
fn get(&self) -> &Self::Item;
}
这里:
type Item;
就是一个关联类型。
某个类型实现这个Trait时,需要明确指定:
type Item = String;
或者:
type Item = i32;
关联类型和泛型看起来很像,但设计目的并不完全相同。Rust标准库中最典型的例子就是:
Iterator
其中包含:
type Item;
用于表示迭代器每次产生的元素类型。本文将介绍:
-
什么是Associated Type
-
type Item基本语法 -
Self::Item -
为Trait实现关联类型
-
Iterator中的关联类型
-
关联类型与泛型区别
-
多个关联类型
-
关联类型Trait Bound
-
where约束关联类型
-
关联类型与泛型Trait选择
-
Repository实战
-
Parser实战
-
Iterator实战
什么是Associated Type
关联类型就是:
与某个Trait实现绑定的类型。
定义:
trait Storage {
type Item;
fn get(&self) -> &Self::Item;
}
这里的:
type Item;
表示Storage拥有一个关联类型:
Item
但Trait定义时并不知道Item具体是什么。
具体实现时再决定:
struct UserStorage {
value: String,
}
impl Storage for UserStorage {
type Item = String;
fn get(&self) -> &Self::Item {
&self.value
}
}
这里:
type Item = String;
表示:
对于UserStorage的Storage实现,Item就是String。
Self::Item是什么意思
在Trait中经常看到:
Self::Item
例如:
trait Storage {
type Item;
fn get(&self) -> &Self::Item;
}
这里:
Self
表示当前实现Trait的具体类型。
而:
Self::Item
表示:
当前实现对应的关联类型Item。
如果:
impl Storage for UserStorage {
type Item = String;
}
那么在这个实现中:
Self::Item
就等价于:
String
因此:
fn get(&self) -> &Self::Item
实际相当于:
fn get(&self) -> &String
最基本的关联类型案例
定义Trait:
trait Value {
type Output;
fn value(&self) -> Self::Output;
}
定义类型:
struct Number {
value: i32,
}
实现:
impl Value for Number {
type Output = i32;
fn value(&self) -> Self::Output {
self.value
}
}
使用:
fn main() {
let number = Number {
value: 100,
};
println!("{}", number.value());
}
这里:
Output = i32
已经由实现固定下来。
一个Trait可以有多个关联类型
关联类型不一定只有一个。
例如:
trait Converter {
type Input;
type Output;
fn convert(
&self,
input: Self::Input,
) -> Self::Output;
}
实现:
struct StringToNumber;
impl Converter for StringToNumber {
type Input = String;
type Output = Result<i32, String>;
fn convert(
&self,
input: Self::Input,
) -> Self::Output {
input
.parse::<i32>()
.map_err(|e| e.to_string())
}
}
这里:
Input = String
Output = Result<i32,String>
不同关联类型可以分别表达输入、输出、错误等类型关系。
Iterator为什么使用关联类型
Rust标准库中最重要的关联类型案例就是Iterator。
概念上可以简化理解为:
trait Iterator {
type Item;
fn next(
&mut self,
) -> Option<Self::Item>;
}
这里:
type Item;
表示:
这个迭代器每次产生什么类型的数据。
例如:
let numbers = vec![1, 2, 3];
let mut iter = numbers.into_iter();
这个迭代器的:
Item = i32
调用:
iter.next()
返回:
Option<i32>
如果是:
let names = vec![
String::from("Tom"),
String::from("Alice"),
];
对应迭代器可能产生:
Item = String
所以Iterator使用关联类型非常自然。
自定义Iterator关联类型
我们可以自己实现一个简单迭代器:
struct Counter {
current: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Self {
Self {
current: 0,
max,
}
}
}
impl Iterator for Counter {
type Item = u32;
fn next(
&mut self,
) -> Option<Self::Item> {
if self.current >= self.max {
return None;
}
self.current += 1;
Some(self.current)
}
}
使用:
fn main() {
let counter = Counter::new(5);
for number in counter {
println!("{}", number);
}
}
输出:
1
2
3
4
5
这里:
type Item = u32;
明确说明Counter只会产生u32。
为什么不用泛型Trait
我们也可以想象Iterator设计成:
trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
看起来也能工作。
例如:
impl Iterator<u32> for Counter {
fn next(
&mut self,
) -> Option<u32> {
// ...
}
}
但这样会出现一个重要区别:
同一个类型理论上可以针对不同T多次实现同一个泛型Trait。
例如:
Iterator<i32>
Iterator<String>
Iterator<User>
这意味着仅看到:
Counter
还不一定知道它迭代的Item是什么。
而关联类型:
trait Iterator {
type Item;
}
通常要求一个具体的Trait实现对应一个明确的Item。
因此:
Counter实现Iterator
→ Item唯一确定
这会让API更加清晰。
泛型与关联类型的核心区别
泛型Trait:
trait Convert<T> {
fn convert(&self) -> T;
}
某个类型可以尝试拥有多种实现:
impl Convert<String> for Data {}
impl Convert<i32> for Data {}
调用时可能需要进一步说明到底使用哪一个T。
关联类型:
trait Convert {
type Output;
fn convert(&self) -> Self::Output;
}
实现:
impl Convert for Data {
type Output = String;
}
对于这个Trait实现,Output被固定。
可以简单记住:
泛型参数
调用者或实现关系可以选择类型
关联类型
实现者为Trait实现确定类型
什么时候适合关联类型
如果一个Trait和某个实现之间天然只有一个对应类型,关联类型通常更合适。
例如:
Iterator -> Item
Repository -> Entity
Parser -> Output
Future -> Output
Deref -> Target
这些关系通常都是:
一个实现
对应一个核心类型
因此非常适合关联类型。
什么时候适合泛型Trait
如果同一个类型需要对多种目标类型实现同一个行为,泛型可能更适合。
例如:
trait Convert<T> {
fn convert(&self) -> T;
}
同一个类型可能:
impl Convert<String> for User
又可能:
impl Convert<Vec<u8>> for User
此时泛型表达“针对不同目标类型提供不同转换”就更加自然。
Repository关联类型
真实项目中,Repository是非常适合关联类型的场景。
定义:
trait Repository {
type Entity;
fn find(
&self,
id: u64,
) -> Option<&Self::Entity>;
fn save(
&mut self,
entity: Self::Entity,
);
}
用户实体:
#[derive(Debug)]
struct User {
id: u64,
name: String,
}
仓储:
struct UserRepository {
users: Vec<User>,
}
实现:
impl Repository for UserRepository {
type Entity = User;
fn find(
&self,
id: u64,
) -> Option<&Self::Entity> {
self.users
.iter()
.find(|user| user.id == id)
}
fn save(
&mut self,
entity: Self::Entity,
) {
self.users.push(entity);
}
}
这里:
UserRepository
→ Entity = User
关系非常明确。
泛型函数约束关联类型
如果写一个通用函数:
fn find_and_print<R>(
repo: &R,
id: u64,
)
where
R: Repository,
R::Entity: std::fmt::Debug,
{
match repo.find(id) {
Some(entity) => {
println!("{:?}", entity);
}
None => {
println!("数据不存在");
}
}
}
这里:
R::Entity
就是关联类型。
约束:
R::Entity: Debug
表示:
Repository保存的实体类型必须支持Debug。
关联类型也可以直接约束
Trait定义时就可以约束:
use std::fmt::Debug;
trait Repository {
type Entity: Debug;
fn find(
&self,
id: u64,
) -> Option<&Self::Entity>;
}
这样所有实现:
Repository
的类型都必须保证:
Entity: Debug
例如:
#[derive(Debug)]
struct User {
id: u64,
}
如果Entity没有实现Debug,就无法满足Trait要求。
多个Trait Bound约束关联类型
可以写:
trait Repository {
type Entity:
Clone
+ std::fmt::Debug;
}
表示Entity必须同时实现:
Clone
Debug
也可以放到where:
trait Repository
where
Self::Entity:
Clone + std::fmt::Debug,
{
type Entity;
}
简单场景推荐直接:
type Entity: Clone + Debug;
使用关联类型等式约束
有时函数要求Repository的Entity必须是指定类型。
例如:
fn handle_user_repo<R>(
repo: &R,
)
where
R: Repository<Entity = User>,
{
}
这里:
Repository<Entity = User>
表示:
R必须实现Repository,并且它的Entity关联类型必须等于User。
这种语法非常重要。
标准库中也经常看到类似:
Iterator<Item = i32>
Iterator<Item=T>
例如:
fn sum_numbers<I>(
iter: I,
) -> i32
where
I: Iterator<Item = i32>,
{
iter.sum()
}
这里表示:
I必须实现Iterator
并且
I::Item必须等于i32
使用:
let values = vec![1, 2, 3];
let result =
sum_numbers(values.into_iter());
如果Iterator产生的是:
String
就不能传入这个函数。
impl Trait配合关联类型
也可以写:
fn numbers()
-> impl Iterator<Item = i32>
{
vec![1, 2, 3].into_iter()
}
这里返回值:
impl Iterator<Item = i32>
表示:
返回某个实现Iterator的具体类型,并且它产生i32。
调用者不需要知道具体迭代器类型是什么。
Trait Object配合关联类型
Trait Object使用带关联类型的Trait时,通常需要明确关联类型。
例如:
trait Producer {
type Item;
fn produce(
&mut self,
) -> Option<Self::Item>;
}
如果要创建Trait Object:
Box<dyn Producer<Item = String>>
就需要告诉编译器:
Item = String
例如:
struct NameProducer;
impl Producer for NameProducer {
type Item = String;
fn produce(
&mut self,
) -> Option<Self::Item> {
Some(String::from("Tom"))
}
}
使用:
let mut producer:
Box<dyn Producer<Item = String>> =
Box::new(NameProducer);
println!("{:?}", producer.produce());
Parser关联类型实战
解析器也是典型场景。
定义:
trait Parser {
type Output;
type Error;
fn parse(
&self,
input: &str,
) -> Result<
Self::Output,
Self::Error,
>;
}
整数解析器:
struct NumberParser;
impl Parser for NumberParser {
type Output = i32;
type Error = String;
fn parse(
&self,
input: &str,
) -> Result<
Self::Output,
Self::Error,
> {
input
.parse::<i32>()
.map_err(|e| e.to_string())
}
}
这里明确:
NumberParser
Output = i32
Error = String
调用:
fn main() {
let parser = NumberParser;
match parser.parse("100") {
Ok(value) => {
println!("{}", value);
}
Err(error) => {
println!("{}", error);
}
}
}
API客户端案例
还可以定义:
trait Api {
type Response;
type Error;
fn request(
&self,
) -> Result<
Self::Response,
Self::Error,
>;
}
用户接口:
struct UserApi;
#[derive(Debug)]
struct User {
name: String,
}
impl Api for UserApi {
type Response = User;
type Error = String;
fn request(
&self,
) -> Result<
Self::Response,
Self::Error,
> {
Ok(User {
name: String::from("Tom"),
})
}
}
关联类型可以很好地表达:
某个API
对应某种Response
对应某种Error
关联类型与默认类型
部分关联类型可以提供默认类型,但相关能力在不同语言特性和稳定性范围下有所区别,普通业务开发中更常见的是显式声明:
type Item = String;
因此初学阶段建议保持明确,不必为了减少一行代码而引入不必要的复杂语法。
Self::AssociatedType的可读性
例如:
trait Service {
type Request;
type Response;
type Error;
fn call(
&self,
request: Self::Request,
) -> Result<
Self::Response,
Self::Error,
>;
}
虽然类型签名比普通函数长,但表达的信息非常完整:
Service有请求类型
Service有响应类型
Service有错误类型
call接收这个Service自己的Request
返回自己的Response或Error
这也是关联类型在框架和库设计中非常重要的原因。
泛型Trait和关联类型如何选择
假设定义转换能力。
方案一:
trait Convert<T> {
fn convert(&self) -> T;
}
适合:
同一个类型可能转换成多个不同目标类型。
方案二:
trait Convert {
type Output;
fn convert(&self) -> Self::Output;
}
适合:
对于这个Trait实现,输出类型应该是唯一明确的。
可以记住:
需要一个类型拥有多个同Trait不同类型实现
→ 泛型Trait
每个Trait实现只对应一个核心类型
→ Associated Type
关联类型与普通泛型Struct区别
泛型Struct:
struct Container<T> {
value: T,
}
这里:
T
属于Container类型本身。
所以:
Container<i32>
Container<String>
是两个不同的具体类型。
关联类型:
trait Container {
type Item;
}
这里Item属于:
某个类型对Container Trait的实现
两者作用层级不同,不要混淆。
关联类型常见命名
Rust中经常看到:
Item
Output
Error
Target
Request
Response
Key
Value
例如:
Iterator::Item
Future::Output
Deref::Target
好的关联类型名称应该能够表达它和Trait之间的关系。
常见错误
忘记实现关联类型
错误:
impl Repository for UserRepository {
fn find(
&self,
id: u64,
) -> Option<&Self::Entity> {
// ...
}
}
如果Trait要求:
type Entity;
实现时必须指定:
type Entity = User;
把Self::Item当字段
Self::Item
不是结构体字段,而是类型。
它只能出现在类型相关的位置,例如:
fn get(&self) -> Self::Item
Trait Object没有指定必要关联类型
例如:
Box<dyn Iterator>
很多场景中无法明确Item是什么。
通常应写:
Box<dyn Iterator<Item = i32>>
泛型和关联类型重复设计
不推荐:
trait Repository<T> {
type Item;
}
如果:
T
和
Item
表达完全相同概念,就会增加API复杂度。
应该选择其中一种。
关联类型约束过多
例如:
type Item:
Clone
+ Debug
+ Display
+ Send
+ Sync;
如果Trait本身不需要这些能力,就不要提前全部加上。
Associated Type最佳实践
一对一类型关系优先关联类型
例如:
Iterator -> Item
Parser -> Output
Repository -> Entity
非常适合。
一对多实现关系考虑泛型
如果一个类型确实需要针对多个目标类型实现同一个Trait,泛型更加灵活。
关联类型名称要表达语义
推荐:
type Item;
type Error;
type Output;
不要随意写:
type T1;
type T2;
复杂关联类型约束使用where
例如:
where
R: Repository,
R::Entity: Debug + Clone,
可读性更高。
不要重复表示同一个类型
如果已经有:
type Entity;
通常没有必要再为同一概念增加:
Repository<T>
标准库Trait是最好的学习材料
理解:
Iterator::Item
Future::Output
Deref::Target
可以帮助我们掌握关联类型真正的工程价值。
本章小结
Associated Type关联类型是Rust Trait系统中非常重要的一部分,它用于描述:
一个Trait实现内部拥有的、由具体实现决定的类型。
本文学习了:
-
使用
type Item;声明关联类型 -
使用
Self::Item引用关联类型 -
在
impl Trait for Type中指定具体类型 -
一个Trait可以拥有多个关联类型
-
Iterator通过
Item描述迭代元素类型 -
关联类型与泛型Trait的核心区别
-
Repository::Entity适合表达仓储实体 -
Parser::Output和Parser::Error适合表达解析结果 -
可以通过
R::Item: Trait约束关联类型 -
可以使用
Trait<Item = T>要求关联类型等于指定类型 -
impl Iterator<Item = i32>可以隐藏具体返回类型 -
Trait Object也可以通过
dyn Trait<Item = T>指定关联类型
可以记住:
泛型通常表达“这个类型参数可以变化”,关联类型通常表达“对于这个Trait实现,这个内部类型应该被确定下来”。
当一个Trait实现天然只对应一个Item、Output、Error或Entity时,Associated Type往往比泛型Trait更加清晰。
下一篇预告
下一篇我们将学习Rust泛型体系中的另一项重要能力:
Rust const泛型详解:让常量也成为泛型参数
内容包括:
-
什么是const泛型
-
const N: usize -
泛型数组长度
-
[T;N] -
const泛型函数
-
const泛型Struct
-
const泛型impl
-
类型泛型与const泛型组合
-
生命周期、类型、const三类泛型参数
-
不同数组长度为什么是不同类型
-
固定容量容器
-
Matrix矩阵案例
-
Buffer缓冲区案例
-
const泛型与运行时参数区别
-
const泛型实际项目应用