FORMA

泛型与 Trait

泛型和 trait 是 Rust 实现代码复用与多态的两大支柱。前置:Rust 基础。泛型让代码可以工作在多种类型上而不牺牲性能,trait 定义了类型间的共享行为,二者结合形成了零成本抽象的强大表达能力。本章将深入泛型定义、trait 的设计实现、标准库中最重要的 trait,以及一些高级模式。

6.1 泛型

泛型允许你编写适用于多种类型的代码,同时编译器通过单态化为每个使用的具体类型生成特化版本,确保零运行时开销。

函数泛型

在函数签名中使用泛型参数 T,并为它附加 trait 约束以明确 T 必须支持的操作。

rust
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

T 后面的 : PartialOrd 是 trait 约束,表示类型 T 必须可比较大小。多个泛型参数用逗号分隔,也可用 where 子句提升可读性:

rust
fn some_fn<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{ ... }

结构体与枚举泛型

结构体可以存储任意类型的数据:

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

let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };

枚举同样支持泛型,标准库中的 Option<T>Result<T, E> 就是最典型的例子。可以用多个泛型参数表示不同类型的字段:

rust
struct Pair<T, U> {
    a: T,
    b: U,
}

方法中使用泛型

为泛型类型实现方法时,需要在 impl 后声明泛型参数:

rust
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

可以针对特定类型或满足特定约束的类型单独实现方法。例如,只有 Point<f64> 才提供 distance_from_origin

rust
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

也可以在 impl 上附加 trait 约束,使得方法对所有满足约束的类型可用:

rust
impl<T: Display + PartialOrd> Point<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("x = {}", self.x);
        } else {
            println!("y = {}", self.y);
        }
    }
}

单态化

调用泛型代码时,编译器为每一个使用的具体类型生成一份专用的副本,这个过程叫单态化。例如 largest(&[1, 2, 3]) 会被编译为一份处理 i32largest 函数,与手写非泛型函数拥有相同的性能。代价是编译时间增加,且可能产生更大的二进制文件。

泛型参数默认类型

在 trait 中定义泛型参数时,可以指定默认具体类型,减少显式标注。典型例子是标准库的 Add trait:

rust
trait Add<Rhs=Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}

当写 a + b 时,如果没有显式指定 Rhs,默认就是 Self,即同类型相加。但你可以实现不同 Rhs 来实现异类型相加。

6.2 Trait(特质)

trait 是 Rust 中的共享行为抽象,告诉编译器不同类型共同拥有的功能。它类似于其他语言中的接口,但更强大。

定义 trait

定义一个 Summary trait,包含一个方法签名,也可带默认实现:

rust
pub trait Summary {
    fn summarize(&self) -> String;
}

也可以提供默认方法体,实现者可用也可覆写:

rust
pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

实现 trait

为某个类型实现 trait 的语法是 impl Trait for Type

rust
pub struct NewsArticle {
    pub headline: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}: {}", self.headline, &self.content[..100])
    }
}

孤儿规则

Rust 的 trait 实现受孤儿规则限制:如果要实现一个 trait 或为某个类型实现 trait,至少其中一个必须在当前 crate 中定义。这防止了上游 crate 添加新 trait 实现后破坏下游代码。换句话说,不能同时为外部类型实现外部 trait。

要绕开这一限制,可以使用新类型模式(newtype pattern),在后文介绍。

trait 作为参数

可以用 impl Trait 语法或 trait bound 语法指定要求参数的 trait。

impl Trait 形式:

rust
fn notify(item: &impl Summary) {
    println!("{}", item.summarize());
}

trait bound 形式(显式泛型):

rust
fn notify<T: Summary>(item: &T) {
    println!("{}", item.summarize());
}

当参数类型复杂时,impl Trait 更简洁;当需要多个参数为同一类型或复杂泛型关系时,trait bound 更合适。

trait 作为返回值

可以让函数返回某个实现了指定 trait 的类型,而不暴露具体类型:

rust
fn returns_summarizable() -> impl Summary {
    NewsArticle {
        headline: String::from("Breaking News"),
        content: String::from("Something happened..."),
    }
}

注意:impl Trait 在返回值位置时,函数只能返回单一具体类型,不能根据条件返回不同的实现类型。如果确实需要不同具体类型,就得使用 trait 对象。

dyn Trait:trait 对象

trait 对象用于动态分发,即运行时决定具体方法。类型写作 &dyn TraitBox<dyn Trait>

rust
fn notify(item: &dyn Summary) {
    println!("{}", item.summarize());
}

let article = NewsArticle { ... };
notify(&article);

不同于静态分发的泛型,使用 trait 对象会有小幅运行时开销(虚表查找),且对象的大小在编译期未知,因此通常放在指针后。trait 对象要求使用的 trait 必须是对象安全的,即其方法不能返回 Self 类型、不能有泛型参数(除了关联类型受某些限制等)。

关联类型

关联类型是在 trait 定义中设定的类型占位符,具体类型由实现者指定。它们简化了复杂的泛型 trait 签名。最经典的例子是 Iterator trait:

rust
pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

实现时指定 Item 具体类型:

rust
impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> { ... }
}

关联类型使 Iterator 不需要像 Iterator<Item=T> 那样每次都要标注泛型,接口更干净。

6.3 常用标准 Trait

Rust 标准库提供了一系列基础 trait,它们是类型系统进行数据操作和生态互操作的基石。

