← 返回信息流
AI 资讯Hacker News·2 天前

Rust无继承机制下的九种实现方式

原标题:Nine Ways to Do Inheritance in Rust, a Language Without Inheritance

速览

Rust语言原生不支持传统的面向对象继承,但提供了多种替代方案来实现类似功能。本文系统梳理了九种在Rust中实现继承逻辑的技术路径,包括组合、特征对象、泛型及宏等。这些方法帮助开发者在保持Rust内存安全和零成本抽象优势的同时,灵活构建复杂的代码结构。

AI 深度解读

Rust 中实现“继承”的九种方式:没有类继承的语言如何构建多态与复用

背景

Rust 是一门以内存安全和零成本抽象著称的系统级编程语言。与 Java、C++ 或 C# 等面向对象语言不同,Rust 在语言层面不支持类继承(Class Inheritance)。这意味着 Rust 中没有类(Class)、没有子类声明(Subclass declarations),也没有从父类继承字段(Fields)的机制。

然而,开发者在面向对象语言中使用继承,通常是为了实现以下三个核心目的之一:

  1. 共享接口(Shared Interfaces):相同的泛型代码可以处理不同的具体类型。
  2. 共享行为(Shared Behavior):多种类型可以复用同一套实现逻辑。
  3. 共享存储(Shared Storage):子类型继承父类型的字段(数据成员)。

Rust 并不直接提供第三种机制(即不直接支持字段继承),但它提供了一套丰富且强大的工具来解决前两种需求。本文基于 CarlKCarlK 在 Seattle Rust User Group 的演讲及 GitHub 项目 inherit,深入探讨了在 Rust 中解决“类继承形态问题”的九种技巧。

注:在本文中,“抽象类”、“接口”和“Trait(特征)”常被混用,指代一种角色或契约。在 Rust 中,机制通常是 Trait,即具体类型可以实现的行为集合。相比之下,“具体类”对应 Rust 中的 structenum,即可以实际创建值的类型。

核心内容

文章通过九个具体的编程谜题(Puzzles),展示了如何从面向对象的设计思维过渡到 Rust 的 Trait 驱动设计。以下是前三个典型案例的深度解析:

1. 为所有整数类型提供共享辅助方法

场景RangeSetBlaze crate 用于存储整数集合(如 u8, i16 等)。该 crate 依赖每个整数类型定义的 min_valuemax_value 方法。在此基础上,我们希望提供额外的共享方法,例如 exhausted_range(返回从最小值到最大值的完整范围)。

面向对象思路: 定义一个抽象基类 Integer,包含必需的方法 min_valuemax_value,以及一个已实现的方法 exhausted_range,由具体整数类型继承。

Rust 解决方案:Trait 默认方法(Trait Default Methods) 在 Rust 中,我们可以定义一个 Trait,包含两个必需方法(无默认实现)和一个带有默认实现的方法:

use std::ops::RangeInclusive;

trait Integer: Copy + Ord {
    fn min_value() -> Self;
    fn max_value() -> Self;
    // 默认实现,共享行为
    fn exhausted_range() -> RangeInclusive<Self> {
        debug_assert!(Self::min_value() < Self::max_value(), "Precondition");
        Self::max_value()..=Self::min_value()
    }
}

具体类型只需实现必需的方法:

impl Integer for u8 {
    fn min_value() -> Self { u8::MIN }
    fn max_value() -> Self { u8::MAX }
}

调用时,可以直接通过类型名调用默认方法:u8::exhausted_range()。这种技术允许在 Trait 中定义共享逻辑,实现者若未重写该方法,则自动复用默认实现。

2. 让动画舵机仍然被视为舵机

场景: 在 device-envoy crate 中,我们需要控制舵机(Servo)。

  • Servo 是一个抽象概念,具有 set_degrees 方法。
  • ServoPlayerServo 的子类,它不仅具备舵机的功能,还能执行一系列角度和时间的动画序列(animate 方法)。

面向对象思路ServoPlayer 类继承自 Servo 类。

Rust 解决方案:超类 Trait(Supertraits) 在 Rust 中,一个 Trait 可以要求另一个 Trait 作为其“超类”(Supertrait)。这意味着实现 ServoPlayer 的类型必须首先实现 Servo

trait Servo {
    fn set_degrees(&self, degrees: u16);
}

// ServoPlayer 要求实现者必须也是 Servo
trait ServoPlayer: Servo {
    fn animate(&self, steps: &[(u16, u64)]);
}

关键区别: Rust 的 ServoPlayer: Servo 表示“ServoPlayer 需要 Servo”,而不是“ServoPlayer 继承 Servo 的实现”。因此,ServoPlayerEsp 必须分别编写 impl Servoimpl ServoPlayer 两个代码块。

这种机制允许泛型代码精确地请求所需的能力:

  • center_servo(servo: &impl Servo) 可以接受任何实现 Servo 的类型。
  • run_wave(player: &impl ServoPlayer) 可以接受任何实现 ServoPlayer 的类型(因为 ServoPlayer 隐含了 Servo 的能力)。

这构建了一个接口的层级结构,而非类的层级结构。

3. 为不拥有的类型添加方法

场景: 我们希望为 Rust 标准库中的 usize 类型添加一个 is_odd() 方法。

面向对象思路: 让 usize 继承自一个新的抽象类。

Rust 解决方案:扩展 Trait(Extension Trait) Rust 禁止对原始类型(Primitive Types)定义“固有实现”(Inherent Implementation,即直接 impl usize { ... })。编译器会报错 E0390

但是,Rust 允许我们定义自己的 Trait 并为 usize 实现它:

trait UsizeExtensions {
    fn is_odd(self) -> bool;
}

impl UsizeExtensions for usize {
    fn is_odd(self) -> bool {
        self & 1 != 0
    }
}

一旦引入了该 Trait(use),就可以像调用原生方法一样调用 count.is_odd()注:这里区分了“继承”(Inherit,传递行为)和“固有”(Inherent,直接属于类型)。Rust 不支持前者,但支持通过 Trait 模拟的效果。

后续谜题预告

文章还涵盖了其余六个场景,虽然未在摘要中展开代码,但核心逻辑依然围绕以下模式: 4. 给小型 Enum 赋予完整的标准行为:通过为 Enum 变体实现 Trait,模拟类的多态。 5. 让包装器感觉像其内部对象:利用 DerefDerefMut Trait 实现自动解引用,使包装类型透明地暴露内部类型的方法。 6. 为任何范围集集合添加 union():利用泛型约束和 Iterator 适配器,在不修改集合类型的情况下扩展功能。 7. 同等对待十五种整数类型:通过定义通用的数值 Trait 层级,统一处理不同位宽的整数。 8. 仅给 OutputArray<8> 提供面向字节的方法:利用关联类型(Associated Types)和泛型约束,针对特定实例化类型提供特化实现。 9. 仅将可序列化的值保存到 Flash:结合 serde 等库,通过 Trait 约束确保只有满足特定条件的类型才能被持久化。

关键要点

  • Rust 没有类继承,但有 Trait:Rust 摒弃了基于类的继承体系,转而使用基于 Trait 的行为多态。Trait 定义了接口契约,具体类型通过 impl 块实现这些契约。
  • 共享行为通过默认方法实现:Trait 可以包含带有默认实现的方法。这允许定义者提供通用的行为逻辑,实现者可以选择覆盖或复用。
  • 接口层级通过超类 Trait 实现:使用 TraitA: TraitB 语法,可以建立 Trait 之间的依赖关系。实现 TraitA
查看原文 →medium.com