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