Skip to content

高级 Trait

关联类型

关联类型是 Trait 定义里的"占位符",它能让不同的实现者为同一个 Trait 指定不同的具体类型.

挖坑与填坑

  • Trait 里"挖坑": 定义 Trait 时,我不确定具体的类型,先给它起个名字(比如 type Item;).
  • Impl 里"填坑": 当你给某个具体的结构体实现这个 Trait 时,你必须指明这个坑里填什么(比如 type Item = u32;).
rust
// 定义一个带关联类型的 Trait
trait MyTrait {
    // 挖坑:先给这个类型起个名字,叫 Value
    type Value;
    // 这里用 Self::Value 来引用这个坑里的类型,和引用关联函数的语法一样
    fn get_value(&self) -> Self::Value;
}

struct MyStruct;

// 实现 Trait,指定关联类型为 i32
impl MyTrait for MyStruct {
    // 填坑:告诉编译器,这个坑里填的是 i32
    type Value = i32;
    // 这里直接写 i32,因为我们已经在上面指定了 Value 是 i32
    fn get_value(&self) -> i32 {
        42
    }
}

fn main() {
    let s = MyStruct;
    println!("{}", s.get_value()); // 输出 42
}

本质是对实现者增加约束:当你实现这个 Trait 时,你必须告诉编译器"我这个 Trait 里那个坑(关联类型)里填的是什么类型".

关联类型的约束

关联类型的约束(Trait Bounds on Associated Types) 是指在定义 Trait 时,对关联类型施加的限制,要求实现该 Trait 的类型必须满足特定的条件(比如实现了某个其他 Trait).这使得 Trait 更加灵活和强大,因为它允许你在 Trait 定义中指定对关联类型的要求,从而确保实现者提供的类型具有所需的功能.

rust
use std::fmt::Display;

trait Container {
    // 约束:不管你填什么类型,它必须实现了 Display(能打印)
    type Item: Display;

    fn print_item(&self, item: Self::Item) {
        // 因为有上面的约束,这里才敢直接用 println!
        println!("Value is: {}", item);
    }
}

多个约束与 where 子句

简单版:

rust
trait ComplexTrait {
    type Item: Display + Clone;
}

复杂版(把约束放在 where 子句里):

rust
trait ComplexTrait {
    type Item;

    // 也可以在 Trait 定义的最下面统一写约束
    where
        Self::Item: Display + Clone;
}

泛型类型参数

泛型类型参数Trait 定义里的类型"占位符",允许同一个 Trait 被同一个类型实现多次,只要类型参数不同.

默认泛型类型参数(Default Generic Type Parameters) 允许你为泛型指定一个默认的类型.如果在使用时没有显式指定具体类型,Rust 就会使用这个默认值.

泛型 Trait 是 Rust 实现"函数重载(Overloading)"和"多态"的机制.

泛型类型参数是打破"同一类型只能实现一个 Trait"的限制,让你可以为同一个结构体写多个 impl,每个 impl 处理不同的类型.

定义语法

  • 泛型类型参数语法: trait TraitName<T>
  • 默认泛型类型参数语法: trait TraitName<T=Type>
rust
trait Into<Rhs = Self> {
    fn into(self) -> Rhs;
}

struct Converter;

// 使用默认的 Rhs(即 Self,此处是 Converter)
impl Into for Converter {
    fn into(self) -> Self {
        self
    }
}

// 重载 1:处理 f64
impl Into<f64> for Converter {
    fn into(self) -> f64 {
        0.0
    }
}

// 重载 2:处理 Vec<u8>
impl Into<Vec<u8>> for Converter {
    fn into(self) -> Vec<u8> {
        vec![]
    }
}

fn main() {
    let c = Converter;
    let _ = c.into(); // 使用默认的 Rhs,得到 Converter
    let _ = c.into::<f64>(); // 显式指定 Rhs 为 f64
    let _ = c.into::<Vec<u8>>(); // 显式指定 Rhs 为 Vec<u8>
}

这里泛型类型参数是 Rhs = Self, 如果你不指定 Rhs,它默认就是 Self(即当前类型本身).

  • Rhs 只是一个名字(Right Hand Side 的缩写,意为"右操作数").你可以把它改成 TOther 或任何你喜欢的名字.
  • = 后面的部分就是默认值.
  • Self 是 Rust 中的一个关键字,代表当前正在实现该 trait 的类型本身,你也可以使用 i32f64 等具体类型.

调用语法

  • 调用自定义泛型类型参数: instance.method::<Type>()
  • 调用默认泛型类型参数: instance.method()
rust
let result: Converter = c.into(); // 使用默认的 Rhs,得到 Converter
let result: f64 = c.into::<f64>(); // 显式指定 Rhs

使用默认泛型类型参数

实现对 + 运算符的重载时,通常会用到默认泛型类型参数.比如我们想让 Point 结构体支持加法运算:

