泛型
问题
Rust 的泛型是如何实现零成本抽象的?单态化有什么优缺点?
答案
基础语法
// 泛型函数
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in &list[1..] {
if item > largest {
largest = item;
}
}
largest
}
// 泛型结构体
struct Point<T> {
x: T,
y: T,
}
// 不同类型的泛型
struct Pair<T, U> {
first: T,
second: U,
}
// 泛型 impl
impl<T> Point<T> {
fn x(&self) -> &T { &self.x }
}
// 针对特定类型的 impl
impl Point<f64> {
fn distance(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
Trait Bound 约束
// 基础约束
fn print_it<T: Display>(item: T) {
println!("{}", item);
}
// 多重约束
fn compare_and_print<T: Display + PartialOrd>(a: T, b: T) {
if a > b { println!("{}", a); }
}
// where 子句(推荐,更清晰)
fn complex_fn<T, U>(t: T, u: U) -> String
where
T: Display + Clone,
U: Debug + Into<String>,
{
format!("{} {:?}", t, u)
}
// impl Trait 语法糖
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
单态化(Monomorphization)
编译器为每个具体类型生成专用代码:
fn identity<T>(x: T) -> T { x }
// 调用
let a = identity(42_i32);
let b = identity("hello");
// 编译器生成(伪代码):
// fn identity_i32(x: i32) -> i32 { x }
// fn identity_str(x: &str) -> &str { x }
单态化的优点
- 运行时零开销,等价于手写特定类型的代码
- 编译器可以内联优化
- 类型信息完整保留
单态化的缺点
- 编译时间增长(每个类型组合生成一份代码)
- 二进制体积增大(代码膨胀)
- 不支持异构集合(需要 Trait Object)
常量泛型(Const Generics)
Rust 1.51+ 支持以常量作为泛型参数:
// 固定大小数组的泛型
struct Matrix<T, const ROWS: usize, const COLS: usize> {
data: [[T; COLS]; ROWS],
}
impl<T: Default + Copy, const ROWS: usize, const COLS: usize> Matrix<T, ROWS, COLS> {
fn new() -> Self {
Matrix {
data: [[T::default(); COLS]; ROWS],
}
}
}
let m: Matrix<f64, 3, 4> = Matrix::new(); // 3x4 矩阵
常见面试问题
Q1: Rust 泛型和 Java 泛型有什么区别?
答案:
| 特性 | Rust | Java |
|---|---|---|
| 实现方式 | 单态化(编译时展开) | 类型擦除(运行时擦除) |
| 运行时开销 | 零 | 有(装箱、类型检查) |
| 运行时类型信息 | 保留 | 擦除(无法 instanceof T) |
| 代码体积 | 膨胀(每个类型一份) | 共享一份 |
| 基本类型支持 | 支持 | 不支持(需要装箱) |
Q2: impl Trait 和泛型参数有什么区别?
答案:
// 泛型参数:调用者决定类型
fn process<T: Display>(item: T) { }
// impl Trait 参数位置:等价于泛型,但更简洁
fn process(item: impl Display) { }
// impl Trait 返回位置:编译器推断唯一具体类型
fn make_iter() -> impl Iterator<Item = i32> {
(0..10).filter(|x| x % 2 == 0)
}
关键区别:泛型参数调用者可以用 turbofish 指定类型(process::<String>(s)),impl Trait 不行。返回位置的 impl Trait 只能返回一种具体类型。
Q3: 什么时候用泛型,什么时候用 Trait Object?
答案:
- 泛型:编译时确定类型、追求性能、类型数量有限 → 静态分发
- Trait Object(
dyn Trait):运行时确定类型、需要异构集合、类型数量不确定 → 动态分发
// 静态分发:编译时确定,零开销
fn draw_static(shape: &impl Shape) { shape.draw(); }
// 动态分发:运行时确定,有虚表开销
fn draw_dynamic(shape: &dyn Shape) { shape.draw(); }
// 异构集合必须用 Trait Object
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle), Box::new(Rect)];
Q4: T: 'static 是什么意思?
答案:
T: 'static 不是说 T 必须是静态的或者永远存活。它表示 T 不包含任何非 'static 的引用——即 T 要么是拥有所有权的类型(如 String、Vec),要么包含的引用都是 'static 的。
fn spawn_thread<T: Send + 'static>(val: T) {
std::thread::spawn(move || {
// val 必须满足 'static,因为线程可能比创建者活得更久
println!("{:?}", val);
});
}
spawn_thread(String::from("ok")); // ✅ String: 'static
// spawn_thread(&local_var); // ❌ 临时引用不满足 'static
Q5: 什么是 turbofish 语法?
答案:
Turbofish(::<>)用于在调用泛型函数时显式指定类型参数:
let x = "42".parse::<i32>().unwrap(); // turbofish
let numbers = vec![1, 2, 3].into_iter().collect::<Vec<_>>();
// 等价于类型标注
let x: i32 = "42".parse().unwrap();
当编译器无法推断类型时必须使用 turbofish 或类型标注。