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
狗吃:"骨头"
牛吃:"青草"