【Rust自学】19.2. 高级trait:关联类型、默认泛型参数和运算符重载、完全限定语法、supertrait和newtype

📅 2026/7/23 19:00:31
【Rust自学】19.2. 高级trait:关联类型、默认泛型参数和运算符重载、完全限定语法、supertrait和newtype
19.2 高级 trait关联类型、默认泛型参数和运算符重载、完全限定语法、supertrait 和 newtype19.2.1 在 trait 定义中使用关联类型来指定占位类型我们首先在 10.3. trait Pt.1trait的定义、约束与实现 介绍了 trait但没有讨论更高级的细节。现在来深入了解。关联类型是 trait 内部的类型占位符。它可以用于 trait 方法签名中。它用来为某些类型定义 trait而无需事先知道这些类型是什么。例如pub trait Iterator { type Item; fn next(mut self) - OptionSelf::Item; }标准库的Iteratortrait 就是一个带有关联类型的 trait其定义如上所示。Item就是关联类型。在迭代过程中用Item代替实际值的类型从而把逻辑和具体数据类型分开。你可以在next的返回类型OptionSelf::Item中看到Item。Item是一个类型占位符。它的核心思想与泛型类似但也有区别泛型关联类型每次实现 trait 时都要指定类型无需指定类型同一个类型可以用不同的泛型参数多次实现同一个 trait同一个类型不能多次实现同一个 trait19.2.2 默认泛型类型参数和运算符重载使用泛型参数时我们可以给泛型一个默认的具体类型。语法是PlaceholderTypeConcreteType。这项技术常用于运算符重载。虽然 Rust 不允许你创建自己的运算符也不能重载任意运算符但你可以通过实现std::ops中列出的那些 trait 来重载某些运算符。看一个例子use std::ops::Add; #[derive(Debug, Copy, Clone, PartialEq)] struct Point { x: i32, y: i32, } impl Add for Point { type Output Point; fn add(self, other: Point) - Point { Point { x: self.x other.x, y: self.y other.y, } } } fn main() { assert_eq!( Point { x: 1, y: 0 } Point { x: 2, y: 3 }, Point { x: 3, y: 3 } ); }在这个例子中我们为Point结构体实现了Addtrait从而重载了运算符。具体来说Add中的add函数逐字段相加。在main中我们可以直接用把两个Point值相加。Addtrait 的定义如下trait AddRhsSelf { type Output; fn add(self, rhs: Rhs) - Self::Output; }它使用了默认泛型参数RhsSelf。这意味着当我们实现Add时如果不给Rhs指定具体类型默认类型就是Self。所以上面例子中的Rhs是Point。现在再看另一个例子这次是毫米和米相加use std::ops::Add; struct Millimeters(u32); struct Meters(u32); impl AddMeters for Millimeters { type Output Millimeters; fn add(self, other: Meters) - Millimeters { Millimeters(self.0 (other.0 * 1000)) } }这里把Millimeters和Meters声明为元组结构体分别表示毫米和米。我们为Millimeters实现Add并显式指定另一个类型是Meters。在add中我们把存储的毫米值与换算成毫米后的米值相加。19.2.3 默认泛型参数的主要用例在不破坏现有代码的前提下扩展类型允许在大多数用户不需要的特殊情况下进行自定义19.2.4 使用完全限定语法调用同名方法直接看例子trait Pilot { fn fly(self); } trait Wizard { fn fly(self); } struct Human; impl Pilot for Human { fn fly(self) { println!(This is your captain speaking.); } } impl Wizard for Human { fn fly(self) { println!(Up!); } } impl Human { fn fly(self) { println!(*waving arms furiously*); } }我们定义了两个 traitPilot和Wizard各自都有一个fly方法但没有具体实现。我们有一个Human结构体。接下来分别为它实现两个 trait也就是为每个 trait 提供一个fly方法。此外我们还在结构体自己的impl块中实现了一个fly方法。此时一共有三个fly方法。如果在main中这样调用fn main() { let person Human; person.fly(); }运行这段代码会打印*waving arms furiously*说明 Rust 直接调用了Human上实现的fly方法。要调用Pilottrait 或Wizardtrait 中的fly需要用更明确的语法指出我们指的是哪一个flyfn main() { let person Human; Pilot::fly(person); Wizard::fly(person); person.fly(); }在方法名前指定 trait 名可以告诉 Rust 我们想要哪一个fly实现。person.fly()也可以写成Human::fly(person)。输出This is your captain speaking. Up! *waving arms furiously*然而不是方法的关联函数没有self参数。当来自不同类型或 trait 的多个方法或关联函数同名时除非使用完全限定语法Rust 并不总是知道你指的是哪一个trait Animal { fn baby_name() - String; } struct Dog; impl Dog { fn baby_name() - String { String::from(Spot) } } impl Animal for Dog { fn baby_name() - String { String::from(puppy) } } fn main() { println!(A baby dog is called a {}, Dog::baby_name()); }Animaltrait 有一个baby_name函数。Dog是一个结构体实现了Animaltrait同时也在自己的impl块中实现了baby_name。所以现在有两个baby_name函数。在main中使用了Dog::baby_name()因此按上面的逻辑会运行Dog自己的impl块中的baby_name实现得到Spot。输出A baby dog is called a Spot那么如何调用Dog对Animaltrait 的baby_name实现呢我们试试上一个例子的逻辑fn main() { println!(A baby dog is called a {}, Animal::baby_name()); }输出error[E0790]: cannot call associated function on trait without specifying the corresponding impl type -- src/main.rs:20:43 | 2 | fn baby_name() - String; | ------------------------- Animal::baby_name defined here ... 20 | println!(A baby dog is called a {}, Animal::baby_name()); | ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait | help: use the fully-qualified path to the only available implementation | 20 | println!(A baby dog is called a {}, Dog as Animal::baby_name()); | For more information about this error, try rustc --explain E0790. error: could not compile traits-example (bin traits-example) due to 1 previous errorAnimaltrait 上的baby_name函数需要知道使用哪个类型的实现但baby_name本身没有参数所以 Rust 无法推断指的是哪个类型的实现。这时就需要完全限定语法。它的形式是Type as Trait::function(receiver_if_method, next_arg, ...);这种语法可以在任何调用函数或方法的地方使用并且可以忽略那些能从其它上下文推断出来的部分。但只有在 Rust 无法区分你想要哪个具体实现时才需要这种语法因为它写起来很麻烦。所以一般来说除非必要否则不要使用它。按这个语法上面的代码应改为fn main() { println!(A baby dog is called a {}, Dog as Animal::baby_name()); }输出A baby dog is called a puppy19.2.5 使用 supertrait 要求额外的 trait 功能有时我们需要在一个 trait 中使用另一个 trait 的功能这意味着那个被间接要求的 trait 也必须被实现。那个被间接要求的 trait 就是当前 trait 的 supertrait。例如use std::fmt; trait OutlinePrint: fmt::Display { fn outline_print(self) { let output self.to_string(); let len output.len(); println!({}, *.repeat(len 4)); println!(*{}*, .repeat(len 2)); println!(* {output} *); println!(*{}*, .repeat(len 2)); println!({}, *.repeat(len 4)); } }OutlinePrint实际上用来在终端用字符打印一个形状。但在打印时self必须实现to_string这意味着self必须实现Displaytraitto_string来自ToStringtrait而任何实现了Display的类型都会自动实现ToString。写法是trait关键字 trait 名 : supertrait。假设我们有一个Point结构体想用OutlinePrint的outline_print方法在终端打印它。因为OutlinePrint要求Display我们必须同时实现OutlinePrint和Display否则会失败struct Point { x: i32, y: i32, } use std::fmt; impl fmt::Display for Point { fn fmt(self, f: mut fmt::Formatter) - fmt::Result { write!(f, ({}, {}), self.x, self.y) } } impl OutlinePrint for Point {}19.2.6 使用 newtype 模式在外部类型上实现外部 trait我们已经讨论过孤儿规则只有当 trait 或类型定义在本地 crate 中时才能为该类型实现这个 trait。我们可以使用 newtype 模式绕过这条规则具体做法是用元组结构体在本地构建一个新类型。例如假设我们想为VecString实现Display但Vec和Display都定义在我们的 crate 之外所以不能直接为VecString实现。于是我们把这个向量包进自己的元组结构体Wrapper再为Wrapper实现Displayuse std::fmt; struct Wrapper(VecString); impl fmt::Display for Wrapper { fn fmt(self, f: mut fmt::Formatter) - fmt::Result { write!(f, [{}], self.0.join(, )) } } fn main() { let w Wrapper(vec![String::from(hello), String::from(world)]); println!(w {w}); }