以spi_device继承spi_controller分析继承和关联的使用区别

区分 “继承” 和 “从属关联”

要清晰区分 “继承” 和 “从属关联”,需从 核心定义、内存结构、逻辑关系、代码表现 四个维度切入 —— 这两个概念的本质差异,是 “是否存在层级归属与属性复用”,而非简单的 “引用关系”。

一、核心定义:本质差异

概念 核心逻辑 通俗类比
继承(Inheritance) 子类(派生类)“is a”(是一种)父类(基类),继承父类的所有属性和行为,同时可扩展自身特性。 核心是 “层级归属 + 属性复用”。 猫(子类)是一种动物(父类),继承 “呼吸、进食” 等动物的通用行为,同时有 “抓老鼠” 的特有行为。
从属关联(Dependency/Association) A 对象 “depends on”(依赖 / 从属)B 对象,A 需借助 B 的能力完成工作,但 A 不继承 B 的属性或行为,两者是独立的个体。 核心是 “功能依赖 + 独立存在”。 学生(A)从属班级(B):学生需要班级提供的 “上课、考勤” 功能,但学生不继承班级的 “班级编号、班主任” 等属性,学生和班级是独立的实体。

二、四个关键区分维度

1. 内存结构:“包含” vs “引用”

继承和从属关联在代码的内存布局上有本质不同,这是最硬核的区分依据:

(1)继承:子类内存包含父类成员

在 C 语言(内核常用)中,继承通过 “结构体嵌套” 实现 ——父类结构体作为子类结构体的第一个成员,子类内存中会完整包含父类的所有成员,可直接访问父类属性。
示例(内核设备模型的继承):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 父类:通用设备基类
struct device {
const char *name; // 通用属性:设备名
struct device *parent; // 通用属性:父设备
// 其他通用成员...
};

// 子类:SPI从设备(继承 struct device)
struct spi_device {
struct device dev; // 第一个成员 = 父类,内存上包含父类所有成员
u32 max_speed_hz; // 子类特有属性:最大速率
struct spi_controller *controller; // 关联控制器(非继承)
// 其他子类特有成员...
};
  • 内存布局:spi_device 的内存起始地址 = 其内部 dev 的地址,可通过 spi_device 指针直接访问 dev 的成员(如 spi_dev->dev.name)。
  • 内核宏 to_spi_device(dev) 本质:将 struct device * 指针强制转换为 struct spi_device *,依赖 “父类是子类第一个成员” 的内存布局 —— 这是继承的典型特征。

(2)从属关联:仅通过指针 “引用”,不包含对方成员

从属关联中,A 对象仅持有 B 对象的指针 / 句柄,内存上不包含 B 的任何成员,无法直接访问 B 的属性(需通过指针间接访问)。
示例(spi_device 与 spi_controller 的关联):

1
2
3
4
5
6
7
8
9
10
struct spi_device {
struct device dev; // 继承的父类
// 仅持有控制器的指针,不包含 spi_controller 的任何成员
struct spi_controller *controller;
};

struct spi_controller {
struct device dev; // 自身继承的父类
u16 num_chipselect; // 控制器特有属性:片选数量
};
  • 内存布局:spi_devicecontroller 指针仅存储 spi_controller 的内存地址,spi_device 内存中没有 num_chipselect 等控制器成员。
  • 访问控制器属性:必须通过指针间接访问(如 spi_dev->controller->num_chipselect),而非直接访问 —— 这是关联的典型特征。

2. 逻辑关系:“是一种” vs “依赖一种”

(1)继承:“是一种”(is a)

子类是父类的 “特殊化”,具备父类的所有本质特征,同时增加特有特征。

  • 例 1:spi_device 是一种 struct device(符合设备模型的通用设备),spi_controller 也是一种 struct device
  • 例 2:“哈士奇” 是一种 “狗”,“狗” 是一种 “动物”—— 层级归属明确。

(2)从属关联:“依赖一种”(depends on)

A 对象需要 B 对象的能力才能工作,但 A 不是 B 的 “特殊化”,两者是独立的逻辑实体。

  • 例 1:spi_device 依赖 spi_controller 的 “传输数据” 能力,但 spi_device 不是 “一种”spi_controller(从设备是被管理者,控制器是管理者,角色完全不同)。
  • 例 2:“手机” 依赖 “基站” 的 “信号传输” 能力,但手机不是 “一种” 基站。

3. 属性与行为:“复用” vs “隔离”

(1)继承:子类复用父类的属性和行为

父类的属性(如 struct devicename)和行为(如内核提供的 device_add 注册函数),子类可直接复用,无需重复定义。

  • 例:spi_devicespi_controller 都无需自己实现 “设备注册” 逻辑,直接复用 device_add 函数(通过 dev 成员)。

(2)从属关联:属性和行为完全隔离

