Skip to content

泛型

变量是对内存中数据抽象: 在编写代码阶段可以用变量名代表某个数据,编译阶段再替换为具体的内存地址。

泛型(Generic)是对数据类型的抽象,与变量的含义相同,在编写代码阶段可以用泛型代表各种可能的数据类型,编译阶段再替换为具体的类型。

泛型的核心作用是减少因类型不同而导致的代码冗余

为什么需要泛型

如果没有泛型,为不同类型定义逻辑完全相同的函数时,需要为每种类型写一份:

rust
fn double_u8(i: u8)   -> u8  { i + i }
fn double_i8(i: i8)   -> i8  { i + i }
fn double_u32(i: u32) -> u32 { i + i }
fn double_i32(i: i32) -> i32 { i + i }
// ...每种数值类型都要写一遍

使用泛型后,一个函数可以处理所有满足条件的类型:

rust
use std::ops::Add;

fn double<T>(i: T) -> T
where
    // 泛型 T 必须实现 Add Trait 和 Copy Trait
    // 且 Add Trait 的 Output 关联类型必须是 T 本身
    T: Add<Output = T> + Copy,
{
    i + i
}

fn main() {
    println!("{}", double(3_i32)); // 6
    println!("{}", double(3_u8));  // 6
}

这里的 T 就是泛型,它代表各种可能的数据类型。多数时候泛型使用单个大写字母表示,但也可以使用多个字母。

泛型函数

语法: fn function_name<T>(v: T) -> T { ... }

函数名后的 <T> 在函数作用域内声明一个泛型 T,声明后才能在函数签名和函数体中使用它;

  • (v: T) 参数类型是 T
  • -> T 表示返回值类型是 T
rust
fn return_value<T>(v: T) -> T {
    v
}

fn main() {
    let a = return_value(2_i8);
    let b = return_value(2_u32);
    let c = return_value("hello"); // T 可以是任意类型
}

泛型函数 Trait 约束

泛型默认可以代表任意类型,但函数体内往往需要对参数执行特定操作(如加法、比较、打印),这要求泛型所代表的类型必须具备相应的能力(Trait),这就是泛型函数的 Trait 约束(Trait Bound)

泛型函数中 Trait 约束有两种等价的语法:

写法一: 在泛型声明处直接约束

语法: fn function_name<T: Trait1 + Trait2>(v: T) -> T { ... }

表示泛型 T 必须同时实现 Trait1Trait2 两个 Trait:

rust
// 约束泛型 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
}

写法二: where 子句(多个泛型时可读性更好)

语法: fn function_name<T>(v: T) -> T where T: Trait1 + Trait2 { ... }

表示泛型 T 必须同时实现 Trait1Trait2 两个 Trait:

rust
fn largest<T>(list: &[T]) -> T
where
    // 泛型 T 必须实现 PartialOrd 和 Copy 两个 Trait
    T: PartialOrd + Copy,
{
    let mut largest = list[0];
    for &item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

多个约束用 + 连接,例如 T: Add<Output=T> + Copy + Clone 表示 T 必须同时实现三个 Trait。

为什么需要多重约束?因为函数体内用到什么能力,就需要加对应的 Trait 约束。比如需要加法操作需要 Add Trait,需要参数不被转移所有权需要 Copy Trait,而 Copy Trait 又依赖 Clone Trait,所以三者都要约束。缺什么功能就加什么约束,但不要过度约束。

如果参数是引用,使用 &T&mut T:

rust
use std::fmt::Display;

fn print_value<T: Display>(i: &T) {
    println!("{}", i);
}

泛型结构体

语法: struct StructName<T> { field: T, ... }

结构体中的字段类型也可以用泛型表示:

rust
#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T, // x 和 y 必须是同一种类型 T
}

fn main() {
    let p1 = Point { x: 1, y: 2 };       // T 是 i32
    let p2 = Point { x: 0.5, y: 1.5 };   // T 是 f64
}

若需要不同类型的字段,使用多个泛型参数:

rust
#[derive(Debug)]
struct Point<T, U> {
    x: T,
    y: U, // x 和 y 可以是不同类型
}

let p = Point { x: 1, y: 2.0 }; // T 是 i32,U 是 f64

泛型枚举

语法: enum EnumName<T> { Variant(T), ... }

标准库中最常见的两个枚举都使用了泛型,Option<T>Result<T, E>:

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

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

自定义泛型枚举:

rust
enum Message<T, U> {
    Quit,
    Data(T),
    Error(U),
}

let msg: Message<i32, String> = Message::Data(42);

泛型结构体实现方法

定义一个泛型结构体 Point<T>, 可以为它的不同类型实现不同的方法:

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

为泛型实现方法

语法: impl<T> StructName<T> { ... }

impl 后声明泛型,才能在 Point<T> 的方法中使用 T:

rust
impl<T> Point<T> {
//   ↑ 先在 impl 后声明泛型 T,才能在 Point<T> 中使用它
    fn x(&self) -> &T {
        &self.x
    }
}

为什么要在 impl 后再声明一次 T?因为如果写成 impl Point<T>,编译器会认为 T 是某个具体的类型名,而不是泛型。impl<T> 告诉编译器: 这里的 T 是一个泛型占位符。

为特定类型实现方法

语法: impl StructName<SpecificType> { ... }

可以只为 T 是某个具体类型的实例实现方法,其他类型的实例不具备此方法:

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

只有 Point<f32> 的实例才有 distance_from_origin() 方法

为泛型添加 Trait 约束

语法:

  • impl<T: Trait> StructName<T> { ... } (在 impl 处约束)
  • impl<T> StructName<T> where T: Trait { ... } (在 where 子句处约束)
rust
impl<T: std::fmt::Display> Point<T> {
    fn print(&self) {
        println!("({}, {})", self.x, self.y);
    }
}

// 等同于
impl<T> Point<T> where T: std::fmt::Display {
    fn print(&self) {
        println!("({}, {})", self.x, self.y);
    }
}

只有当泛型 T 实现了 Display Trait 时,Point<T> 才拥有 print() 方法

方法中使用独立泛型

方法可以拥有独立于结构体的泛型参数:

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

impl<T, U> Point<T, U> {
    // 方法自己的泛型 V、W,与结构体的 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); // p3.x = 5, p3.y = c
}

泛型的设计原则

一个重要的设计原则: 尽量不在定义类型时限制泛型,而是在 impl 时限制

rust
// ❌ 不推荐: 在 struct 定义时就限制
struct Food<T: Debug>(T); // 过度限制,所有 Food<T> 的 T 都必须实现 Debug Trait

// ✅ 推荐: struct 定义时不限制,impl 时按需限制
#[derive(Debug)]
struct Food<T>(T);

impl<T: Debug> Eatable for Food<T> {
    fn eat_me(&self) {
        println!("Eating: {:?}", self);
    }
}

原因: 泛型本就是为了表示更通用的类型而存在的,在定义时过度限制会使类型变得具体,适用场景也更窄。在 impl 时遵守"缺什么功能就加什么约束"的原则,这样既不过度泛化,也不过度具体化。

简而言之: 尽量不限制类型是什么,而是限制类型能做什么

单态化: 零运行时开销

Rust 编译器在编译期会将所有泛型替换为具体的数据类型,这个过程叫单态化(monomorphization):

rust
fn double<T: std::ops::Add<Output=T> + Copy>(i: T) -> T { i + i }

fn main() {
    double(3_u32); // 编译器生成 double_u32 版本
    double(3_u8);  // 编译器生成 double_u8 版本
    double(3_i8);  // 编译器生成 double_i8 版本
}

编译后实际上生成了三个函数,每个都针对具体类型。这意味着:

  • 运行时零开销: 程序运行时直接调用对应类型的函数,不需要额外计算泛型所代表的具体类型
  • 代码膨胀: 编译后的二进制文件会因为泛型被展开为多个函数而变大,但多数情况下这不是问题

基于 MIT 协议发布