跳到主要内容

泛型

问题

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 泛型有什么区别?

答案

特性RustJava
实现方式单态化(编译时展开)类型擦除(运行时擦除)
运行时开销有(装箱、类型检查)
运行时类型信息保留擦除(无法 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 Objectdyn 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 要么是拥有所有权的类型(如 StringVec),要么包含的引用都是 '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 或类型标注。

相关链接