第五章:泛型与Trait——代码复用的艺术

第五章:泛型与Trait——代码复用的艺术

本章导读:好的代码应该一次编写,处处适用。但如何在保持灵活性的同时确保类型安全?Rust 的答案是泛型和 Trait。泛型让你编写适用于多种类型的代码,Trait 让你定义共享的行为。本章将揭示 Rust 如何通过这两者的结合,实现零成本的多态。


🧩 5.1 泛型:参数化类型

泛型是一种将类型作为参数的机制,让你编写可以处理多种类型的通用代码。

📦 5.1.1 泛型函数

// 接收任何类型的向量,返回最大值
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    let numbers = vec![34, 50, 12, 100, 65];
    println!("最大数字: {}", largest(&numbers));
    
    let chars = vec!['y', 'm', 'a', 'q'];
    println!("最大字符: {}", largest(&chars));
}

` 声明这是一个泛型函数,T 是类型参数。: PartialOrd 是一个 trait 约束,表示 T` 必须支持比较操作。

为什么需要 trait 约束? 泛型 T 可以是任何类型,但不是所有类型都能比较大小。PartialOrd trait 约束告诉编译器:只有实现了比较操作的类型才能使用这个函数。

🏗️ 5.1.2 泛型结构体

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

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

// 为特定类型实现特化方法
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let integer_point = Point { x: 5, y: 10 };
    let float_point = Point { x: 1.0, y: 4.0 };
    
    println!("integer_point.x = {}", integer_point.x());
    println!("distance = {}", float_point.distance_from_origin());
}

🎭 5.1.3 多类型参数

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

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };
    
    let p3 = p1.mixup(p2);
    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

🔬 5.1.4 泛型枚举

标准库中有很多泛型枚举:

// Option<T>
enum Option<T> {
    Some(T),
    None,
}

// Result<T, E>
enum Result<T, E> {
    Ok(T),
    Err(E),
}

🎯 5.2 Trait:共享行为

Trait 定义了一组方法签名,描述某种行为。任何类型都可以实现 Trait,从而拥有这种行为。

📝 5.2.1 定义和实现 Trait

// 定义 Trait
trait Summary {
    fn summarize(&self) -> String;
    
    // 可以有默认实现
    fn author(&self) -> String {
        String::from("未知作者")
    }
}

// 定义结构体
struct Article {
    title: String,
    content: String,
    author: String,
}

struct Tweet {
    username: String,
    content: String,
}

// 为 Article 实现 Trait
impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{} - {}", self.title, self.author)
    }
    
    // 覆盖默认实现
    fn author(&self) -> String {
        format!("作者: {}", self.author)
    }
}

// 为 Tweet 实现 Trait
impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.content)
    }
    // 使用默认的 author 实现
}

fn main() {
    let article = Article {
        title: String::from("Rust 入门"),
        content: String::from("Rust 是一门系统编程语言..."),
        author: String::from("张三"),
    };
    
    let tweet = Tweet {
        username: String::from("rustdev"),
        content: String::from("今天学 Rust!"),
    };
    
    println!("{}", article.summarize());
    println!("{}", tweet.summarize());
    println!("{}", tweet.author());  // 使用默认实现
}

Trait vs 接口:如果你来自 Java 或 C#,可能会把 Trait 等同于接口。它们确实相似,但有一些关键区别:Rust 允许为外部类型实现外部 Trait(孤儿规则除外),这使得扩展性更强;Rust 的 Trait 可以有关联类型和关联常量。

🏠 5.2.2 Trait 作为参数

// impl Trait 语法
fn notify(item: &impl Summary) {
    println!("通知: {}", item.summarize());
}

// Trait Bound 语法(等价但更灵活)
fn notify2<T: Summary>(item: &T) {
    println!("通知: {}", item.summarize());
}

// 多个 Trait 约束
fn notify3(item: &(impl Summary + Display)) {
    // ...
}

// where 子句(更清晰)
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    // ...
}

🔄 5.2.3 返回 Trait

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        Article {
            title: String::from("Rust"),
            content: String::from("内容"),
            author: String::from("作者"),
        }
    } else {
        Tweet {
            username: String::from("user"),
            content: String::from("推文"),
        }
    }
}

限制impl Trait 返回类型只能返回单一具体类型。如果函数可能返回 ArticleTweet(取决于运行时条件),需要使用 Trait 对象 Box

🎨 5.2.4 条件实现

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

// 对所有 T 实现 new
impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

// 只对实现了 Display + PartialOrd 的 T 实现 cmp_display
impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("x >= y: {} >= {}", self.x, self.y);
        } else {
            println!("x < y: {} < {}", self.x, self.y);
        }
    }
}

🏆 5.3 标准库中的重要 Trait

Rust 标准库定义了许多核心 Trait,理解它们对于编写惯用代码至关重要。

