Skip to content

特征约束

Rust 中的特征约束是泛型编程的核心机制,它像一个“契约”或“安全边界”,规定了泛型参数必须实现哪些特征,从而确保类型具有特定的行为。这实现了编译时的多态性,既保证了类型安全,又避免了运行时性能开销。

特征约束语法如下:

rust
T: Trait_1 + Trait2 + ...

在进行泛型声明时,可以在对应的泛型后面使用:对这个泛型进行约束,多个类型使用+号连接

只有实现某些Trait的泛型,才能传入。泛型声明主要出现在函数、方法、复合类型、泛型Trait中,

约束类型语法示例核心用途
基本约束fn func<T: Trait>(t: T) {}确保泛型类型 T拥有 Trait定义的行为。
多重约束fn func<T: TraitA + TraitB>(t: T) {}要求 T同时满足多个特征的行为契约。
Where 子句fn func<T, U>(t: T, u: U) where T: TraitA, U: TraitB {}提升复杂约束的可读性,常用于函数签名较长时。
impl Trait语法糖fn func(item: &impl Trait) {}简化函数签名的写法,适用于简单场景。

1.函数中的特征约束

比如有个面试函数,接收一个面试者,要求这个面试者必须实现Poilt这个trait

rust
trait Poilt {
    fn fly(&self);
}

struct Human;

impl Poilt for Human {
    fn fly(&self) {
        println!("i can fly");
    }
}

fn interview<T: Poilt>(h: T) {
    h.fly();
}

fn main() {
    let h = Human {};
    interview(h);
}

如果要求这个吊毛也会开车呢

rust
trait Poilt {
    fn fly(&self);
}

trait Driver {
    fn drive(&self);
}

struct Human;

impl Poilt for Human {
    fn fly(&self) {
        println!("i can fly");
    }
}

impl Driver for Human {
    fn drive(&self) {
        println!("i can drive");
    }
}

fn interview<T: Poilt + Driver>(h: T) {
    h.fly();
    h.drive();
}

fn main() {
    let h = Human {};
    interview(h);
}

这样就实现了多重约束

2.方法中的特征约束

前面我们已经知道,println!()宏对于有些数据类型,不可以直接打印,需要修改{}占位符

如果一个函数中的参数限定了某个泛型T,我想在函数中打印这个参数,由于编译器不知道这个T具体是什么类型,所以不会让你打印

rust
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn show(&self) {
        println!("x = {}, y = {}", self.x, self.y);  // 报错`T` doesn't implement `std::fmt::Display`
    }
}

fn main() {
    let p = Point { x: 1.11, y: 2.22 };
    p.show();
}

但是如果我的函数中的参数,我明确知道它是一个数值类型,我就想打印,怎么办呢

std::fmt::Display是标准库中一个trait,它用于控制println!()宏里占位符的数据是怎样打印的

我们给泛型定义上这么一个约束,这样我们就可以打印了

rust
struct Point<T> {
    x: T,
    y: T,
}

impl<T: std::fmt::Display> Point<T> {
    fn show(&self) {
        println!("x = {}, y = {}", self.x, self.y);  
    }
}

fn main() {
    let p = Point { x: 1.11, y: 2.22 };
    p.show();			// x = 1.11, y = 2.22
}

std::fmt::Display类似的还有std::fmt::Debug通常用于打印复合数据类型

rust
#[derive(Debug)]
struct MyStruct<T> {
    x: T,
}

impl<T: std::fmt::Debug> MyStruct<T> {
    fn print_self(&self) {
        println!("{:#?}", self);
    }
}

fn main() {
    let s = MyStruct {
        x: "你好".to_string(),
    };
    s.print_self();
}

3.类型中的特征约束

rust
struct Holder<T: std::fmt::Display> {
    value: T,
}

impl<T: std::fmt::Display> Holder<T> {
    pub fn print_value(&self) {
        println!("我持有的值是: {}", self.value);
    }
}

以上代码中,将特征约束放在了定义类型时,此时只有实现了Display的类型才能作为Holder::value

但是在定义类型时进行了约束,不代表impl的时候可以省略这个约束,在impl时还是需要写出T的特征约束。这个特性用的比较少,一般不会在类型使用特征约束,而是在impl层面使用。

4.泛型Trait中的特征约束

Trait 本身也可以是泛型 Trait,也就是说,一个 Trait 的定义可以引入泛型参数。这时如果你希望限制这些泛型参数的范围,就需要为它们添加特征约束。