格式化:DisplayDebug

  • Debug:面向开发者的调试输出,{:?} 格式串使用。通常通过 #[derive(Debug)] 自动生成。
  • Display:面向用户的格式化输出,{} 使用,需要手动实现 fmt 方法。
rust
use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

CloneCopy

  • Clone:显式深拷贝,调用 .clone() 方法。
  • Copy:隐式浅拷贝(按位复制),标记类型,要求类型不涉及资源所有权。简单的标量、无分配的聚合类型通常派生 Copy, Clone
rust
#[derive(Copy, Clone)]
struct Color { r: u8, g: u8, b: u8 }

实现 Copy 必须同时实现 Clone

比较:PartialEq / EqPartialOrd / Ord

  • PartialEq:允许部分等价关系(如 f64NaN != NaN),提供 ==!=
  • Eq:标记 trait,表示等价关系完全,无部分性。通常也通过 derive 实现。
  • PartialOrd:部分顺序,提供 <> 等。
  • Ord:全序关系,要求类型形成严格的全序。

Hash

实现 Hash 的类型可被哈希,用于 HashMap 的键。可以通过 derive 自动生成,只要所有字段都实现 Hash

Default

提供类型的默认值。很多集合(如 Vec)使用 Default 来初始化。

rust
#[derive(Default)]
struct Config {
    port: u16,
    debug: bool,
}
let config = Config { port: 8080, ..Default::default() };

From / Into

类型转换 trait。实现了 From<A> for B,会自动得到 Into<B> for A。推荐手动实现 From,因为 Into 由标准库通用实现自动生成。

rust
impl From<i32> for MyNumber {
    fn from(val: i32) -> Self {
        MyNumber(val)
    }
}
let num: MyNumber = 42.into();

? 运算符在错误传播时也依赖 From 来转换错误类型。

Deref / DerefMut

实现解引用操作符 *Deref 让智能指针能当普通引用来使用;DerefMut 用于可变解引用。过度使用可能破坏封装性,主要应用在智能指针类型上。

rust
impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.data
    }
}

Drop

析构器,值离开作用域时会自动调用 drop 方法,用于释放资源、关闭文件等。不能手动调用,但可以用 std::mem::drop 提前转移所有权并触发析构。

rust
impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping!");
    }
}

Iterator

迭代器的核心 trait,实现了 Iterator 的类型就能使用 mapfiltercollect 等一整套适配器方法。只需要实现 next 方法,关联类型 Item 指定迭代出的元素类型。

rust
struct Counter { count: u32 }
impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count < 6 { Some(self.count) } else { None }
    }
}

标准库为所有迭代器提供了大量可用方法,零成本抽象。

6.4 高级 Trait

Supertrait(超 trait)

一个 trait 可能依赖于另一个 trait 的功能,此时可以使用超 trait 语法 trait Foo: Bar 表示实现 Foo 的类型必须同时实现 Bar

rust
trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        println!("{}", "*".repeat(output.len() + 4));
        println!("*{}*", " ".repeat(output.len() + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(output.len() + 2));
        println!("{}", "*".repeat(output.len() + 4));
    }
}

这样 OutlinePrint 的方法就可以调用 self.to_string(),因为它要求 Display

完全限定语法

当多个 trait 或类型自身拥有同名方法时,需要使用完全限定语法消除歧义:

rust
<Type as Trait>::function(receiver_if_method, args...);

例如,一个类型同时实现了两个有同名方法的 trait:

rust
trait Pilot { fn fly(&self); }
trait Wizard { fn fly(&self); }
struct Human;

impl Pilot for Human {
    fn fly(&self) { println!("Captain speaking."); }
}
impl Wizard for Human {
    fn fly(&self) { println!("Up!"); }
}
impl Human {
    fn fly(&self) { println!("Waving arms."); }
}

let person = Human;
person.fly();                 // 调用 Human 自身方法
Pilot::fly(&person);          // 调用 Pilot trait 方法
Wizard::fly(&person);         // 调用 Wizard trait 方法

对于关联函数(无 self),需要用 <Human as Wizard>::fly() 形式。

新类型模式(newtype pattern)

孤儿规则阻止我们为外部类型实现外部 trait。新类型模式通过定义一个元组结构体包装外部类型,使它能获得本地的身份,从而可以安全地实现外部 trait。

rust
struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

Wrapper 是本地类型,因此可以为它实现 Display。使用时通过 Wrapper 包装和解构访问内部值。

标记 trait:Send, Sync, Sized, Unpin

这些较为特殊的 trait 通常由编译器自动推导,或者在 unsafe 中手动实现。

  • Send:类型的所有权可以安全地转移到另一个线程。大多数类型都是 Send,但 Rc<T> 不是。
  • Sync:类型的共享引用可以安全地在多个线程间访问。基本类型是 SyncRefCell<T> 不是。
  • Sized:编译期已知大小的类型。默认泛型参数有隐性 Sized 约束,如果要放宽,可用 T: ?Sized
  • Unpin:表示类型可以安全地在内存中移动而不破坏内部指针。大多数类型自动实现 UnpinPin 相关的内容用于异步编程和自引用结构体。

这些标记 trait 构成了 Rust 强大并发和内存安全保证的一部分,会在高级主题中进一步详细应用。

参考文献

以下链接在编写时均可正常访问:

资料说明
泛型官方书第 10 章
Trait官方书
Rust Reference · Traits参考

相关文章