🔄 5.3.1 CloneCopy

// Clone:显式深拷贝
let s1 = String::from("hello");
let s2 = s1.clone();  // 显式调用

// Copy:隐式拷贝(只适用于栈上简单类型)
let x = 5;
let y = x;  // 自动复制

CopyClone 的子集,表示类型可以通过简单的位复制来复制。实现 Copy 的类型在赋值时会自动复制,而不是移动。

📋 5.3.2 DebugDisplay

use std::fmt;

// Debug:用于调试,可用 {:?} 或 {:#?} 打印
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

// Display:用于用户友好的输出,用 {} 打印
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 10, y: 20 };
    println!("Debug: {:?}", p);    // Debug: Point { x: 10, y: 20 }
    println!("Display: {}", p);     // Display: (10, 20)
}

🔍 5.3.3 PartialEqEq

#[derive(PartialEq, Eq)]
struct Person {
    id: u32,
    name: String,
}

fn main() {
    let p1 = Person { id: 1, name: String::from("张三") };
    let p2 = Person { id: 1, name: String::from("张三") };
    
    if p1 == p2 {
        println!("是同一个人");
    }
}

PartialEq 允许部分相等比较(如浮点数的 NaN),Eq 要求完全的相等关系。

📊 5.3.4 PartialOrdOrd

用于比较大小:

use std::cmp::Ordering;

#[derive(Eq, PartialEq)]
struct Person {
    name: String,
    age: u32,
}

impl PartialOrd for Person {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.age.cmp(&other.age))
    }
}

impl Ord for Person {
    fn cmp(&self, other: &Self) -> Ordering {
        self.age.cmp(&other.age)
    }
}

🏭 5.3.5 FromInto

用于类型转换:

let s = String::from("hello");  // From<&str> for String

let num: i32 = "42".parse().unwrap();  // FromStr trait

// Into 自动获得(实现了 From 就自动实现 Into)
fn print_it(input: impl Into<String>) {
    let s = input.into();
    println!("{}", s);
}

print_it("hello");  // &str 自动转换为 String

🏃 5.3.6 Default

提供默认值:

#[derive(Default)]
struct Config {
    debug: bool,      // 默认 false
    port: u16,        // 默认 0
    host: String,     // 默认空字符串
}

fn main() {
    let config = Config::default();
    println!("debug: {}, port: {}", config.debug, config.port);
    
    // 使用 .. 语法部分覆盖
    let config = Config {
        port: 8080,
        ..Default::default()
    };
}

⚡ 5.4 Trait 对象:动态多态

Trait 对象允许在运行时处理不同具体类型的值,实现动态分发。

🎭 5.4.1 什么是 Trait 对象?

// 静态分发(编译时确定具体类型)
fn static_dispatch(item: &impl Summary) {
    println!("{}", item.summarize());
}

// 动态分发(运行时确定具体类型)
fn dynamic_dispatch(item: &dyn Summary) {
    println!("{}", item.summarize());
}

dyn Summary 是一个 Trait 对象。它使用虚函数表(vtable)在运行时查找正确的方法实现。

📦 5.4.2 使用场景

// 存储不同类型的集合
let items: Vec<Box<dyn Summary>> = vec![
    Box::new(Article { /* ... */ }),
    Box::new(Tweet { /* ... */ }),
];

for item in &items {
    println!("{}", item.summarize());
}

⚖️ 5.4.3 静态分发 vs 动态分发

特性静态分发(泛型)动态分发(Trait 对象)
性能更快(可内联)略慢(虚函数调用)
代码大小可能膨胀更小
异构集合不支持支持
编译时检查完全部分

选择指南:优先使用静态分发(泛型 + impl Trait)。只有在需要异构集合或运行时灵活性时才使用 Trait 对象。


🧪 5.5 高级 Trait 特性

🔗 5.5.1 关联类型

trait Container {
    type Item;  // 关联类型
    
    fn get(&self) -> Option<&Self::Item>;
    fn add(&mut self, item: Self::Item);
}

struct Backpack {
    items: Vec<String>,
}

impl Container for Backpack {
    type Item = String;  // 指定具体类型
    
    fn get(&self) -> Option<&Self::Item> {
        self.items.last()
    }
    
    fn add(&mut self, item: Self::Item) {
        self.items.push(item);
    }
}

关联类型让 Trait 更加灵活,同时避免了泛型参数爆炸。

🏷️ 5.5.2 关联常量

trait Shape {
    const NAME: &'static str;
    const SIDES: u32;
    
    fn describe(&self) -> String {
        format!("{} 有 {} 条边", Self::NAME, Self::SIDES)
    }
}

struct Triangle;
impl Shape for Triangle {
    const NAME: &'static str = "三角形";
    const SIDES: u32 = 3;
}