rust
use std::ops::Add;

// + 运算符对应的 Trait 定义如下(来自 std::ops 模块):
// 默认的 Rhs 是 Self,也就是说,如果你不指定,默认就是自己加自己
// trait Add<Rhs=Self> {
//     type Output;
//     fn add(self, rhs: Rhs) -> Self::Output;
// }

#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

// 实现 Add Trait,使用默认的 Rhs(即 Self,此处是 Point)
impl Add for Point {
    type Output = Point;

    fn add(self, rhs: Point) -> Point {
        Point {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}

使用自定义泛型类型参数

实现将毫米值与米的值相加,并让 Add 的实现正确处理单位转换:

rust
use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

// 实现 Add Trait,指定 Rhs 是 Meters
impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, rhs: Meters) -> Millimeters {
        Millimeters(self.0 + (rhs.0 * 1000))
    }
}

fn main() {
    let mm = Millimeters(500);
    let m = Meters(2);
    let result = mm + m; // 500mm + 2m (2000mm) = 2500mm
    println!("Result: {} mm", result.0); // 输出 Result: 2500 mm
}

泛型类型参数 vs 关联类型

维度泛型 TraitTrait<T>关联类型 Traittype Item
实现数量一个类型可以有多个不同参数的实现一个类型只能有一个实现
使用场景行为可能因输入类型而异(如 Add<T>类型内部紧密相关的属性(如 Iterator::Item
代码简洁度签名较长,需重复声明参数签名简洁,自动从 Self 推导
语义表示"多种可能"表示"这就是我的配套类型"

同一类型不能重复实现同一个 Trait 不是关联类型的特性,而是 Rust 的设计原则之一.

关联类型只是在不能重复实现的基础上,增加了对类型的明确指定和约束.

何时使用

如果你在写一个 Trait,并要对其实现进行约束,问自己一个问题:

"你是否允许一个类型多次实现同一个 Trait"

  • 不需要(选关联类型):比如迭代器 Iterator.一个 Vec<u32> 的迭代器,吐出来的一定是 u32,不可能又是 u32 又是 String.这时候用关联类型,用户调用 .next() 时就不需要手动标注类型,非常省心.
  • 需要(选泛型类型参数):比如加法 Add.一个数字可能要加整数,也可能要加浮点数.这时候用泛型,允许你为同一个结构体写多个 impl.

在同名方法之间消歧义

Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法,也不能阻止为同一类型同时实现这两个 trait.同时还可以直接在类型上实现一个与 trait 方法同名的方法.当调用这些同名方法时,需要告诉 Rust 我们想要使用哪一个.

普通方法消歧义

语法: TraitName::method(&instance)

当同一个类型实现多个 Trait,并且这些 Trait 中有同名的方法时,Rust 无法确定我们想要调用哪个方法.

比如下面这个例子:

rust
trait TraitA {
    fn do_something(&self);
}

trait TraitB {
    fn do_something(&self);
}

struct MyStruct;

impl TraitA for MyStruct {
    fn do_something(&self) {
        println!("TraitA's implementation");
    }
}

impl TraitB for MyStruct {
    fn do_something(&self) {
        println!("TraitB's implementation");
    }
}

impl MyStruct {
    fn do_something(&self) {
        println!("MyStruct's own implementation");
    }
}

此时的 MyStruct 同时实现了 TraitATraitBdo_something 方法,并且自己也有一个 do_something 方法.当我们调用 do_something 时,Rust 无法确定我们想要调用哪个版本的 do_something.

我们需要明确指定我们想要调用哪个方法:

rust
fn main() {
    let s = MyStruct;
    s.do_something(); // 默认调用 MyStruct 的方法
    TraitA::do_something(&s); // 调用 TraitA 的方法
    TraitB::do_something(&s); // 调用 TraitB 的方法
}

TraitA::do_something(&s) 语法告诉 Rust 我们想要调用 TraitA 中定义的 do_something 方法,并且传入 &s 作为参数.

完全限定语法

语法: <Type as Trait>::method

当同一个类型实现多个 Trait,并且这些 Trait 中有同名的关联函数时(没有 self 参数),可以使用完全限定语法来调用特定的关联函数.

rust
trait TraitA {
    fn do_something();
}

trait TraitB {
    fn do_something();
}

struct MyStruct;

impl TraitA for MyStruct {
    fn do_something() {
        println!("TraitA's implementation");
    }
}

impl TraitB for MyStruct {
    fn do_something() {
        println!("TraitB's implementation");
    }
}

impl MyStruct {
    fn do_something() {
        println!("MyStruct's own implementation");
    }
}

在这个例子中,do_something 是一个关联函数(没有 self 参数).当我们想要调用特定的 do_something 时,可以使用完全限定语法:

rust
fn main() {
    MyStruct::do_something(); // 调用 MyStruct 的固有方法
    <MyStruct as TraitA>::do_something(); // 调用 TraitA 的方法
    <MyStruct as TraitB>::do_something(); // 调用 TraitB 的方法
}

超 trait (Super Traits)

超 trait 是指一个 trait 依赖于另一个 trait 的功能.通过使用超 trait,你可以在一个 trait 中声明它需要另一个 trait 的实现,从而在实现这个 trait 时自动获得另一个 trait 的功能.

rust
// 定义一个超 trait,要求实现者必须同时实现 Display 和 Debug
trait SuperTrait: std::fmt::Display + std::fmt::Debug {
    fn super_method(&self);
}

// 定义一个结构体,实现 Debug 来满足 SuperTrait 的要求
#[derive(Debug)]
struct MyStruct {
    value: i32,
}

// 实现 Display,来满足 SuperTrait 的要求
impl std::fmt::Display for MyStruct {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "MyStruct with value: {}", self.value)
    }
}

