泛型、trait 与闭包 intermediate 30 分钟 更新 2026-06-15

泛型

用泛型参数写出适用于多种类型的函数和结构体。

泛型

你会学到什么

  • <T> 声明类型参数,写一份代码服务多种类型。
  • 用 trait bound(如 T: PartialOrd)约束泛型能做什么。
  • 给泛型结构体写泛型方法,并支持多个类型参数(Labeled<K, V>)。
  • where 子句表达较长的约束,用条件实现让方法只对满足额外约束的类型可用。
  • 泛型在编译期单态化,没有运行时开销。

最小示例

fn largest<T: PartialOrd + Copy>(items: &[T]) -> Option<T> {
    let mut max = *items.first()?;
    for &item in items {
        if item > max { max = item; }
    }
    Some(max)
}

运行代码

cd examples
cargo run -p rt_16_generics
cargo test -p rt_16_generics

代码讲解

T: PartialOrd + Copy 是 trait bound:PartialOrd> 可用,Copy*item 能按位复制。没有约束的泛型几乎什么都做不了,因为编译器不知道这个类型支持哪些操作。示例里的 largestsmallest 共用同一组约束,只是比较方向不同。

结构体也可以是泛型的。Pair<T> 的两个字段必须同类型,它的 impl<T: Clone> 块里写了泛型方法 newswapped——swapped 需要 Clone,因为 &self 不允许把字段移出借用:

struct Pair<T> {
    first: T,
    second: T,
}

条件实现:同一个类型可以有多个 impl 块,各自带不同约束。Pair<T> 还有一个 impl<T: PartialOrd + Clone> 块提供 larger 方法。于是 Pair 对不可比较的 T 依然能用,只是没有 larger——能力随约束“按需解锁”。

多个类型参数Labeled<K, V> 的标签和数据可以是不同类型。它的 map_value 方法还在方法层面引入了新参数 W,把数据从一种类型映射成另一种,标签类型保持不变。

where 子句:当约束变长时,把它从尖括号挪到 where 更易读。示例用 where K: Display, V: DisplayLabeled 加上只在两者都能打印时才存在的 describe 方法。

调用 largest(&[3, 7, 2]) 时编译器会为 i32 生成一份专用代码(单态化),所以泛型既灵活又零成本。

常见错误

对泛型用了未声明的能力:

fn largest<T>(items: &[T]) -> T {
    let mut max = items[0];
    for item in items {
        if item > max { /* ❌ T 不一定能比较 */ }
    }
    max
}

加上 T: PartialOrd + Copy 即可。

练习

  • Pair<T> 再加一个条件实现:仅当 T: std::fmt::Display 时提供方法,打印两个字段。
  • Labeled<K, V> 加一个 map_label 方法,仿照 map_value 只替换标签部分。

小结

泛型用类型参数复用逻辑,trait bound 声明所需能力,单态化保证零运行时开销。

下一步

trait bound 背后的 trait 本身才是 Rust 抽象的核心。下一章深入 trait。

完整示例代码

下面是 examples/16_generics/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。

examples/16_generics/src/main.rs
//! 泛型:用类型参数写出适用于多种类型的函数、结构体和方法。
//!
//! 本例覆盖:
//! - 带 trait bound 的泛型函数(`largest<T: PartialOrd + Copy>`)。
//! - 泛型结构体 `Pair<T>` 及其泛型方法(`new` / `swapped`)。
//! - 多个类型参数的结构体 `Labeled<K, V>` 与 `where` 子句。
//! - 条件实现(conditional impl):仅当 `T: Display` 时才提供 `describe`。

use std::fmt::Display;

/// 返回切片中最大的元素;空切片返回 `None`。
///
/// `T: PartialOrd + Copy`:`PartialOrd` 让 `>` 可用,`Copy` 让
/// `*item` 能按位复制,从而把元素移出切片而不影响原切片。
fn largest<T: PartialOrd + Copy>(items: &[T]) -> Option<T> {
    let mut iter = items.iter();
    let mut max = *iter.next()?;
    for &item in iter {
        if item > max {
            max = item;
        }
    }
    Some(max)
}

/// 返回切片中最小的元素;空切片返回 `None`。
///
/// 与 `largest` 同样的约束,只是比较方向相反。
fn smallest<T: PartialOrd + Copy>(items: &[T]) -> Option<T> {
    let mut iter = items.iter();
    let mut min = *iter.next()?;
    for &item in iter {
        if item < min {
            min = item;
        }
    }
    Some(min)
}

