Rust where详解:让泛型与Trait约束更加清晰

《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实战案例

相关推荐
zzzzzz3102 小时前
react-bits 为什么会吸引前端开发者:别把动效当装饰,先把它当成页面能力
前端·react.js·动效
程序员爱钓鱼3 小时前
Go 编程实战:函数 Function——参数、返回值与代码复用
后端·面试·go
风月说与山鬼4 小时前
二、React入口文件(main.jsx)
前端·react.js
To_OC8 小时前
跑了3个Docker容器后,我终于搞懂镜像和容器到底啥关系
后端·docker·容器
东风破_9 小时前
Docker 基础:为什么需要容器?
前端·后端·nginx
东风破_9 小时前
Docker 实战:使用 Nginx 作为容器入口代理 Node 服务
前端·后端·nginx
Lyyaoo.10 小时前
Spring Boot Validation 声明式参数校验
spring boot·后端·mysql
剩下了什么10 小时前
float32 与 float64 精度陷阱:如何在 Go 中避免错误使用
开发语言·后端·golang
你别说话了10 小时前
Vue项目解决跨域
前端·javascript·vue.js