rust
trait Summary<T: std::fmt::Display> {
    fn summarize(&self, item: T) -> String;
}

struct News;

impl Summary<String> for News {
    fn summarize(&self, item: String) -> String {
        format!("新闻摘要:{}", item)
    }
}

以上代码中,Summary是一个泛型Trait,它要求T实现了Display。后续别的类型实现这个Trait的时候,保证参数item是可以直接被输出的。

5.关联类型中的特征约束

除去以上使用泛型的位置,关联类型也是可以被特征约束的。

rust
trait Container {
    type Item: std::fmt::Display;
}

以上代码中,Container内部有一个关联类型Item,并通过特征约束要求其实现Display,后续impl的时候,具体的Item类型就必须是实现了Display的类型。

最后,可以回想一下Trait继承语法,它也是通过:Trait进行限定,使用+隔开多个Trait。这两个语法很相似,实际上Trait继承本质上也是一个特征约束。

6.where 子句

where 子句让泛型约束变的清晰

当特征约束变得复杂时,使用 : 语法会让代码可读性变差,特别是当泛型参数较多或多个特征约束组合时。

比如说刚才的:impl<T: std::ops::Add<Output = T> + Copy> Point<T>,这里仅仅涉及到两个Trait,就已经十分难以辨别了。

为此,Rust 提供了 where 子句来更清晰地组织约束。

语法如下:

rust
// 定义类型时
struct MyType<T, U>
where
	T: Trait_1 + Trait_2 ... ,
	U: Trait_3 + Trait_4 ... ,
{}

// 定义方法时
impl<T, U> MyType<T, U>
where
	T: Trait_1 + Trait_2 ... ,
	U: Trait_3 + Trait_4 ... ,
{}

// 定义函数时
fn func<T>() -> T 
where
    T: Trait_1 + Trait_2 ... ,
{}

// Trait 继承时
trait Mytrait
where
    Self: Trait_1 + Trait_2 ... ,
{
	type Item where Self::Item: Trait_1; // 关联类型
}

当需要使用特征约束时,可以用where子句语法代替原本的:语法。以上示例展示了四种情况,分别是定义类型、定义方法、定义函数、Trait 继承。但其实它们不用分开记,它们有统一的特点:where子句直接写在{}前面。

使用where定义一个子句,子句内部可以对所有之前声明过的泛型进行特征约束,多个泛型之间用,逗号隔开。对于每个泛型使用:语法表示特征约束。

另外的,关联类型特征约束时也是可以使用where子句的。当Trait继承时,受到约束的类型是Self。而在关联类型中使用where时,必须使用Self::做前缀。

实际上对于关联类型和Trait继承,不使用where子句反而更简洁,这个语法更多的用于有多个泛型的情况下,分别把每个泛型的特征约束列举出来。

7.嵌套约束

有时候,我们不仅要约束泛型 T 本身,还要约束 T 内部包含的类型(关联类型)

假设我们定义了一个 Team(团队)特征,每个团队都有一个关联类型 Leader(队长)。

rust
trait Team {
    type Leader; // 关联类型:队长
}

trait Speak {
    fn say_hello(&self);
}

现在我们需要写一个函数 interview,采访一个团队。要求如下:

  1. 传入的参数 T 必须是一个 Team
  2. 这个团队的 Leader 必须能言善辩(实现了 Speak 特征)。

这就是跨层约束:我们不关心 T 是否实现了 Speak,我们关心的是 T 里的 Leader 是否实现了 Speak

rust
fn interview<T>(t: T, leader: T::Leader)
where
    T: Team,               // 第一层约束:T 是个团队
    T::Leader: Speak,      // 第二层约束:团队里的 Leader 必须会 Speak
{
    leader.say_hello();
}

如果不写第二行约束,编译器只知道 T 有个 Leader,但不知道这个 Leader 能干什么,就无法调用 say_hello

8.完全限定

想象一下,一个人既是飞行员又是工程师。这两个职业都有一个关联类型叫“徽章 (Badge)”,但一个是金属做的,一个是布做的。

如果你只说“把他的徽章给我”,编译器就会懵圈:“你到底要哪个徽章?”

rust
trait Pilot {
    type Badge; // 飞行员徽章
}

trait Engineer {
    type Badge; // 工程师徽章
}

// 定义一个函数,接受的泛型 T 既是飞行员又是工程师
fn check_badge<T: Pilot + Engineer>(t: T) {
    let my_badge: T::Badge; 
}