/// 同类型的一对值。`T` 是唯一的类型参数,两个字段必须同类型。
#[derive(Debug, PartialEq)]
struct Pair<T> {
    first: T,
    second: T,
}

impl<T: Clone> Pair<T> {
    /// 构造一个新的 `Pair`。
    fn new(first: T, second: T) -> Self {
        Self { first, second }
    }

    /// 返回交换两个字段顺序后的新 `Pair`。
    ///
    /// 需要 `T: Clone`,因为 `&self` 不允许把字段移出借用。
    fn swapped(&self) -> Self {
        Self {
            first: self.second.clone(),
            second: self.first.clone(),
        }
    }
}

/// 条件实现:只有当 `T: PartialOrd` 时,`Pair<T>` 才有 `larger` 方法。
///
/// 这样 `Pair<T>` 对不可比较的 `T` 依然可用,只是不提供这个方法。
impl<T: PartialOrd + Clone> Pair<T> {
    /// 返回两个字段中较大的那个(克隆一份)。
    fn larger(&self) -> T {
        if self.first >= self.second {
            self.first.clone()
        } else {
            self.second.clone()
        }
    }
}

/// 带标签的值:两个类型参数 `K`(标签)和 `V`(数据)可以不同。
#[derive(Debug, PartialEq)]
struct Labeled<K, V> {
    label: K,
    value: V,
}

impl<K, V> Labeled<K, V> {
    /// 构造一个带标签的值。
    fn new(label: K, value: V) -> Self {
        Self { label, value }
    }

    /// 只替换数据部分,返回标签类型不变、数据类型可变的新值。
    ///
    /// 注意返回类型是 `Labeled<K, W>`:方法本身又引入了新的类型参数 `W`。
    fn map_value<W>(self, f: impl FnOnce(V) -> W) -> Labeled<K, W> {
        Labeled {
            label: self.label,
            value: f(self.value),
        }
    }
}

/// 用 `where` 子句表达较长的约束,比写在尖括号里更清晰。
///
/// 仅当标签和数据都能打印时,才提供这个方法。
impl<K, V> Labeled<K, V>
where
    K: Display,
    V: Display,
{
    /// 返回 `"标签: 值"` 形式的字符串。
    fn describe(&self) -> String {
        format!("{}: {}", self.label, self.value)
    }
}

fn main() {
    println!("== 泛型函数 ==");
    println!("largest int  = {:?}", largest(&[3, 7, 2, 9, 4]));
    println!("largest char = {:?}", largest(&['a', 'z', 'm']));
    println!("smallest f64 = {:?}", smallest(&[3.5, 1.2, 8.0]));

    println!("\n== 泛型结构体与方法 ==");
    let pair = Pair::new(1, 2);
    println!("pair       = {pair:?}");
    println!("swapped    = {:?}", pair.swapped());
    println!("larger     = {}", pair.larger());

    println!("\n== 多类型参数与 where 子句 ==");
    let item = Labeled::new("price", 42);
    println!("describe   = {}", item.describe());
    // map_value 把数据从 i32 变成 String,标签类型保持不变。
    let mapped = item.map_value(|v| format!("${v}.00"));
    println!("mapped     = {}", mapped.describe());
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn finds_largest() {
        assert_eq!(largest(&[3, 7, 2]), Some(7));
        assert_eq!(largest(&['a', 'c', 'b']), Some('c'));
        assert_eq!(largest::<i32>(&[]), None);
    }

    #[test]
    fn finds_smallest() {
        assert_eq!(smallest(&[3, 7, 2]), Some(2));
        assert_eq!(smallest(&[3.5, 1.2, 8.0]), Some(1.2));
        assert_eq!(smallest::<i32>(&[]), None);
    }

    #[test]
    fn swaps_pair() {
        assert_eq!(Pair::new(1, 2).swapped(), Pair::new(2, 1));
    }

    #[test]
    fn picks_larger_field() {
        assert_eq!(Pair::new(10, 3).larger(), 10);
        assert_eq!(Pair::new("apple", "banana").larger(), "banana");
    }

    #[test]
    fn maps_value_changes_type() {
        let item = Labeled::new("n", 7);
        let mapped = item.map_value(|v| v.to_string());
        assert_eq!(mapped, Labeled::new("n", "7".to_string()));
    }

    #[test]
    fn describes_with_display() {
        assert_eq!(Labeled::new("price", 42).describe(), "price: 42");
    }
}
examples/16_generics/Cargo.toml
[package]
name = "rt_16_generics"
version.workspace = true
edition.workspace = true
publish.workspace = true