在之前的章节中,我们已经接触过 Vec<T>
和 HashMap<K, V>
。你是否想过,这些尖括号里的 T
、K
、V
是什么?它们就是泛型(Generics),一种让我们编写灵活、可复用代码的工具,而无需预先知道具体的类型。
在 Java 中,我们同样使用泛型来实现类型参数化。而当我们想定义一个“契约”,让不同的类都能遵循同一种行为标准时,我们会使用接口(Interfaces)。通过 class MyClass implements MyInterface
,我们实现了多态和代码解耦。
Rust 也有这样一套黄金组合,甚至更为强大。它就是 泛型(Generics) 与 特性(Traits)。在本章,你将看到 trait
是如何成为 Rust 世界里“超级接口”的。
泛型(Generics):编写适用于多种类型的代码
泛型允许我们编写不依赖于特定具体类型的函数、结构体或枚举。这与 Java 泛型的目的一致。
假设我们要写一个函数,找出 i32
切片中最大的数字。很简单:
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
但如果想让它也适用于 char
切片,或者我们自己定义的其他类型呢?我们会发现无法编译,因为编译器不知道一个通用的类型 T
是否支持比较操作(>
)。
这时,泛型就登场了。我们可以定义一个通用的 largest
函数:
// 注意:这段代码还不能完全编译!
fn largest<T>(list: &[T]) -> T {
// ...
}
编译器会报错,提示 binary operation
>cannot be applied to type
T``。它需要一个保证,一个契约,来确保类型 T
是可比较的。这个契约,就是trait
。
特性(Traits):定义共享行为的“接口”
一个 trait 告诉 Rust 编译器,某种类型具有哪些并且可以与其它类型共享的功能。它定义了一组方法签名,非常类似于 Java 的 interface
。
让我们来定义一个 Summary
trait:
pub trait Summary {
// 定义一个方法签名,它需要一个 &self 引用,并返回一个 String
fn summarize(&self) -> String;
}
现在,任何类型都可以通过 impl Trait for Type
的语法来实现这个 trait
,就像 Java 的 implements
关键字一样。
pub struct NewsArticle {
pub headline: String,
pub author: String,
}
// 为 NewsArticle 实现 Summary trait
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {}", self.headline, self.author)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
}
// 为 Tweet 实现 Summary trait
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{} says: {}", self.username, self.content)
}
}
默认实现(Default Implementations): 和 Java 8+ 的接口一样,trait
也可以拥有默认的方法实现。其他类型可以直接使用这个默认实现,或者重写它。
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)") // 默认实现
}
}
// 现在,一个类型可以 `impl Summary for MyType {}` 而无需提供任何代码
强强联合:泛型 + Trait 限定(T: Trait
)
现在,我们可以回头解决 largest
函数的问题了。我们需要告诉编译器,泛型 T
必须是可比较的。在标准库中,这个行为由 PartialOrd
trait 定义。
我们可以通过 trait 限定(trait bound) 将这两者结合起来:
// T: PartialOrd + Copy 是 trait 限定
// 它表示 T 可以是任何实现了 PartialOrd 和 Copy 这两个 trait 的类型
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
T: PartialOrd
这个语法,与 Java 中的 <T extends Comparable<T>>
在概念上是完全等价的。它为泛型参数 T
设定了能力的边界。
作为参数与返回值的 Trait:Rust 的多态性
Trait 作为函数参数
我们可以编写一个函数,它能接收任何实现了 Summary
trait 的类型。这和在 Java 方法参数中使用接口类型是一样的。
// 语法糖形式:item 参数是任何实现了 Summary trait 的类型的不可变引用
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
// 等价的、更正式的 trait 限定形式
pub fn notify_long<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
Trait 作为函数返回值
同样,你也可以声明一个函数返回一个“实现了某个 trait 的类型”,而无需指明具体的返回类型。
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
}
}
性能内幕:静态分发 vs. 动态分发
这里隐藏着 Rust 与 Java 的一个重大性能差异。 当你使用泛型和 trait 限定(如 fn notify<T: Summary>(item: &T)
) 时,Rust 在编译期会执行一个叫做**单态化(Monomorphization)**的过程。编译器会根据你实际使用的具体类型(NewsArticle
或 Tweet
),为这个函数生成专门的版本。
notify_NewsArticle(item: &NewsArticle)
notify_Tweet(item: &Tweet)
这意味着,在运行时,调用 item.summarize()
就和直接调用具体类型的方法一样快,没有任何额外的查找开销。这被称为静态分发(Static Dispatch)。
而 Java 中使用接口作为参数时,通常会导致动态分发(Dynamic Dispatch)。JVM 在运行时需要通过虚函数表(vtable)来查找应该调用哪个对象的具体方法。这会带来微小但确实存在的运行时开销。
Rust 的这种设计,让它的抽象能力达到了“零成本抽象”(Zero-Cost Abstraction)的境界。 你可以编写高度抽象、可复用的代码,而无需为这种抽象付出任何运行时性能代价。
本章小结
我们今天探索了 Rust 实现代码抽象和复用的核心武器。
- 泛型(Generics) 让我们的代码可以适用于多种不同的数据类型。
- 特性(Traits) 定义了共享的行为契约,是 Rust 版本的“接口”。
- 泛型与 Trait 限定的结合 (
<T: Trait>
),实现了与 Java<T extends Interface>
类似的功能,但通过静态分发实现了零成本的性能。
可以说,trait
是 Rust 语言设计中最为闪亮的瑰宝之一。它避免了传统面向对象语言中复杂的继承体系,鼓励使用更灵活的组合模式,并通过与所有权系统的结合,提供了无与伦比的性能和安全性。
至此,你已掌握 Rust 的核心语言、安全机制和抽象能力。前方,我们将进入 Rust 最富盛名的领域——并发编程。
在下一章 《“无畏并发”不是梦:Rust 如何在编译期终结数据竞争》 中,你将亲眼见证,我们之前学习的所有权和借用规则,是如何在多线程的世界里大放异彩,兑现 Rust “无畏并发”的承诺。