这时候就必须使用完全限定语法来消除歧义,明确告诉编译器你要把 T 当作哪个身份来看待。

它的格式是:<Type as Trait>::AssociatedType

rust
fn check_badge<T: Pilot + Engineer>(t: T) {
    let pilot_badge: <T as Pilot>::Badge;
    let eng_badge: <T as Engineer>::Badge;
}

在前面Team的例子中,我们写了 T::Leader。但在更复杂的场景下,或者是为了语法的绝对严谨,我们需要使用完全限定语法 。

这种写法的作用是告诉编译器:“请把 Type 当作 Trait 来看待,并取出里面的 AssociatedType”。

  1. 消除歧义:如果 T 实现了两个特征,这两个特征都有一个叫 Item 的关联类型,必须指明是哪一个。
  2. where 子句中更明确:Rust 编译器在处理复杂的依赖关系时,更推荐这种显式写法。

让我们把上面的 interview 函数写成最标准、最“地道”的形式:

rust
fn interview_strict<T>(t: T, leader: <T as Team>::Leader)
where
    T: Team,
    <T as Team>::Leader: Speak, // 使用完全限定语法
{
    leader.say_hello();
}

这里的 <T as Team>::Leader: Speak 翻译过来就是:

“对于泛型 T,把它作为 Team 来看待时,它所定义的那个 Leader 类型,必须实现 Speak 特征。”

9.Nominal Typing

学完Trait限定,不知道你有没有感觉它有点像鸭子类型。如果你没听过 Duck Typing,简而言之就是一句话:

“如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。”*

在编程语言里,这意味着:只要一个对象拥有某些方法,就可以被当作某个类型来使用,而不需要显式声明它属于这个类型。

比如在 Python 里,你写一个函数 make_it_quack(x),只要传入的对象有 quack() 方法,就能正常运行,根本不管它是什么类型。这就是典型的 Duck Typing,特点是灵活,在运行时对方法进行检查。

而在类似于C++这样的语言,一个函数写出来后,每个参数的类型都必须是写死的。它不以这个类型的功能为依据,而是以类型本身作为依据。

打个比方:某个公司招聘软件工程师。

鸭子类型的逻辑:公司要求受聘者可以进行软件开发。不管你曾经是什么专业,你可以学数学,可以学汉语言文学,甚至哪怕你不是一个人,你是一个会敲代码的猴子。只要你会软件开发,你都可以来面试。但是面试前谁知道你会不会敲代码?因此只能面试过程中对你提问,这就有可能导致面试过程中才发现这个人根本不会敲代码,浪费了时间。

非鸭子类型的逻辑:公司要求受聘者是软件工程专业毕业生。公司不以你是否可以敲代码为依据,而是要求你必须就是该专业的人。这样招募进来的人一定是会敲代码的,但是也会错失某些优秀人才。

因此鸭子类型和非鸭子类型的区别就体现出来了。鸭子类型下更加灵活,但是在运行过程中才能检查出问题,导致安全性和效率降低。而非鸭子类型,在起初就限制好了类型,只要你能通过类型检查,那就保证一定可以完成函数内部的操作,提高了安全性,但也降低了灵活性。

Rust 的特征约束看起来是不是有点像 Duck Typing

当我们写下:

rust
fn make_it_swim<T: Swim>(x: T) {
    x.swim();
}

这段代码的语义就是:只要能游泳的类型都能传进来。是不是很像Duck Typing

但关键的不同在于:Rust 会在编译期就检查 T 是否真的实现了 Swim。换句话说,Rust 提供了 Duck Typing 的表达力,却把它变成了静态 Duck Typing

这背后体现的是 Rust 的设计思想:

  • 它不是动态语言的 Duck Typing,它不会等到运行时才发现对象不会叫。
  • 采用的是 Nominal Typing(名义类型):只有当你显式 impl Trait for Type 时,编译器才承认这个类型具备某个能力。

名义类型的逻辑:公司招募要求用户必须完成一场笔试,笔试通过才有面试机会,而且任何专业的人都可以笔试。这样既可以招募到各式各样的人才,又保证了参与面试的人一定具有开发能力。

而这个笔试的过程,就是Rust中的impl for

这种方案既保证了安全性,而且在编译期完成检查,对运行时没有任何效率影响,这也实现了Rust最注重的 效率 + 安全。

MIT Licensed