Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

7. 泛型与关联类型

7.1 泛型简介

  在开发过程中,可能会遇到逻辑完全一致、仅数据类型不同的场景。泛型允许在定义函数、结构体、枚举或 trait 时使用类型参数,将具体类型延迟到使用时再确定,使同一份代码适用于多种数据类型。泛型不会引入运行时开销,同时保留静态类型检查的安全性——编译器在编译时即根据实际使用情况,为每种具体类型生成专属代码并执行类型检查,从而在编译期杜绝类型错误。

  不过,泛型并非没有代价,当同一泛型被实例化为大量不同的具体类型时,编译器会为每种类型生成独立代码,导致二进制文件体积膨胀,这种现象称为代码膨胀(Code Bloat)。

7.2 泛型基本使用

  在 Rust 中,泛型通过在函数、结构体或枚举的定义后附加类型参数定义,通常以 <T> 的形式表示。其中 T 为自定义标识符,作为类型占位符代表任意类型。此外,还可同时声明多个泛型参数,以满足更复杂的类型抽象需求。

// 泛型函数,使函数能够接收不同类型的参数/返回值
fn identity<T>(value: T) -> T {
    value
}

// 泛型枚举
#[derive(Debug)]
enum Option<T = i32> {
    Some(T),
    None,
}

// 泛型结构体,为结果体同时声明多个泛型参数
#[allow(dead_code)]
#[derive(Debug)]
struct Point<T, E> {
    x: T,
    y: E,
}

fn main() {
    // 使用泛型函数,可以传递多种类型的参数
    let a = identity(10);
    let b = identity("hello");
    println!("泛型函数:{}, {}", a, b);

    // 使用泛型枚举
    let some_value: Option<i32> = Option::Some(42);
    let none_value: Option<i64> = Option::None;
    println!("泛型枚举:{:?}, {:?}", some_value, none_value);

    // 使用泛型结构体,可以支持多种类型,如 i32 和 f64
    let int_point = Point { x: 1_i32, y: 2_i32 };
    let float_point = Point { x: 1.0_f64, y: 2.0_f64 };
    println!("泛型结构体:{:?}, {:?}", int_point, float_point);
}
shell> cargo run
泛型函数:10, hello
泛型枚举:Some(42), None
泛型结构体:Point { x: 1, y: 2 }, Point { x: 1.0, y: 2.0 }

  在编译期,泛型会被单态化替换为具体类型,编译器会为每种实例化的类型生成专属实现。编译上述代码时,由于 identity() 分别以 i32 和 f64 两种类型调用,最终生成的可执行文件中包含两个独立的 identity() 函数。

#![allow(unused)]
fn main() {
查看符号表,存在两个 identity() 函数
shell> cargo nm | grep identity
0000000000014850 t hello::identity::h929bb7f5fad73533
0000000000014860 t hello::identity::hb8887b338e6642bf
}

7.3 泛型约束

  默认情况下,泛型可接受任意类型,但实际使用中通常需通过泛型约束(Trait Bound)加以限制,确保泛型具备指定行为,以便在泛型代码中调用相关方法或执行运算。泛型约束可通过 <T: Trait> 形式直接声明,也可以使用 where T: Trait 表达,两者均表示泛型类型必须实现指定的 trait。

trait Transport {
    fn travel_time(&self, distance_km: f64) -> f64;
}

// 写法一:在尖括号中直接约束
fn estimate_time<T: Transport>(transport: T, distance_km: f64) -> f64 {
    transport.travel_time(distance_km)
}

// 写法二:使用 where 子句(更清晰,适合复杂约束)
fn estimate_time_with_where<T>(transport: T, distance_km: f64) -> f64
where
    T: Transport
{
    transport.travel_time(distance_km)
}

// 具体实现类型
struct Car;

impl Transport for Car {
    fn travel_time(&self, distance_km: f64) -> f64 {
        let speed_kmh = 80.0;
        distance_km / speed_kmh
    }
}

fn main() {
    let car = Car;

    let time1 = estimate_time(car, 160.0);
    println!("时间: {} 小时", time1);

    let time2 = estimate_time_with_where(Car, 200.0);
    println!("时间: {} 小时", time2);
}

  此外,泛型约束可同时指定多个 trait,使用加号(+)进行组合,表示类型必须同时满足所有约束条件。

// 定义两个 trait
trait Named {
    fn name(&self) -> &str;
}

trait Transport {
    fn travel_time(&self, distance_km: f64) -> f64;
}

// 写法一:在尖括号中声明多个约束
fn print_travel_info<T: Transport + Named>(transport: T, distance_km: f64) {
    let time = transport.travel_time(distance_km);
    println!("{} 需要 {} 小时", transport.name(), time);
}

