模块内contains块子程序与模块外定义子程序的底层差异探讨
我碰到一个情况,在Fortran里,模块的相关子程序既可以定义在模块的contains块内部,也可以作为独立子程序放在模块外部(只要通过use引用模块就能访问类型)。用gfortran编译时两种写法都能正常运行,从使用层面看不出区别,想请教这两种写法在底层有哪些差异?
写法1:子程序独立于模块之外
module core implicit none type :: disc_status sequence real*8 :: alpha1, alpha2, alpha3 end type disc_status end module core subroutine tester(input) use core type(disc_status), intent(in) :: input print *, input%alpha1, input%alpha2, input%alpha3 end subroutine tester ! 调用程序 program flyingDiscSimulator use core implicit none type(disc_status) :: disc disc%alpha1 = 1.1D0 disc%alpha2 = 1.2D0 disc%alpha3 = 1.3D0 call tester(disc) print *, 'it works' end program flyingDiscSimulator
写法2:子程序放在模块contains块内
module core implicit none type :: disc_status sequence real*8 :: alpha1, alpha2, alpha3 end type disc_status contains subroutine tester(input) type(disc_status), intent(in) :: input print *, input%alpha1, input%alpha2, input%alpha3 end subroutine tester end module core ! 调用程序 program flyingDiscSimulator use core implicit none type(disc_status) :: disc disc%alpha1 = 1.1D0 disc%alpha2 = 1.2D0 disc%alpha3 = 1.3D0 call tester(disc) print *, 'it works' end program flyingDiscSimulator
好问题!这两种写法虽然功能上看起来一致,但底层的绑定关系、作用域和编译处理逻辑都有不少区别,我给你拆解一下:
作用域与模块绑定关系
当子程序放在模块的contains块内时,它是模块的内部过程,属于模块的一部分,和模块的私有/公有属性直接挂钩。默认情况下,模块内的过程是公有的,但你可以用private tester把它设为私有,外部程序就无法直接调用了。
而独立在模块外的子程序只是通过use core引用了模块的类型,它本身不属于模块,是一个独立的外部过程。不管模块怎么设置私有属性,只要这个子程序能use到模块的类型,它就能被任何能访问它的程序调用,没法通过模块的访问控制来限制它的可见性。编译时的符号解析与接口检查
对于模块内的子程序,编译器在处理模块的时候就会把它的符号和模块绑定在一起,生成的.mod模块文件里会包含这个子程序的完整接口信息。其他程序use模块时,直接就能拿到接口,编译时可以做严格的类型检查——比如你调用tester时传错参数类型,编译器会直接抛出错误。
而独立的外部子程序,默认情况下编译器不会自动生成它的接口(除非你显式编写接口块)。如果调用它的时候没有接口,编译器只能做基础的参数数量检查,类型不匹配的问题可能只会在运行时暴露,甚至某些情况下会悄悄引发错误(比如实数和整数混传)。虽然gfortran可能会做额外的检查,但这并非Fortran标准规定的行为。性能优化空间
模块内的子程序因为和模块深度绑定,编译器能获取到更多上下文信息,比如模块类型的内存布局、模块内其他变量或过程的细节,更容易进行优化(比如过程内联、参数传递优化)。
独立的外部子程序,编译器对它的上下文感知更少,优化潜力相对有限——除非你通过显式接口或者编译器优化选项来弥补,但效果还是不如模块内过程直接。代码封装性与可维护性
从软件工程角度,把操作模块类型的子程序放在contains块内是Fortran推荐的封装方式,能把数据类型(disc_status)和对应的操作(tester)聚合在一起,代码的可读性和可维护性更好。而独立子程序相当于把操作和数据分离,违反了封装原则,在大型项目里容易出现代码分散、难以追踪管理的问题。模块内部数据的访问权限
如果模块里有私有变量(比如在implicit none后定义real*8 :: private_var,再用private private_var设置私有),模块内的子程序可以直接访问这个私有变量,但独立的外部子程序就算use了模块,也无法访问私有变量——只能通过模块提供的公有接口来操作。这一点在你的示例中没体现,但涉及模块内部状态时差异会非常明显。
内容的提问来源于stack exchange,提问作者James Wright

