5.1 继承与多态:类型扩展与动态绑定


5.1 继承与多态:类型扩展与动态绑定

本节摘要:面向对象在 Fortran 里叫"类型扩展",这个叫法暴露了本质——继承首先是内存布局的延续,其次才是行为复用。本节讲透类型扩展的内存机制、class 与 type 的区别、延迟绑定的接口契约、select type 类型守卫,以及无限多态 class(*) 的用途与代价。最后给出性能取舍的明确策略:核心循环用静态类型,框架边界用多态。

一个流体求解器为什么需要继承

假设你维护一个求解器框架,今天支持有限差分,明天要加有限元,后天可能有人想挂谱方法。三种方法的骨架一样:组装矩阵、施边界条件、线性求解,只是"组装"的具体逻辑不同。F77 的写法是巨大的条件分支:if (method==1) ... else if (method==2) ...,加一种方法就改一遍主逻辑。类型扩展的思路是:定义一个抽象的"求解器"类型,每种方法作为它的扩展实现各自的组装过程,主程序只对着抽象类型编程。加新方法,不动主逻辑。

类型扩展:内存布局的延续

Fortran 的继承用 extends 关键字,且只支持单继承。为什么限制单继承?因为 Fortran 的继承首先是内存前缀兼容:扩展类型的内存布局 = 父类型字段 + 自己的新字段,子类型实例的前缀部分与父类型完全一致。这保证指向子类型的指针可以安全地当父类型用(向上转型),编译器不需要调整指针偏移。多继承会破坏这种线性可预测性,所以被拒绝。

module shape_mod use iso_fortran_env, only: real64 implicit none type, abstract :: shape_t real(real64) :: area = 0.0_real64 contains procedure(calc_area_i), deferred :: calc_area ! 延迟绑定:只给签名 end type shape_t abstract interface subroutine calc_area_i(self) import :: shape_t class(shape_t), intent(inout) :: self end subroutine calc_area_i end interface type, extends(shape_t) :: circle_t real(real64) :: radius = 1.0_real64 contains procedure :: calc_area => circle_area end type circle_t type, extends(shape_t) :: rect_t real(real64) :: w = 1.0_real64, h = 1.0_real64 contains procedure :: calc_area => rect_area end type rect_t contains subroutine circle_area(self) class(circle_t), intent(inout) :: self self%area = 3.141592653589793_real64 * self%radius**2 end subroutine circle_area subroutine rect_area(self) class(rect_t), intent(inout) :: self self%area = self%w * self%h end subroutine rect_area end module shape_mod

type, abstract :: shape_t 是不能实例化的抽象类型,deferred 过程是必须由子类实现的契约。circle_t 与 rect_t 各自实现 calc_area。这就是"接口稳定、实现多变"的骨架。

class 与动态绑定:把"具体"留给运行时

type 声明静态类型,class 声明多态类型。class(shape_t) :: s 可以指向 shape_t 或任何它的扩展。调用 s%calc_area() 时,编译器不知道 s 的实际类型,运行时通过分派表找到正确实现——这就是动态绑定。代价是间接调用,编译器无法内联。

program poly_demo use shape_mod, only: shape_t, circle_t, rect_t use iso_fortran_env, only: real64 implicit none class(shape_t), allocatable :: s allocate(circle_t :: s) select type (s) type is (circle_t) s%radius = 2.0_real64 end select call s%calc_area() ! 动态绑定到 circle 的实现 write(*,*) 'circle area =', s%area deallocate(s) allocate(rect_t :: s) select type (s) type is (rect_t) s%w = 3.0_real64; s%h = 4.0_real64 end select call s%calc_area() ! 动态绑定到 rect 的实现 write(*,*) 'rect area =', s%area end program poly_demo

select type 是类型守卫:把多态变量"窄化"回具体类型后,才能安全访问子类专有字段。注意匹配顺序从最具体到最通用,别让父类分支先吃掉子类。

图:类型扩展的继承体系与内存布局

图:类型扩展的继承体系与内存布局

无限多态 class(*):能装一切的容器

class(*) 可以关联任何类型——内置类型、任意自定义类型、互相没有继承关系的类型。它让通用容器(类似 Python 的列表)成为可能。代价是:编译器不知道实际类型,你没法直接访问它的字段或调用方法,必须配合 select type 一层层分辨。class(*) 的正确位置在框架边界:配置解析、通用日志、序列化这类"先装进去、再按类型处理"的场景。核心数值计算域里滥用它,等于把类型安全扔进垃圾桶。

性能取舍:核心静态,边缘动态

动态绑定阻止内联,热循环里放多态调用是性能灾难。标准策略是"多态分发、静态执行":在循环外把多态对象解析成具体类型(select type 后用一个 type 指针),循环内全部走静态调用。框架的高层逻辑可以用多态换扩展性,核心数值内核保持静态类型。这条取舍线,是 Fortran 面向对象与 C++/Java 最大的不同——它从不鼓励你为抽象牺牲性能。

学习目标

阅读完本节,你应当能够:

  1. 解释类型扩展为何是"内存前缀兼容",以及它对向上转型的意义
  2. 区分 type 与 class 声明,说明多态变量何时指向派生类型
  3. 用抽象类型 + 延迟绑定定义接口契约
  4. 用 select type 安全处理多态变量的实际类型
  5. 说出 class(*) 的适用场景与代价

本节要点回顾

  • 继承是内存前缀兼容:单继承保证布局线性可预测,向上转型零成本
  • 抽象类型 + 延迟绑定 = 接口契约:子类不实现就编不过,契约强制到位
  • class 是多态声明:动态绑定运行时分派,编译期内联被阻止
  • select type 是类型守卫:把多态窄化回具体类型,匹配顺序从具体到通用
  • class(*) 只放框架边界:通用容器的代价是丢类型安全
  • 核心静态、边缘动态:热循环拒绝多态,这是 Fortran OOP 的底线取舍

机制清楚了,看它们怎么组合成框架——设计模式落地。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U