A 对象和 B 对象的属性、行为互不复用,各自有独立的定义。

  • 例:spi_devicemax_speed_hz(从设备速率)与 spi_controllermax_speed_hz(控制器最大速率)是完全独立的属性,前者受后者约束,但不复用后者的值。
  • 例:spi_controllertransfer_one(传输函数)是控制器的行为,spi_device 无法复用,只能通过 controller->transfer_one 调用该行为。

4. 生命周期:“绑定” vs “独立”

(1)继承:子类与父类生命周期绑定

子类的生命周期与父类完全一致 —— 创建子类时,父类成员会同时初始化;销毁子类时,父类成员也会同时释放。

  • 例:创建 spi_device 时,其内部的 struct device dev 会被一起初始化;销毁 spi_device 时,dev 也会被 device_del 清理。

(2)从属关联:生命周期独立

A 对象和 B 对象的生命周期可独立存在,互不影响。

  • 例 1:spi_controller 先注册(生命周期开始),之后 spi_device 再注册(依赖已存在的控制器);spi_device 可先注销(生命周期结束),控制器仍可继续管理其他从设备。
  • 例 2:“学生” 毕业(生命周期结束),“班级” 仍可存在(继续招收新学生)。

三、总结:一句话区分

  • 继承:子类是父类的 “一种”,内存包含父类成员,复用父类属性和行为,生命周期绑定(如 spi_devicestruct device)。
  • 从属关联:A 依赖 B 的能力,内存仅持有 B 的指针,属性行为完全隔离,生命周期独立(如 spi_devicespi_controller)。

回到你最初的疑问:spi_device 没有继承 spi_controller,而是通过指针 “从属关联” 到 spi_controller;两者真正的继承关系,是都继承自内核的 struct device(通用设备基类)。

在实际编程中,如何判断应该使用继承还是从属关联?

在实际编程(尤其是驱动开发、框架设计等场景)中,判断使用继承还是从属关联,核心是围绕 “逻辑关系”“复用需求”“生命周期”“耦合度” 四个维度做决策,可按以下步骤逐步判断,结合具体场景落地。

一、第一步:先判断核心逻辑关系 ——“is a” 还是 “has a/depends on”

这是最根本的判断标准,直接决定两种设计的适用场景:

1. 若满足 “is a”(A 是 B 的一种)→ 优先用继承

当 A 实体本质上是 B 实体的特殊化版本,具备 B 的所有核心特征,且需要在此基础上扩展特有属性 / 行为时,用继承。
判断问题:能否用 “XX 是一种 XX” 描述两者关系?能则选继承。

典型场景示例:

  • 内核设备模型spi_device 是一种 struct device(符合通用设备的所有特征:有名称、能匹配驱动、支持电源管理),因此 spi_device 继承 struct device
  • 面向对象编程Dog 是一种 Animal(有呼吸、进食等通用行为,扩展 “汪汪叫” 特有行为),因此 Dog 继承 Animal
  • 驱动开发i2c_client(I2C 从设备)是一种 struct device,因此继承 struct device

2. 若满足 “has a”(A 包含 / 依赖 B 的能力)→ 优先用从属关联

当 A 实体需要 借助 B 实体的能力完成工作,但 A 不是 B 的 “特殊化版本”,两者是独立个体时,用从属关联。
判断问题:能否用 “XX 需要 XX 的帮助才能工作” 或 “XX 包含 XX 的引用” 描述?能则选关联。

典型场景示例:

  • SPI 子系统spi_device 需要 spi_controller 的 “数据传输能力” 才能收发数据,但 spi_device 不是 “一种”spi_controller(角色是 “被管理者” vs “管理者”),因此用指针关联(spi_controller *controller)。
  • 硬件驱动:“LED 设备” 需要 “GPIO 控制器” 的 “引脚控制能力” 才能亮灭,但 LED 不是 “一种” GPIO 控制器,因此用 struct gpio_desc *gpio 关联 GPIO 引脚。
  • 应用开发:“播放器” 需要 “音频解码器” 的 “解码能力” 才能播放音乐,但播放器不是 “一种” 解码器,因此用指针关联解码器实例。

二、第二步:判断属性 / 行为的复用需求

若第一步的逻辑关系不明确,可通过 “是否需要复用属性 / 行为” 进一步判断:

1. 需要复用属性 / 行为 → 用继承

继承的核心价值是避免重复代码:父类的属性(如 struct devicenameparent)和行为(如内核提供的 device_add 注册函数),子类可直接复用,无需重新定义。
反例:若 spi_device 不继承 struct device,则需要自己实现设备命名、驱动匹配、电源管理等逻辑,导致代码冗余(I2C、USB 等其他总线设备也需重复实现)。

2. 仅需调用功能,无需复用属性 → 用从属关联

从属关联中,A 仅需调用 B 的特定接口(如 spi_controller->transfer_one),无需复用 B 的属性(如 num_chipselect),此时用指针关联即可,避免不必要的属性暴露。
反例:若 spi_device 继承 spi_controller,则会被迫包含 num_chipselectdma_tx 等与从设备无关的属性,造成内存浪费和逻辑混乱。