// 写法二:使用 where 子句声明多个约束
fn print_travel_info_with_where<T>(transport: T, distance_km: f64)
where
    T: Transport + Named,
{
    let time = transport.travel_time(distance_km);
    println!("{} 需要 {} 小时", transport.name(), time);
}

// 具体实现类型
struct Car;

impl Named for Car {
    fn name(&self) -> &str {
        "Car"
    }
}

impl Transport for Car {
    fn travel_time(&self, distance_km: f64) -> f64 {
        let speed_kmh = 80.0;
        distance_km / speed_kmh
    }
}

fn main() {
    print_travel_info(Car, 160.0);
    print_travel_info_with_where(Car, 200.0);
}

7.4 泛型与幽灵数据类型

  幽灵数据(PhantomData)用于标示类型与泛型参数、生命周期之间的关联,其大小为零(即零大小类型,Zero-Sized Type,ZST),不占用实际内存,也不存储任何运行时数据,仅在编译期存在,但可辅助编译器完成类型检查与生命周期分析。它常用于实现类型状态模式,确保仅处于正确状态的类型方可调用特定方法,从而在编译期保证程序的一致性与正确性。

  下述示例通过 PhantomData 实现火箭发射的类型状态模式。定义 Prepare(预备)和 Ready(就绪)两个零大小类型表示火箭状态,并通过 PhantomData 将状态信息绑定到 Rocket 类型。随后,为不同状态定义对应的操作函数:Prepare 状态下可以添加燃料,转换为 Ready 状态后才能执行发射操作。由于编译器会在编译阶段检查类型是否匹配,若在 Prepare 状态下调用 launch(),将导致编译错误,从而将运行时风险提前至编译期,保证状态转换的正确性和操作安全性。

use std::marker::PhantomData;

// 定义火箭状态,mod 见工程化章节
#[allow(non_snake_case)]
mod RocketState {
    // 预备阶段
    pub struct Prepare;
    // 已准备好
    pub struct Ready;
}

// 定义火箭结构体
struct Rocket<S> {
    name: String,
    _state: PhantomData<S>,
}

// 定义预备阶段可以执行的操作
impl Rocket<RocketState::Prepare> {
    fn new(name: &str) -> Self {
        println!("{} 已创建,准备添加燃料...", name);
        Rocket {
            name: name.to_string(),
            _state: PhantomData,
        }
    }

    // 添加燃料操作,完成后从 Prepare 切换为 Ready 状态
    fn add_fuel(self) -> Rocket<RocketState::Ready> {
        println!("{} 添加燃料完毕!", self.name);
        Rocket {
            name: self.name,
            _state: PhantomData,
        }
    }
}

// 定义已准备好状态可以执行的操作
impl Rocket<RocketState::Ready> {
    fn launch(&self) {
        println!("{} 发射升空,目标星辰大海!", self.name);
    }
}

fn main() {
    // 创建火箭
    let rocket = Rocket::<RocketState::Prepare>::new("神舟火箭");
    // rocket.launch();  // 编译报错,预备阶段无法执行发射操作,只有 Ready 状态下才能执行发射操作

    // 添加燃料
    let rocket = rocket.add_fuel();

    // 发射
    rocket.launch();
}
shell> cargo run
神舟火箭 已创建,准备添加燃料...
神舟火箭 添加燃料完毕!
神舟火箭 发射升空,目标星辰大海!

7.5 关联类型

  泛型和关联类型都可以实现对类型的抽象,但二者确定具体类型的时机不同,另外关联类型只能用于 trait。泛型由使用者在使用时指定具体类型,使同一份代码能够适用于多种不同类型,而关联类型由 trait 实现者在实现 trait 时指定具体类型,使用时无需像泛型一样显式指定类型参数。

trait Animal {
    // 定义一个关联类型
    type Food;
    
    // 使用关联类型作为返回值
    fn eat(&self) -> Self::Food;
}

struct Dog;

// 狗和狗的食物
#[derive(Debug)]
struct Bone(String);

impl Animal for Dog {
    
    // 实现者实现时指定具体类型
    type Food = Bone;

    fn eat(&self) -> Self::Food {
        return Bone(String::from("骨头"));
    }
}

// 牛和牛的食物
struct Cow;

#[derive(Debug)]
struct Grass(String);

impl Animal for Cow {
    type Food = Grass;

    fn eat(&self) -> Self::Food {
        return Grass(String::from("青草"));
    }
}

fn main() {
    // 关联类型使用时,无需指定具体类型
    let dog = Dog;
    let cow = Cow;
    
    println!("狗吃:{:?}", dog.eat().0);
    println!("牛吃:{:?}", cow.eat().0);
}
shell> cargo run
狗吃:"骨头"
牛吃:"青草"