4.1 模块体系与接口隔离


4.1 模块体系与接口隔离

本节摘要:模块是 Fortran 的命名空间与封装单元,是"全局共享"的文明替代品。本节讲清 use 的三种形态(裸用、only、重命名)、默认私有策略如何把可触碰状态收窄到接口,以及子模块如何把接口与实现物理分离、换来可观的增量编译收益。读完你能设计出一棵边界清晰的模块依赖树。

没有模块的年代,全局状态是一地鸡毛

想象一栋楼里所有人的私人物品都堆在广场上,任何人随手取用。F77 的 COMMON 块就是这个广场:一段命名内存区,任何程序单元都能读写。规模小的时候无所谓,代码长到十万行,你改了广场中央的 X,另一个子程序读了 X 却不知情——查 bug 的绝望感,老 Fortran 工程师都懂。模块的出现,本质是给这座广场修了围墙和门禁。

use 的三种形态:裸用、only、重命名

模块通过 use 被引入。裸 use module_name 把全部公有符号导入,省事但污染命名空间;use, only: 只导入需要的符号,代码即文档——读者一眼看清这个单元依赖了外部的哪些具体能力;重命名则解决两个模块提供同名符号的冲突。

module constants_mod use iso_fortran_env, only: real64 implicit none real(real64), parameter :: pi = 3.141592653589793_real64 real(real64), parameter :: euler = 2.718281828459045_real64 end module constants_mod module solver_mod implicit none contains subroutine solve() write(*,*) 'solving...' end subroutine solve end module solver_mod program use_forms use constants_mod, only: pi ! 只导入 pi use solver_mod, solve_it => solve ! 重命名,避免与别的 solve 冲突 implicit none write(*,*) 'pi =', pi call solve_it() end program use_forms

经验规则:工程代码一律用 use, only,能重名的地方就用重命名。这不是洁癖——符号冲突的概率随模块数量平方增长,显式导入是唯一的低成本解法。

默认私有:把"能碰的"收窄到接口

模块默认把所有实体设为 public,但这通常是错误的默认。工程惯例是反向设置:模块开头写 private,然后显式把要暴露的接口标为 public。效果是"默认拒绝、白名单放行"——模块内部状态(临时数组、状态标志、内部常量)对外不可见,外部只能走公开接口。

module integrator_mod use iso_fortran_env, only: real64 implicit none private ! 默认全部私有 public :: integrate, set_step ! 只放行两个接口 real(real64) :: step = 0.01_real64 ! 内部状态,外部碰不到 contains subroutine set_step(h) real(real64), intent(in) :: h step = h end subroutine set_step real(real64) function integrate(f, a, b) result(r) interface pure function f(x) result(y) use iso_fortran_env, only: real64 real(real64), intent(in) :: x real(real64) :: y end function f end interface real(real64), intent(in) :: a, b integer :: n real(real64) :: h, x n = max(1, nint((b - a) / step)) h = (b - a) / n r = 0.0_real64 do x = a + 0.5_real64 * h, b, h r = r + f(x) end do r = r * h end function integrate end module integrator_mod

内部 step 只能通过 set_step 修改,积分步长这个全局状态有了门禁。这正是模块替代 COMMON 块的方式:不是消灭全局状态,而是给全局状态加上受控的读写通道。

子模块:接口与实现物理分离

大模块有个编译痛点:过程实现一改,模块文件时间戳变了,所有 use 它的单元全部重编译。子模块(submodule)把实现下沉到单独文件:父模块只留接口,子模块放实现,实现细节变化不影响使用方的编译。

module matrix_ops_mod implicit none interface module function mat_norm(a) result(n) use iso_fortran_env, only: real64 real(real64), intent(in) :: a(:,:) real(real64) :: n end function mat_norm end interface end module matrix_ops_mod submodule (matrix_ops_mod) matrix_ops_impl use iso_fortran_env, only: real64 contains module function mat_norm(a) result(n) real(real64), intent(in) :: a(:,:) real(real64) :: n n = sqrt(sum(a * a)) ! Frobenius 范数 end function mat_norm end submodule matrix_ops_impl

增量编译的收益在大型项目里是实打实的:改一个 LU 分解的内部循环,不必让整个依赖它的应用重新构建。子模块还能继续嵌套出层级,把"物理模型"模块拆成"流体力学""热力学"等子模块,再各自细分,文件树直接映射功能树。

图:模块依赖结构——从全局共享到受控接口

图:模块依赖结构——从全局共享到受控接口

避免循环依赖

模块树应该是无环的:A 用 B、B 用 C,但 A 和 B 不能互相 use。循环依赖会让编译器和维护者都发疯。预防方法:把共同依赖下沉到底层模块;把接口与实现分离(用子模块切断物理依赖);重构时发现循环,先拆出公共部分,别硬绕。

学习目标

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

  1. 用 use、use only、use 重命名三种方式控制符号导入
  2. 采用"默认私有、显式公开"策略封装模块内部状态
  3. 说明子模块如何实现接口与实现的分离,以及增量编译为何受益
  4. 识别并避免模块间的循环依赖

本节要点回顾

  • use only 是工程铁律:显式导入让依赖关系可读,重命名解决符号冲突
  • 默认私有、显式公开:模块内部状态有门禁,外部只能走接口
  • 子模块物理分离接口与实现:改实现不触发全量重编译,大型项目收益显著
  • 模块树必须无环:循环依赖靠下沉公共部分破解
  • 模块是 COMMON 块的文明替代:不消灭全局状态,而是给它装上受控通道
  • 模块内的过程自带显式接口:这是 4.2 过程契约与第 7 章互操作的前提

容器讲完了,看内容——过程设计的参数契约与并行属性。


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