泛型
变量是对内存中数据的抽象: 在编写代码阶段可以用变量名代表某个数据,编译阶段再替换为具体的内存地址。
泛型(Generic)是对数据类型的抽象,与变量的含义相同,在编写代码阶段可以用泛型代表各种可能的数据类型,编译阶段再替换为具体的类型。
泛型的核心作用是减少因类型不同而导致的代码冗余。
为什么需要泛型
如果没有泛型,为不同类型定义逻辑完全相同的函数时,需要为每种类型写一份:
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 }
// ...每种数值类型都要写一遍使用泛型后,一个函数可以处理所有满足条件的类型:
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
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 必须同时实现 Trait1 和 Trait2 两个 Trait:
// 约束泛型 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 必须同时实现 Trait1 和 Trait2 两个 Trait:
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:
use std::fmt::Display;
fn print_value<T: Display>(i: &T) {
println!("{}", i);
}泛型结构体
语法: struct StructName<T> { field: T, ... }
结构体中的字段类型也可以用泛型表示:
#[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
}若需要不同类型的字段,使用多个泛型参数:
#[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>:
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}自定义泛型枚举:
enum Message<T, U> {
Quit,
Data(T),
Error(U),
}
let msg: Message<i32, String> = Message::Data(42);泛型结构体实现方法
定义一个泛型结构体 Point<T>, 可以为它的不同类型实现不同的方法:
struct Point<T> {
x: T,
y: T,
}为泛型实现方法
语法: impl<T> StructName<T> { ... }
在 impl 后声明泛型,才能在 Point<T> 的方法中使用 T:
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 是某个具体类型的实例实现方法,其他类型的实例不具备此方法:
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 子句处约束)
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()方法
方法中使用独立泛型
方法可以拥有独立于结构体的泛型参数:
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 时限制。
// ❌ 不推荐: 在 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):
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 版本
}编译后实际上生成了三个函数,每个都针对具体类型。这意味着:
- 运行时零开销: 程序运行时直接调用对应类型的函数,不需要额外计算泛型所代表的具体类型
- 代码膨胀: 编译后的二进制文件会因为泛型被展开为多个函数而变大,但多数情况下这不是问题