第 12 章:以为 Rust 没有接口?以为没有泛型?

错了!Rust 有的是 Traits 和 Generics,比 Java 的接口和泛型强大一千倍。

Java 的接口系统?20 年前的设计。Rust 的 Trait 系统?21 世纪的现代抽象。

准备好被彻底震撼吧。

Java 接口:功能有限的古董

Java 接口的局限

// Java 接口:只能定义方法签名
interface Drawable {
    void draw();
    // 不能定义字段
    // 不能定义静态方法(Java 8之前)
    // 不能为现有类型实现接口
}

class Circle implements Drawable {
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

问题一览:

  • 只能在定义类时实现接口
  • 不能为第三方类型添加接口
  • 接口污染(所有实现类都要声明 implements
  • 多重继承的复杂性

Rust Traits:接口的完美进化

基本定义和实现

// Trait 定义:可以包含方法签名和默认实现
trait Drawable {
    fn draw(&self);

    // 默认实现!Java 8 才有的功能
    fn area(&self) -> f64 {
        0.0  // 默认返回0
    }
}

struct Circle {
    radius: f64,
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle with radius {}", self.radius);
    }

    fn area(&self) -> f64 {
        3.14 * self.radius * self.radius
    }
}

比 Java 接口强在哪里?

  1. 默认实现 - 不用在每个实现类里写重复代码
  2. 后期实现 - 可以为任何类型实现 trait,包括第三方类型
  3. 关联类型 - 比 Java 的泛型更强大
  4. 运算符重载 - Java 做不到的事

为第三方类型实现 Trait

// 为标准库的 Vec 实现自定义 trait!
trait Summarize {
    fn summarize(&self) -> String;
}

impl Summarize for Vec<String> {
    fn summarize(&self) -> String {
        format!("Vector with {} elements", self.len())
    }
}

fn main() {
    let vec = vec!["hello".to_string(), "world".to_string()];
    println!("{}", vec.summarize());  // Vector with 2 elements
}

在 Java 里想为 ArrayList 添加方法?做梦去吧!

泛型:类型参数化的艺术

Java 泛型 vs Rust 泛型

// Java 泛型:运行时擦除
List<String> list = new ArrayList<String>();
// 编译后变成 List list = new ArrayList();
// Rust 泛型:编译期单态化,零运行时成本
struct Container<T> {
    value: T,
}

impl<T> Container<T> {
    fn new(value: T) -> Self {
        Container { value }
    }

    fn get(&self) -> &T {
        &self.value
    }
}

Rust 的泛型在编译期就确定了具体类型,没有运行时开销。

Trait Bounds:约束的威力

// Java 的有界泛型:语法丑陋
// public <T extends Comparable<T>> T max(T a, T b) { ... }

// Rust 的 trait bounds:清晰明了
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 largest<T>(list: &[T]) -> T
where
    T: PartialOrd + Copy
{
    // 实现同上
}

Trait bounds 告诉编译器:T 必须实现 PartialOrd(可比较)和 Copy(可复制)trait。

标准库的 Trait:系统的基石

Debug:打印调试的艺术

#[derive(Debug)]  // 自动实现 Debug trait
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 1, y: 2 };
    println!("{:?}", p);  // Point { x: 1, y: 2 }
    println!("{:#?}", p); // 美化打印
}

Java 的 toString() 需要手动实现,Rust 的 Debug 一行搞定。

Clone 和 Copy:复制的哲学

#[derive(Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1;  // Copy:栈上按位复制
    let p3 = p1.clone();  // Clone:显式克隆

    // p1 依然有效,因为实现了 Copy
    println!("{:?} {:?} {:?}", p1, p2, p3);
}

Copy vs Clone 的区别:

  • Copy:编译器自动调用,栈上按位复制
  • Clone:显式调用,可能涉及堆分配

PartialEq 和 Eq:比较的层次

#[derive(Debug, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}

fn main() {
    let p1 = Point { x: 1.0, y: 2.0 };
    let p2 = Point { x: 1.0, y: 2.0 };

    assert_eq!(p1, p2);  // 相等比较
}

为什么是 PartialEq 而不是 Eq? 因为浮点数的 NaN 不等于任何值,包括它自己。数学的严谨性体现在类型系统中。

运算符重载:Java 做不到的事

use std::ops::Add;

#[derive(Debug, Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 3, y: 4 };
    let p3 = p1 + p2;  // 运算符重载!

    println!("{:?}", p3);  // Point { x: 4, y: 6 }
}

Java 程序员看了想哭:为什么我们不能重载运算符?

关联类型:比泛型更强大

trait Iterator {
    type Item;  // 关联类型

    fn next(&mut self) -> Option<Self::Item>;
}

// 实现
impl Iterator for Counter {
    type Item = u32;  // 指定关联类型

    fn next(&mut self) -> Option<Self::Item> {
        // 实现
    }
}

关联类型 vs 泛型参数:

  • 泛型参数:调用者决定类型
  • 关联类型:实现者决定类型

这种设计比 Java 的泛型更灵活、更精确。

实战:构建一个完整的 trait 系统

// 定义一个图形系统
trait Shape {
    fn area(&self) -> f64;
    fn perimeter(&self) -> f64;

    // 默认实现
    fn description(&self) -> String {
        format!("A shape with area {} and perimeter {}",
                self.area(), self.perimeter())
    }
}

trait Drawable {
    fn draw(&self);
}

// 组合多个 traits
trait GraphicsObject: Shape + Drawable {
    fn render(&self) {
        println!("Rendering: {}", self.description());
        self.draw();
    }
}

#[derive(Debug)]
struct Circle {
    radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        3.14159 * self.radius * self.radius
    }

    fn perimeter(&self) -> f64 {
        2.0 * 3.14159 * self.radius
    }
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle with radius {}", self.radius);
    }
}

impl GraphicsObject for Circle {}  // 自动获得 render 方法

fn main() {
    let circle = Circle { radius: 5.0 };
    circle.render();
}

这种设计比 Java 的接口继承更清晰、更模块化。

性能:零成本抽象的威力

Java 接口的运行时成本

interface Drawable {
    void draw();
}

// 运行时多态:虚函数调用
Drawable shape = new Circle();
shape.draw();  // 虚函数表查找有性能开销

Rust trait 的编译期优化

fn render_shape<T: Drawable>(shape: &T) {
    shape.draw();  // 编译期确定具体方法,无虚函数开销
}

// 编译器生成的代码等价于:
fn render_circle(shape: &Circle) {
    shape.draw();  // 直接调用,无间接开销
}

这就是零成本抽象:抽象不带来运行时开销。

写在最后:抽象的进化

Java 接口的哲学:运行时多态,灵活但有开销。

Rust Trait 的哲学:编译期多态,既灵活又高效。

Rust 的 trait 系统给你的不只是接口的功能,还有:

  • 后期实现 - 为任何类型添加行为
  • 零成本抽象 - 编译期优化,无运行时开销
  • 运算符重载 - Java 做不到的表达力
  • 关联类型 - 比泛型更精确的类型关系
  • 默认实现 - 减少重复代码

当你掌握了 Rust 的 trait 系统,你会发现:

  • 代码更模块化了(组合优于继承)
  • 抽象更精确了(类型系统表达设计意图)
  • 性能更好了(编译期优化)
  • 扩展更容易了(后期实现 trait)

这就是现代编程语言的抽象系统应该有的样子。

下一章我们要学习 Rust 的并发编程。准备好体验真正的无畏并发了吗?

因为在 Rust 的世界里,数据竞争不是运行时的问题,而是编译期就被消灭的问题。