如果把 Rust 的类型系统比作一座大厦,那么 泛型(Generics <T>) 和 特性(Traits trait) 就是这座大厦的钢筋与水泥。
用最形象的一句话概括它们俩的配合关系:
-
泛型 <T> 负责"把门敞开" ------ 让一份代码能通吃宇宙间的任意类型,不用重复复制粘贴;
-
特性 Trait 负责"安检把关" ------ 给泛型 T 设下能力门槛,只有考取了对应"资格证(Trait)"的类型才准入内。
下面我们分 5 个核心板块,把泛型与特性的所有高频语法和底层原理一次性讲透:
一、 泛型 <T> 全景:它能写在哪 4 个地方?
之前我们在 fn largest<T> 函数上见过泛型,但其实泛型可以用在 4 个地方:
1. 用在函数上(最常见)
Rust
// <T> 先声明代号,后面参数和返回值都能用 T
fn first_element<T>(list: &[T]) -> &T {
&list[0]
}
2. 用在结构体 struct 上(造出万能容器)
比如你想定义一个坐标点 Point,有时候想存整数坐标 (1, 2),有时候想存小数坐标 (1.5, 2.5):
Rust
// 单个泛型 T:要求 x 和 y 必须是同一种类型
struct Point<T> {
x: T,
y: T,
}
// 也可以定义多个泛型 <T, U>:x 和 y 可以是不同类型!
struct MixedPoint<T, U> {
x: T,
y: U,
}
fn main() {
let p1 = Point { x: 5, y: 10 }; // 自动推导为 Point<i32>
let p2 = Point { x: 1.5, y: 4.2 }; // 自动推导为 Point<f64>
let p3 = MixedPoint { x: 5, y: 4.2 }; // 自动推导为 MixedPoint<i32, f64>
}
3. 用在枚举 enum 上(破案了!Option 和 Result 就是泛型枚举!)
还记得你前面学的两个"薛定谔的快递盒"吗?它们在标准库里的真实源码就是两段极简的泛型枚举:
Rust
// Option<T>:盒子里可以装任意类型 T!
enum Option<T> {
Some(T),
None,
}
// Result<T, E>:成功装 T 类型,失败装 E 类型!
enum Result<T, E> {
Ok(T),
Err(E),
}
4. 用在 impl 方法块上(包含一个超酷的"偏心特权"玩法!)
给泛型结构体 Point<T> 写方法时,有两种写法:
Rust
// 写法 A:给【所有类型 T】的 Point 都加上 get_x 方法
// (⚠️ 注意:impl 后面必须紧跟 <T>,告诉编译器后面的 T 是泛型代号,不是真名)
impl<T> Point<T> {
fn get_x(&self) -> &T {
&self.x
}
}
// 写法 B(独门绝技):只给【小数类型 Point<f64>】单独开小灶加方法!
impl Point<f64> {
// 只有装 f64 小数的坐标点,才能调用算勾股定理距离的方法!Point<i32> 调不了!
fn distance(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
二、 特性(Trait)深度用法:怎么定义与实现"资格证"?
我们在面向对象里讲过,trait 就是 接口(能力资格证)。它规定了一系列方法签名:
Rust
// 定义一张叫 Summary(生成摘要)的资格证
pub trait Summary {
// 1. 没写函数体(带分号):谁考这张证,谁必须自己亲自实现它!
fn summarize_author(&self) -> String;
// 2. 写了默认函数体:考过证的人自动白嫖这个方法(且内部还能调上面的方法!)
fn summarize(&self) -> String {
format!("(阅读更多,作者: {}...)", self.summarize_author())
}
}
三、 泛型 + 特性约束的 3 种写法(看懂所有开源项目的关键!)
当你想写一个函数:"只允许实现了 Summary 特性的类型传进来" ,在 Rust 中有 3 种完全等价的写法(按场景由简到繁):
写法 1:impl Trait 语法糖(最简短,适合简单参数)
不用写 <T>,直接在参数类型的位置写 impl 接口名:
Rust
pub fn notify(item: &impl Summary) {
println!("突发新闻!{}", item.summarize());
}
写法 2:标准泛型约束 <T: Trait>(适合强制要求两个参数是同一种具体类型)
-
如果写 fn notify(a: &impl Summary, b: &impl Summary),a 可以是微博,b 可以是新闻(不同类型)。
-
如果你想强制要求 a 和 b 必须是完全相同的类型,就得用 <T: Summary>:
Rust
// a 和 b 必须都是同一种类型 T!
// 如果要求同时拥有多张资格证,用 + 号连接:
pub fn notify<T: Summary + std::fmt::Display>(a: &T, b: &T) {
println!("{} 和 {}", a.summarize(), b.summarize());
}
写法 3:where 子句(真实工程最爱!拯救长长的函数头)
当一个函数有好几个泛型参数,每个泛型又要求三四张资格证时,全挤在函数名后面会丑得没法看。
Rust 提供了 where 关键字,让你把所有约束条件整齐地挪到小括号后面:
Rust
// ❌ 不用 where:全挤在前面,连参数在哪都找不到了
fn some_function<T: std::fmt::Display + Clone, U: Clone + std::fmt::Debug>(t: &T, u: &U) -> i32 { 0 }
// ✅ 用 where(上面代码的 100% 等价美化版):函数头干干净净,约束在下面一目了然!
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: std::fmt::Display + Clone,
U: Clone + std::fmt::Debug,
{
0
}
四、 返回值也能写 -> impl Trait!
在函数返回值的位置写 -> impl Summary,意思是告诉调用者:
"我给你返回了一个实现了 Summary 接口的东西,你只管调它的 .summarize() 方法就行,不用关心它底层具体是个什么结构体!"
Rust
// 调用者根本不需要知道里面返回的具体是 Tweet 还是 News,只知道它能当 Summary 用!
fn create_summarizable() -> impl Summary {
Tweet {
username: String::from("张三"),
content: String::from("今天学会了 Rust 泛型!"),
}
}
五、 底层原理对比:为什么 Rust 的泛型比 Java 的泛型快得多?
很多学过 Java 的人会担心:"用了泛型 <T>,运行时会不会变慢?"
-
Java 的泛型(假泛型 / 类型擦除 Type Erasure):
-
Java 里的 ArrayList<Integer> 在编译后,泛型会被擦除变成 ArrayList<Object>。
-
普通整数 int 必须被迫打包成堆内存上的 Integer 对象(装箱 Boxing),每次读写都要去堆内存寻址并做运行时类型转换,有明显的性能损耗。
-
-
Rust 的泛型(真泛型 / 单态化 Monomorphization):
-
当你在代码里用到了 Point<i32> 和 Point<f64> 时,Rust 编译器会在编译期间,直接自动在后台生成两个专属结构体:
-
struct Point_i32 { x: i32, y: i32 }
-
struct Point_f64 { x: f64, y: f64 }
-
-
结果 :到了运行时,机器码里根本没有泛型这回事,全是最底层的原生类型直接操作,0 装箱、0 堆内存浪费、0 运行时损耗!
-
总结速查表
|------------------------------------------|-------------------|-------------------------------|
| 语法长相 | 叫什么名字 | 什么时候用 |
| struct Box<T> / enum Option<T> | 泛型数据结构 | 定义能装任意类型的通用容器 |
| fn foo(x: &impl Trait) | impl Trait 参数简写 | 参数比较简单,只要实现了某个接口就行 |
| fn foo<T: TraitA + TraitB>(x: &T) | 特征约束(Trait Bound) | 多个参数必须是同一种类型 T,或有多个约束条件 |
| fn foo<T>(x: &T) where T: Trait | where 约束子句 | 工程最推荐:当约束条件比较长时,让函数签名保持整洁 |
| #derive(Debug, Clone) | 自动派生 Trait | 用宏让编译器自动帮你的结构体考取标准库的常用资格证 |