三、第三步:判断生命周期是否绑定

两种设计的生命周期关系不同,直接影响资源管理的安全性:

1. 生命周期必须绑定(同生共死)→ 用继承

继承中,子类的生命周期与父类完全绑定:创建子类时,父类成员会同步初始化;销毁子类时,父类成员也会同步释放,无需单独管理父类资源。
示例:创建 spi_device 时,其内部的 struct device dev 会通过 device_initialize 初始化;销毁 spi_device 时,dev 会通过 device_del 自动清理,无需单独处理 dev 的生命周期。

2. 生命周期独立(可分别创建 / 销毁)→ 用从属关联

从属关联中,A 和 B 的生命周期相互独立:B 可先于 A 创建(如 spi_controller 先注册,spi_device 后注册),A 可先于 B 销毁(如 spi_device 注销后,spi_controller 仍管理其他设备)。
示例spi_controller 注册后,可动态添加 / 删除多个 spi_device;即使所有 spi_device 都注销,spi_controller 仍可保留(除非主动卸载控制器驱动)。

四、第四步:考虑耦合度需求(松耦合优先)

继承会导致强耦合(子类依赖父类的实现细节),关联则是松耦合(仅依赖接口,不依赖实现),实际编程中应优先选择松耦合设计,除非继承的收益(复用、逻辑清晰)远大于耦合成本。

1. 需降低耦合(便于扩展 / 维护)→ 优先关联

若 B 的实现可能变化(如更换不同厂商的 SPI 控制器),A 只需适配 B 的接口(如 transfer_one 函数),无需修改 A 的代码,此时关联更合适。
示例:更换 SPI 控制器驱动时,spi_device 无需修改(只需确保新控制器实现 transfer_one 接口),因为 spi_device 仅通过指针调用接口,不依赖控制器的内部实现。

2. 需统一规范(不允许灵活修改)→ 用继承

若需要强制所有子类遵循统一的接口或属性(如内核设备模型要求所有设备必须有 struct device 成员),则用继承,确保规范不被打破。
示例:内核所有硬件设备(SPI、I2C、PCI 等)都必须继承 struct device,以保证电源管理、sysfs 接口等功能的统一性,不允许通过关联绕过这一规范。

五、总结:决策流程图(实际编程可按此步骤)

​ A[“判断两个实体关系”] –> B{是否满足“is a”?}

1
2
3
4
5
6
7
8
graph TD
A[判断两个实体关系] --> B{是否满足is a关系}
B -->|是| C[用继承,复用属性/行为,生命周期绑定]
B -->|否| D{是否仅需调用对方功能,无需复用属性?}
D -->|是| E[用从属关联,指针引用,松耦合,生命周期独立]
D -->|否| F[重新审视逻辑关系是否误判is a?]
F -->|确认不是is a| E
F -->|确认是is a| C

关键结论

  • 继承:适用于 “XX 是一种 XX”、需复用属性 / 行为、生命周期绑定、需统一规范的场景(如内核设备模型、类的特殊化)。
  • 从属关联:适用于 “XX 依赖 XX 的能力”、仅需调用接口、生命周期独立、需降低耦合的场景(如设备与控制器的关系、模块间的功能调用)。

回到之前的 SPI 例子:

  • spi_device 继承 struct device:因为 “spi_device 是一种设备”,需复用设备命名、电源管理等功能,生命周期绑定。
  • spi_device 关联 spi_controller:因为 “spi_device 依赖控制器的传输能力”,仅需调用接口,生命周期独立,需降低耦合。

优点 清晰的逻辑层级,降低理解成本

继承直接体现 “is a” 的语义关系,子类与基类的层级明确,代码阅读者可快速理解实体间的逻辑归属,无需额外文档说明。

  • 示例:看到 struct spi_device 嵌套 struct device,可立刻判断 “SPI 从设备是一种内核通用设备”;看到 struct spi_controller 也嵌套 struct device,可判断 “SPI 控制器也是一种通用设备”—— 这种层级关系比通过指针关联更直观。
  • 收益:提升代码可读性,降低团队协作中的认知成本。

优点 统一接口与框架化管理

基类可定义 通用操作规范,所有子类遵循同一接口,便于框架统一管理不同类型的子类,实现 “多态” 效果(C 中通过函数指针和基类接口间接实现)。

  • 示例:内核设备模型通过 struct device 定义了统一的设备生命周期接口(proberemovesuspendresume)。无论子类是 spi_device 还是 i2c_client,框架都能通过 struct device * 指针调用通用接口(如 dev_pm_domain_attach(dev)),无需区分设备类型 —— 这是内核能统一管理所有硬件设备的关键。
  • 收益:简化框架设计,支持 “新增子类无需修改框架”(如新增一种总线设备,只需嵌套 struct device 即可融入现有模型)。