🔄 5.5.3 Supertrait

Trait 可以要求实现它的类型同时也实现其他 Trait:

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

📐 5.5.4 Newtype 模式

为外部类型实现外部 Trait(绕过孤儿规则):

// 无法直接为 Vec<i32> 实现 Display(都是外部类型)
// 使用 newtype 包装
struct Wrapper(Vec<i32>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.iter()
            .map(|n| n.to_string())
            .collect::<Vec<_>>()
            .join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![1, 2, 3]);
    println!("{}", w);  // [1, 2, 3]
}

🏗️ 5.6 实战:构建一个泛型缓存

让我们综合运用本章知识,构建一个类型安全的缓存系统:

use std::collections::HashMap;
use std::hash::Hash;
use std::time::{Duration, Instant};

/// 缓存条目
struct CacheEntry<T> {
    value: T,
    expires_at: Instant,
}

impl<T> CacheEntry<T> {
    fn new(value: T, ttl: Duration) -> Self {
        Self {
            value,
            expires_at: Instant::now() + ttl,
        }
    }
    
    fn is_expired(&self) -> bool {
        Instant::now() > self.expires_at
    }
}

/// 泛型缓存
struct Cache<K, V> {
    store: HashMap<K, CacheEntry<V>>,
    default_ttl: Duration,
}

impl<K, V> Cache<K, V>
where
    K: Eq + Hash + Clone,
{
    fn new(default_ttl: Duration) -> Self {
        Self {
            store: HashMap::new(),
            default_ttl,
        }
    }
    
    fn insert(&mut self, key: K, value: V) {
        self.insert_with_ttl(key, value, self.default_ttl);
    }
    
    fn insert_with_ttl(&mut self, key: K, value: V, ttl: Duration) {
        let entry = CacheEntry::new(value, ttl);
        self.store.insert(key, entry);
    }
    
    fn get(&mut self, key: &K) -> Option<&V> {
        // 清理过期条目
        self.cleanup_expired();
        
        self.store.get(key).map(|entry| &entry.value)
    }
    
    fn remove(&mut self, key: &K) -> Option<V> {
        self.store.remove(key).map(|entry| entry.value)
    }
    
    fn cleanup_expired(&mut self) {
        let expired: Vec<K> = self.store
            .iter()
            .filter(|(_, entry)| entry.is_expired())
            .map(|(key, _)| key.clone())
            .collect();
        
        for key in expired {
            self.store.remove(&key);
        }
    }
    
    fn len(&self) -> usize {
        self.store.len()
    }
}

fn main() {
    let mut cache: Cache<String, String> = Cache::new(Duration::from_secs(60));
    
    cache.insert("name".to_string(), "张三".to_string());
    cache.insert_with_ttl("temp".to_string(), "临时数据".to_string(), Duration::from_millis(100));
    
    println!("缓存大小: {}", cache.len());
    
    if let Some(name) = cache.get(&"name".to_string()) {
        println!("姓名: {}", name);
    }
    
    std::thread::sleep(Duration::from_millis(150));
    cache.cleanup_expired();
    println!("清理后大小: {}", cache.len());
}

这个例子展示了:

  • 泛型结构体Cache<K, V> 支持任意键值类型
  • Trait 约束K: Eq + Hash + Clone 确保键可用作 HashMap 的键
  • 关联类型:虽然这里没用到,但 HashMap 内部使用了关联类型
  • 内部可变性和生命周期管理

📝 本章小结

本章我们深入学习了 Rust 的泛型和 Trait 系统:

  • 泛型让你编写适用于多种类型的代码,编译器为每种具体类型生成专门的代码(单态化)
  • Trait 定义共享行为,任何类型都可以实现 Trait
  • Trait 约束限制泛型参数必须实现特定的 Trait
  • Trait 对象 dyn Trait 实现动态分发和异构集合
  • 标准库 TraitCloneDebugDisplayFrom 等是 Rust 生态的基础

关键要点:

  • 优先使用静态分发,只在需要时使用动态分发
  • 使用 where 子句让复杂的 trait 约束更清晰
  • 利用 #[derive] 自动实现常见 Trait
  • 理解孤儿规则:只能为本地类型实现本地 Trait

在下一章,我们将学习 Rust 的错误处理机制——如何用 OptionResult 优雅地处理可能失败的操作。


动手实验

  1. 为上一章的 JsonValue 实现 Display trait,使其能够格式化输出。
  2. 实现一个泛型函数 first_match<T, P>(items: &[T], predicate: P) -> Option<&T>,其中 P 是一个返回 bool 的闭包。
  3. 定义一个 Drawable trait,包含 draw(&self) 方法。为不同的形状(圆、矩形)实现这个 trait,然后用 Vec<Box<dyn Drawable>> 存储并绘制它们。
← 返回目录