《Rust编程实战》系列第48篇
上一篇文章中,我们学习了Rust泛型Generics,知道可以通过:
1fn show<T>(value: T) { 2} 3
让同一套代码适配不同类型。但泛型本身只表示“类型可以变化”,如果代码需要这个类型具备某种能力,就必须配合Trait Bound。例如:
1use std::fmt::Display; 2fn show<T: Display>(value: T) { 3 println!("{}", value); 4} 5
这里:
1T: Display 2
表示泛型类型T必须实现Display。当约束比较简单时,这种写法非常方便。但真实项目中可能出现:
1fn process<T: Display + Clone + Send, U: Debug + PartialEq>( 2 first: T, 3 second: U, 4) { 5} 6
随着泛型数量和Trait约束增加,函数签名很快就会变得难以阅读。Rust因此提供:
1where 2
用于把泛型约束从参数声明位置移动到更加清晰的位置。本文将介绍:
- 什么是where
- where基本语法
T: Trait与where区别- 多个泛型约束
- 多个Trait Bound
- where与返回值
- 泛型Struct中的where
- impl中的where
- 方法中的where
- 生命周期约束
T: 'a'a: 'bSelf: Sized- 关联类型约束
- 实际项目中的where设计
什么是where
where本质上是Rust编写泛型约束的另一种语法。
例如:
1use std::fmt::Display; 2fn print_value<T>(value: T) 3where 4 T: Display, 5{ 6 println!("{}", value); 7} 8
这与:
1fn print_value<T: Display>(value: T) { 2 println!("{}", value); 3} 4
表达的核心含义相同。
都表示:
T必须实现Display Trait。
因此where并不是一种新的泛型机制,而是让复杂约束更容易阅读和维护。
where基本语法
基本格式:
1fn function<T>(value: T) 2where 3 T: Trait, 4{ 5} 6
多个泛型:
1fn function<T, U>( 2 first: T, 3 second: U, 4) 5where 6 T: TraitA, 7 U: TraitB, 8{ 9} 10
一个类型实现多个Trait:
1where 2 T: TraitA + TraitB + TraitC, 3
可以简单记住:
1泛型参数写在<> 2约束条件写在where后面 3
简单约束不一定需要where
如果只有一个简单约束:
1fn show<T: Display>(value: T) { 2} 3
通常没有必要强行改成:
1fn show<T>(value: T) 2where 3 T: Display, 4{ 5} 6
前者更加简洁。
where真正有价值的场景通常是:
- 泛型参数很多
- 每个类型有多个Trait Bound
- 有生命周期约束
- 有关联类型约束
- 函数签名已经很长
因此:
简单约束直接写,复杂约束使用where。
多个Trait Bound
假设函数既要打印,又要复制数据:
1use std::fmt::Display; 2fn duplicate<T>(value: T) 3where 4 T: Display + Clone, 5{ 6 let second = value.clone(); 7 println!("{}", value); 8 println!("{}", second); 9} 10
这里:
1T: Display + Clone 2
表示T同时实现:
1Display 2Clone 3
如果没有Clone:
1value.clone() 2
无法保证可以调用。
如果没有Display:
1println!("{}", value) 2
也无法保证成立。
Trait Bound其实就是在告诉编译器:
我接下来要对T执行这些操作,所以T必须具备这些能力。
多个泛型参数
例如:
1use std::fmt::{Debug, Display}; 2fn process<T, U>( 3 first: T, 4 second: U, 5) 6where 7 T: Display + Clone, 8 U: Debug, 9{ 10 println!("{}", first); 11 println!("{:?}", second); 12} 13
这里:
1T必须实现Display和Clone 2U必须实现Debug 3
相比:
1fn process<T: Display + Clone, U: Debug>( 2 first: T, 3 second: U, 4) 5
当约束不断增加时,where版本会更加容易阅读。
where不会改变泛型类型关系
例如:
1fn compare<T>( 2 first: T, 3 second: T, 4) 5where 6 T: PartialEq, 7{ 8 println!("{}", first == second); 9} 10
这里两个参数仍然要求是同一个具体类型,因为它们都使用:
1T 2
where只增加:
1T: PartialEq 2
能力约束,并不会改变原有泛型关系。
where与返回值
返回泛型值时同样可以添加约束:
1fn larger<T>( 2 first: T, 3 second: T, 4) -> T 5where 6 T: PartialOrd, 7{ 8 if first >= second { 9 first 10 } else { 11 second 12 } 13} 14
这里需要:
1PartialOrd 2
是因为使用了:
1>= 2
而不是因为返回值是T。
这是理解Trait Bound的重要方法:
看函数内部对泛型执行了什么操作,再决定需要什么约束。
Struct中的where
泛型Struct也可以使用where。
例如:
1use std::fmt::Display; 2struct Container<T> 3where 4 T: Display, 5{ 6 value: T, 7} 8
这表示:
Container只能使用实现Display的T。
例如:
1let container = Container { 2 value: 100, 3}; 4
因为i32实现了Display,所以可以使用。
不过要注意:
不要过早在Struct定义上添加约束。
如果结构体只是保存:
1value: T 2
本身并不需要Display,那么更推荐:
1struct Container<T> { 2 value: T, 3} 4
等真正需要打印的方法出现时再添加约束。
把约束放到impl中
这是非常常见的设计。
定义:
1struct Container<T> { 2 value: T, 3} 4
然后:
1use std::fmt::Display; 2impl<T> Container<T> 3where 4 T: Display, 5{ 6 fn print(&self) { 7 println!("{}", self.value); 8 } 9} 10
这样Container本身可以保存任何类型,但:
1print() 2
只对实现Display的T可用。
相比直接约束整个Struct,这种设计更加灵活。
不同impl使用不同where
同一个泛型类型可以根据能力拥有不同方法。
例如:
1#[derive(Debug)] 2struct Data<T> { 3 value: T, 4} 5
所有T都可以使用:
1impl<T> Data<T> { 2 fn new(value: T) -> Self { 3 Self { 4 value, 5 } 6 } 7} 8
只有实现Display的类型可以打印:
1use std::fmt::Display; 2impl<T> Data<T> 3where 4 T: Display, 5{ 6 fn print(&self) { 7 println!("{}", self.value); 8 } 9} 10
只有实现Clone的类型可以复制内部数据:
1impl<T> Data<T> 2where 3 T: Clone, 4{ 5 fn cloned_value(&self) -> T { 6 self.value.clone() 7 } 8} 9
这样方法能力与类型能力之间的关系非常清楚。
方法单独使用where
不一定整个impl都要约束。
例如:
1struct Container<T> { 2 value: T, 3} 4impl<T> Container<T> { 5 fn new(value: T) -> Self { 6 Self { 7 value, 8 } 9 } 10 fn duplicate(&self) -> T 11 where 12 T: Clone, 13 { 14 self.value.clone() 15 } 16} 17
这里:
1new() 2
适用于任何T。
只有:
1duplicate() 2
要求T实现Clone。
这比:
1impl<T: Clone> Container<T> 2
更加精确。
where与生命周期
where不仅可以约束Trait,也可以约束生命周期。
例如:
1fn print_ref<'a, T>( 2 value: &'a T, 3) 4where 5 T: 'a, 6{ 7} 8
这里:
1T: 'a 2
表示T中包含的引用数据必须能够至少在'a期间有效。
现代Rust中很多简单场景可以自动推断,因此你不一定经常手写它,但在复杂泛型代码中会看到。
生命周期之间的where约束
生命周期本身也可以互相约束:
1fn example<'a, 'b>( 2 value: &'a str, 3) -> &'b str 4where 5 'a: 'b, 6{ 7 value 8} 9
这里:
1'a: 'b 2
表示:
'a至少和'b一样长。
因为只有这样,生命周期为'a的引用才能安全地作为生命周期更短的'b引用返回。
可以简单理解:
1'a: 'b 2'a比'b活得至少一样久 3
where与'static
经常可以看到:
1fn run<T>(value: T) 2where 3 T: 'static, 4{ 5} 6
这里:
1T: 'static 2
不是说变量value永远存在。
它主要表示:
T内部不能依赖生命周期短于
'static的借用数据。
例如:
1String 2Vec<i32> 3u32 4
这类拥有型数据通常满足'static约束。
在线程和异步任务中经常会看到:
1T: Send + 'static 2
意思是:
T能够安全移动到其他线程,并且不依赖短生命周期借用。
where Self: Sized
Trait中经常看到:
1trait Create { 2 fn create() -> Self 3 where 4 Self: Sized; 5} 6
为什么需要:
1Self: Sized 2
因为返回:
1Self 2
需要编译器知道具体返回值大小。
如果Trait未来作为:
1dyn Trait 2
使用,那么Trait Object本身隐藏了具体类型。
通过:
1where 2 Self: Sized 3
表示:
这个方法只允许具体大小已知的类型调用,不要求Trait Object支持它。
例如:
1trait Animal { 2 fn speak(&self); 3 fn create() -> Self 4 where 5 Self: Sized; 6} 7
这样:
1&dyn Animal 2
仍然可以调用:
1speak() 2
但不能通过Trait Object调用create()。
where Self: Clone
Trait默认方法也可以只对部分实现类型开放。
例如:
1trait CopyValue { 2 fn copy_value(&self) -> Self 3 where 4 Self: Clone, 5 { 6 self.clone() 7 } 8} 9
如果某个类型实现了这个Trait,但没有实现Clone,那么Trait本身仍然可以实现,只是不能使用这个需要Clone的方法。
这种设计可以让Trait更加灵活。
关联类型与where
Rust中一些复杂Trait会包含关联类型。
例如:
1trait Repository { 2 type Item; 3 fn save(&mut self, item: Self::Item); 4} 5
如果需要限制关联类型:
1use std::fmt::Debug; 2fn print_repository<R>( 3 repository: &R, 4) 5where 6 R: Repository, 7 R::Item: Debug, 8{ 9} 10
这里:
1R::Item: Debug 2
表示:
Repository的关联类型Item必须实现Debug。
这种语法在Iterator、Future、Repository等抽象中非常常见。
Iterator中的where
标准库风格代码经常出现:
1fn print_all<I>( 2 values: I, 3) 4where 5 I: IntoIterator, 6 I::Item: std::fmt::Display, 7{ 8 for value in values { 9 println!("{}", value); 10 } 11} 12
这里约束分成两层:
1I必须能够转换为迭代器 2I产生的Item必须实现Display 3
调用:
1print_all(vec![1, 2, 3]); 2
这里:
1I = Vec<i32> 2I::Item = i32 3
这就是where在真实泛型代码中的典型应用。
where与多个生命周期泛型组合
例如:
1use std::fmt::Display; 2fn show<'a, T, U>( 3 first: &'a T, 4 second: U, 5) 6where 7 T: Display + 'a, 8 U: Display + Clone, 9{ 10 println!("{}", first); 11 println!("{}", second); 12} 13
这里同时包含:
1'a 生命周期参数 2T 泛型参数 3U 泛型参数 4Display Trait约束 5Clone Trait约束 6生命周期约束 7
如果全部塞进函数名旁边,签名会非常难读,而where能把关系整理清楚。
实战:通用Response
定义:
1#[derive(Debug)] 2struct Response<T> { 3 code: u16, 4 data: T, 5} 6
通用构造:
1impl<T> Response<T> { 2 fn new( 3 code: u16, 4 data: T, 5 ) -> Self { 6 Self { 7 code, 8 data, 9 } 10 } 11} 12
只有可显示的数据支持打印:
1use std::fmt::Display; 2impl<T> Response<T> 3where 4 T: Display, 5{ 6 fn print(&self) { 7 println!( 8 "code={} data={}", 9 self.code, 10 self.data 11 ); 12 } 13} 14
这样:
1Response<String> 2Response<i32> 3
可以使用print(),而其他没有实现Display的类型仍然可以正常使用Response的基础能力。
实战:分页结果
1#[derive(Debug)] 2struct Page<T> { 3 page: u32, 4 total: u64, 5 items: Vec<T>, 6} 7
定义通用方法:
1impl<T> Page<T> { 2 fn len(&self) -> usize { 3 self.items.len() 4 } 5 fn is_empty(&self) -> bool { 6 self.items.is_empty() 7 } 8} 9
只有T实现Clone时才能复制元素:
1impl<T> Page<T> 2where 3 T: Clone, 4{ 5 fn cloned_items(&self) -> Vec<T> { 6 self.items.clone() 7 } 8} 9
这种设计遵循:
哪个方法真正需要什么能力,就在哪个位置添加什么约束。
实战:通用比较函数
1use std::fmt::Display; 2fn max_and_print<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
调用:
1let result = max_and_print( 2 100, 3 200, 4); 5println!("{}", result); 6
这里:
1PartialOrd 2负责比较 3Display 4负责输出 5
每一个Trait约束都有明确原因。
实战:Repository约束
定义:
1trait Repository { 2 type Item; 3 fn find( 4 &self, 5 id: u64, 6 ) -> Option<&Self::Item>; 7} 8
通用打印函数:
1use std::fmt::Debug; 2fn debug_find<R>( 3 repository: &R, 4 id: u64, 5) 6where 7 R: Repository, 8 R::Item: Debug, 9{ 10 match repository.find(id) { 11 Some(value) => { 12 println!("{:?}", value); 13 } 14 None => { 15 println!("数据不存在"); 16 } 17 } 18} 19
这里不关心Repository具体保存:
1User 2Product 3Order 4
只要求它的Item支持Debug。
常见错误
为所有泛型提前添加大量约束
不推荐:
1struct Container<T> 2where 3 T: Clone 4 + Debug 5 + Display 6 + Send 7 + Sync, 8{ 9 value: T, 10} 11
如果Struct只是保存数据,这些约束可能完全没有必要。
应该尽量把约束放在真正需要它的方法上。
where中写了无用Trait
例如函数只比较:
1fn max<T>(...) 2where 3 T: PartialOrd + Clone + Display, 4
但代码没有Clone和打印,就不需要后两个约束。
误以为where性能更好
下面两种写法:
1fn show<T: Display>(value: T) 2
和:
1fn show<T>(value: T) 2where 3 T: Display, 4
通常只是语法组织不同,并不存在“where版本更快”。
混淆T:'a和'a:T
生命周期约束通常写:
1T: 'a 2'a: 'b 3
不能随意颠倒。
约束整个impl导致方法无法使用
例如:
1impl<T> Container<T> 2where 3 T: Clone, 4{ 5 fn new(value: T) -> Self { 6 Self { value } 7 } 8} 9
这样没有实现Clone的T连new()都不能使用。
更合理:
1impl<T> Container<T> { 2 fn new(value: T) -> Self { 3 Self { value } 4 } 5 fn clone_value(&self) -> T 6 where 7 T: Clone, 8 { 9 self.value.clone() 10 } 11} 12
where最佳实践
简单约束保持简单
推荐:
1fn show<T: Display>(value: T) 2
复杂约束使用where
推荐:
1fn process<T, U>( 2 first: T, 3 second: U, 4) 5where 6 T: Display + Clone, 7 U: Debug + PartialEq, 8{ 9} 10
约束放在最小必要范围
如果只有一个方法需要Clone,就只约束那个方法。
每个Trait Bound都应该有理由
看到:
1T: Clone + Debug + Send 2
应该能够解释每个Trait为什么存在。
泛型Struct不要过度约束
数据容器通常保持:
1struct Container<T> 2
然后在impl或方法中逐步增加能力。
生命周期复杂时使用where提升可读性
例如:
1where 2 T: 'a, 3 'a: 'b, 4
比把所有约束塞进泛型声明更加清晰。
关联类型约束优先使用where
例如:
1where 2 I: Iterator, 3 I::Item: Display, 4
非常适合表达多层类型关系。
本章小结
where是Rust泛型和Trait系统中非常重要的约束语法,它本身不会增加新的类型能力,而是让复杂泛型关系更加清晰。
本文学习了:
where用于编写Trait Bound和生命周期约束- 简单的
T: Trait和where通常表达相同含义 - 多泛型、多Trait时where可读性更好
- Struct、impl和方法都可以使用where
- 约束应该尽量放在最小必要范围
T: 'a表示T中的借用至少在'a期间有效'a: 'b表示'a至少和'b一样长where Self: Sized常用于限制Trait中的特定方法- 可以通过
R::Item: Trait约束关联类型 - Iterator、Repository等复杂抽象大量使用where
可以记住:
泛型回答“类型可以是什么”,Trait Bound回答“类型必须会什么”,where负责把这些约束写得更加清楚。
where不是为了让代码显得高级,而是在类型关系复杂时降低阅读和维护成本。
下一篇预告
下一篇我们将继续深入Rust Trait体系:
Rust Associated Type关联类型详解:为Trait定义内部类型
内容包括:
- 什么是Associated Type
type Item语法- 关联类型与泛型区别
- Trait中的关联类型
- impl中指定关联类型
- 为什么Iterator使用
Item Self::Item- 多个关联类型
- 关联类型Trait Bound
- where约束关联类型
- 泛型Trait与关联类型如何选择
- Repository、Iterator、Parser实战案例
《Rust where详解:让泛型与Trait约束更加清晰》 是转载文章,点击查看原文。