// 实现 SuperTrait,必须同时满足 Display 和 Debug 的要求
impl SuperTrait for MyStruct {
    fn super_method(&self) {
        println!("Super method called with value: {}", self.value);
    }
}

fn main() {
    let s = MyStruct { value: 42 };
    s.super_method(); // 输出 Super method called with value: 42
}

超 trait 的本质就是对实现者增加约束:当你实现这个 Trait 时,你必须同时满足另一个 Trait 的要求. 这使得你的 Trait 更加灵活和强大,因为它可以依赖于其他 Trait 的功能,而不需要在每个实现中重复代码.

newtype 模式

基本概念

newtype 模式 是 Rust 中的一种设计模式,允许你通过创建一个新的类型来包装一个现有的类型,从而为这个新类型实现外部 trait.这种模式非常有用,因为它可以让你在不修改原始类型的情况下,为其添加新的行为或接口.

newtype 模式变相地打破了"外部 trait 不能在外部类型上实现"(孤儿规则)的限制,因为你不是直接在原始类型上实现 Trait,而是在一个新的包装类型上实现 Trait.

在外部类型上实现外部 trait

假如想让 Vec 打印得更漂亮,但因为 VecDisplay 都是标准库定义的(孤儿规则),你不能直接实现.

rust
use std::fmt;

// 1. 定义一个新类型,包装现有的类型 Vec<String>
struct MyList(Vec<String>);

// 2. 为这个新类型实现外部 Trait (Display)
impl fmt::Display for MyList {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // self.0 用来访问内部的数据
        write!(f, "[{}]", self.0.join(" ~ "))
    }
}

fn main() {
    let names = MyList(vec!["Alice".to_string(), "Bob".to_string()]);

    // 现在可以像普通类型一样使用 Display 了
    println!("我的列表: {}", names);
    // 输出: 我的列表: [Alice ~ Bob]
}

本质就是给类型穿个"马甲",然后在这个"马甲"上实现你想要的 Trait,从而间接地为原始类型提供了这个 Trait 的功能.给人一种打破了孤儿规则的错觉,但实际上是通过包装类型来实现的.

实现类型安全与抽象

通过 newtype 模式,你可以创建一个新的类型来包装一个现有的类型,并为这个新类型实现特定的 Trait,从而实现类型安全和抽象.

新的类型来包装一个现有的类型

可以避免直接使用原始类型带来的潜在错误,比如误用或混淆不同的类型.

使用自定义泛型类型参数 例子中,使用 newtype 来表示单位:Millimeters 和 Meters 结构体都在 newtype 中封装了 u32 值.如果编写了一个有 Millimeters 类型参数的函数,不小心使用 Meters 或普通的 u32 值来调用该函数的程序是不能编译的

抽象类型的细节

通过 newtype 模式,你可以隐藏原始类型的实现细节,只暴露你想要的接口.这有助于实现信息隐藏和封装,使得代码更易于维护和理解.

例如,可以提供一个封装了 HashMap<i32, String>People 类型,用来储存人名以及相应的 ID.使用 People 的代码只需与我们提供的公有 API 交互即可,比如向 People 集合增加名字字符串的方法;这样这些代码就无需知道在内部我们将一个 i32 ID 赋予了这个名字了.

rust
use std::collections::HashMap;

// 定义一个新的类型 People,包装一个 HashMap
struct People {
    map: HashMap<i32, String>,
}

impl People {
    // 提供一个公有方法来添加人名
    fn add_person(&mut self, id: i32, name: String) {
        self.map.insert(id, name);
    }

    // 提供一个公有方法来获取人名
    fn get_person(&self, id: i32) -> Option<&String> {
        self.map.get(&id)
    }
}

fn main() {
    let mut people = People {
        map: HashMap::new(),
    };

    people.add_person(1, "Alice".to_string());
    people.add_person(2, "Bob".to_string());

    if let Some(name) = people.get_person(1) {
        println!("ID 1 is {}", name); // 输出: ID 1 is Alice
    }
}

基于 MIT 协议发布