Fotran笔记之 派生数据类型

参考自 Introduction to Modern Fortran for the Earth System Sciences

3.3.2 派生数据类型

在数值模式中,会经常要用到一些实体(entities),它们比单个变量或由同类型变量组成的数组更加复杂。为了能搞满足这种需求,Fortran支持了用户定义类型(user-defined types)(也叫派生数据类型(Derived Data Types (DTS)或absctact data types (ADTS))。这提供了将不同类型的实体打包成一个逻辑单元的方法,类似于传统的OOP类,提供了基本的用于封装(encapsulation)的工具(vehicle)。

3.3.2.1 定义派生类型

派生类型的定义的方式为 type DtName - end type DtName.

 5 ! 派生数据类型通常被包含在modules中
 6 module Vec2D_class
 7   implicit none
 8 
 9   type Vec2D ! 以下: 对数据成员的声明
10 real :: mU = 0., mV = 0. 11 contains ! 以下: 对类绑定过程的声明
12 procedure :: getMagnitude => getMagnitudeVec2D 13 end type Vec2D 14 15 contains 16 real function getMagnitudeVec2D( this ) 17 class(Vec2D), intent(in) :: this 18 getMagnitudeVec2D = sqrt( this%mU**2 + this%mV**2 ) 19 end function getMagnitudeVec2D 20 end module Vec2D_class

 Listing 3.27 src/Chapter3/dt_basic_demo.f90 (节选)  

为了重复使用,每个派生类型通常被放在一个module(理想状况下是一个派生类型一个module,这也是为什么一些作者,包括我们通常在其中加入后缀_class) 

在很多方面,派生类型很像module:

首先,它由一个声明部分(specification part)(上面的第10行),指定了数据的部分。在本例中,每个Vec2D实体有两个real-类型变量。假设myVec是一种Vec2D类型的变量,我们可以通过myVec%mU和myVec2D%mV来访问这两个分量。

其次,有一个contains声明,将程序部分(procedures part)(上面的第12行)分隔开来。这并不完全和module一样,因为在这里仅有声明部分出现——程序的实际实现在出现在代码的其他位置。

Fortran2003标准支持了这些程序(称为类型绑定过程(type-bound procedures)或方法(methods))。上述程序的接口需要是显式的(所以它们既可以是模块过程(module procdures),也可以是具有接口块(interface-block)的外部过程(external procedures))。在我们定义的最初版本的Vec2D中,只有一个方法(method)——函数getMagnitude,它是函数getMagnitudeVec2D(它们在第16-19行)的别名。它看起来像是一个正常的函数定义部分,除了虚参(this)的声明得有些不同:通常使我们使用系统内置类型定义,但在这里我们使用class(<DtName>)(第17行),来告诉编译器我们使用的是派生类型。这个参数,又叫做传递对象虚参(passed-object dummy argument),当它绑定到特定的派生类型中时,它将对应到调用这个方法的对象。在本例中,这个虚参的绑定是在第12行发生。这类绑定的基本语法是:

procedure [( interfaceName )] [ListOfBindAttrs ::] bindName [=> procedureName ]

 其中:

  • interfaceName ,如果指定,将会实现类似抽象基类(abstract base classes)的功能。相关讨论超出本文范畴,详见 Clerman and Spector [Clerman, N.S., Spector, W.: Modern Fortran: Style and Usage. Cambridge University Press, Cambridge (2011) ]
  • ListOfBindAttrs,如果指定,他是一个由逗号分隔开的一系列属性。这些属性成员可以是public或private(他们和信息隐藏有关,随后讨论),pass或nopass(和传递对象的虚参有关),以及non_overridable(和继承(inheritance)有关)。
  • bindName,唯一必需的参数,代表使用这个派生类型的程序的名称。如果procedureName没有写,那么bindName需要对应程序的实际名称。
  • procddureName,当指定时,表示实际程序的名称(于它相对应,此时bindName将会是一个别名)

关于this,需要注意的是,默认情况下,它不会出现在调用行,因为它会被编译器悄悄的加入。默认情况下,它对应于实际程序定义中的第一个参数(就像在我们的例子中一样)。然而,对于绑定属性列表这一步,需要好好地调试:

  • 当使用nopass绑定参数时,对象将不再传递到程序。这并不十分常见,但是作为优化时将会很有用,比如出现以下这种情况时:当方法并不实际需要访问到对象的数据(或者没有实际的数据)时
  • 通过使用pass(dummyArgName)绑定属性,可以选取其他的参数而不是第一个参数,用来指向作为传递对象的虚参的程序。很显然,dummyArgName需要用程序中虚参的实际名称代替。这种技巧对于运算重载(operator overloading)的情形下很有用(参见3.3.4节)。

最后,请注意,任何名称将会被选作用于传递对象的虚参。然而,约定俗成的名称是this,从而和其他面向对象语言保持一致。

3.3.2.2 使用派生类型定义

包含了派生类型的模块可被程序use,用于声明新类型的变量和常量,如下:

22 program test_driver_a
23   use Vec2D_class
24   implicit none
25 
26   type(Vec2D) :: A ! Implicit initialization
27   type(Vec2D) :: B = Vec2D(mU=1.1, mV=9.4) ! can use mU&mV as keywords
28   type(Vec2D), parameter :: C = Vec2D(1.0, 3.2)
29 
30   ! Accessing components of a data-type.
31   write(*, '(3(a,1x,f0.3))') &
32        "A%U =", A%mU, ", A%V =", A%mV, ", A%magnitude =", A%getMagnitude(), &
33        "B%U =", B%mU, ", B%V =", B%mV, ", B%magnitude =", B%getMagnitude(), &
34        "C%U =", C%mU, ", C%V =", C%mV, ", C%magnitude =", C%getMagnitude()
35 end program test_driver_a

Listing 3.28 src/Chapter3/dt_basic_demo.f90 (节选)  

对于声明(第26-28行),数据类型用type (<DtName>)-结构给定。关于初始化,可以直接用声明行(B)直接初始化——显然,这对于常量(C)是需要的。然而,注意到我们没有显式的声明A。这是为了演示一种机制,该机制适用于派生类型,但不适用于隐式类型——defaults values。虽然没有标准的方法将常规默认值(例如0)分配给内置类型的变量,但对于派生类型,我们可以指定这样的值(mU=mV=0——参见Listing 3.27中的第10行,其中定义了派生类型)。

为了使派生类型初始化成为可能,Fortran提供了隐式构造函数(implicit constructors)。这些看起来像函数调用,其中派生类型的数据成员的名称可以用作关键字,以提高可读性(上面第27行)。如果默认构造函数不够,可以编写自定义构造函数(但有一些重要的观察结果,将在下面讨论)。

类似地,其他语言中类似的析构函数也是终止(final)-的过程。当使用指针时,或者当派生类型不存在时需要特殊操作时,析构函数应该被写入。析构器(finalizer)也在派生类型定义中的contains-statement之后指定(尽管严格来说,它们不是类型绑定过程)。它们的语法是:

final :: ListOfProcedures

 

在讨论netCDF输出时,我们给出了一个这样的程序(第5.2.2节)。

派生类型的方法可以被调用,调用方式与引用数据成员的方式类似,先引用对象的名称,后跟%,然后引用方法的名称,并在括号中加上参数。例如,在Listing 3.28的第32-34行中,我们称之为Vec2D的getMagnitude方法。我们可以在这里使用前面的传递对象的虚参的讨论:虽然在本例中似乎没有为方法指定任何参数,但我们从方法的定义中知道,应该有一个参数——this由编译器悄悄的添加(接收A、B和C作为实际参数,分别参见第32、33和34行)。

3.3.2.3 访问控制和信息隐藏

出于演示目的,我们让上面的Vec2D类型的内部数据可以从主程序访问。然而,我们这样做违反了OOP的数据隐藏和封装原则,这破坏了该范例的许多好处:例如,如果Vec2D类型的维护者认为极坐标(r,θ)的表示比(x,y)更有效,他们就不能简单地进行更改,而不考虑所有用户也需要修改他们的程序。为了解决这个问题,最好通过机智的使用private和public关键字,精确调整给其他程序单元可见的内容。在本例中,我们可以采用如下定义:

 1 ! 注意: 这个派生类型的访问是有限制的(参见下面的讨论)
 2 type , public : : Vec2D ! DT explicitly declared"public"
 3   private ! Make internal data "private" by default.
 4   real :: mU = 0., mV = 0.
 5 contains
 6   private ! Make methods "private" by default.
 7     ! (good practice for the case when we have
 8     ! implementation -specific methods , that the user
 9     ! does not need to know about ).
10   procedure , public : : getMagnitude
11 end type Vec2D

 Listing 3.29 使用限制性访问的方法,创建Vec2D

请注意,我们在派生类型定义的两个部分中都添加了private语句(数据和方法具有独立的访问控制),以更改默认策略。然而,访问限制确实带来了一小部分成本:现在我们有责任设计与派生类型交互的适当机制。

首先,请注意,使用上面的类型定义,不可能用一些用户的分量来构造向量。事实上,其他程序单元中的代码无法创建非零向量,这使得我们的实现不是很有用!问题的核心是编译器不能提供隐式构造函数,因为数据成员现在是私有的。

  1. 这个问题的第一个可能的解决方案是定义一个自定义构造函数。与其他语言相比,Fortran中实现这一点的机制有所不同,因为自定义的构造函数不是类型绑定的过程。相反,构造函数与类型的绑定是通过一个命名的接口块(在Fortran中也称为“generic interface"(“通用接口”))实现的,该接口块的名称是我们希望构造的派生类型。
  2. 或者,我们可以声明一个普通的类型绑定过程(例如,名为init),它通过参数接受初始化数据,并相应地修改对象的状态。与自定义构造函数相比,它的优点是不需要制作对象的临时副本。然而,调用语法(稍微)不太方便

在接下来,我们展示了如何通过这两种机制定义这个过程。

 7 module Vec2D_class
 8   implicit none
 9   private ! Make module-entities "private" by default.
10 
11   type, public :: Vec2D ! DT explicitly declared "public"
12      private  ! Make internal data "private" by default.
13      real :: mU = 0., mV = 0.
14    contains
15      private  ! Make methods "private" by default.
16      procedure, public :: init => initVec2D  !类型绑定过程
17      ! . . . more methods (ommitted in this example) . . .
18   end type Vec2D
19 
20   ! Generic IFACE, for type-overloading
21   ! (to implement user-defined CTOR)
22   interface Vec2D  ! 通过命名的接口块,实现构造函数和类型的绑定
23      module procedure createVec2D
24   end interface Vec2D
25 
26 contains
27   type(Vec2D) function createVec2D( u, v ) ! CTOR 构造函数
28     real, intent(in) :: u, v
29     createVec2D%mU = u
30     createVec2D%mV = v
31   end function createVec2D
32 
33   subroutine initVec2D( this, u, v ) ! init-subroutine
34     class(Vec2D), intent(inout) :: this
35     real, intent(in) :: u, v
36     ! copy-over data inside the object
37     this%mU = u
38     this%mV = v
39   end subroutine initVec2D
40 end module Vec2D_class

 Listing 3.30 src/Chapter3/dt_constructor_and_initializer.f90 (节选)  

我们可以使用上述module,去声明和初始化Vec2D: 

42 program test_driver_b
43   use Vec2D_class
44   implicit none
45 
46   type(Vec2D) :: A, D
47   ! 错误:不能对具有私有数据的派生类型定义常量!
48   !type(Vec2D), parameter :: B = Vec2D(1.0, 3.2)
49   ! 错误:不能在声明时候使用自定义构造函数!
50   !type(Vec2D) :: C = Vec2D(u=1.1, v=9.4)
51 
52   ! Separate call to CTOR.
53   A = Vec2D(u=1.1, v=9.4)
54 
55   ! Separate call to init-subroutine
56   call D%init(u=1.1, v=9.4)
57 end program

Listing 3.31 src/Chapter3/dt_constructor_and_initializer.f90 (节选)  

如上所示,用户定义的构造函数是有限制的(与隐式构造器(由编译器提供,可以直接访问数据)相比——listing 3.27):它们既不能用于定义基于派生类型的常量,也不能在变量的声明行进行初始化。唯一允许的用法是将初始化的语句放在(子)程序的声明部分之外。

正如预料到的那样,这些限制同样存在于第二种初始化机制(使用“init”子程序——参见第56行)。因此,用户构造函数的唯一好处是他们的语法更加方便,并且,作为函数,他们可以被直接在表达式中使用。另一方面,初始化机制的选择依赖于程序员的偏好(或许,也可能是项目的约定)。

然而,我们应当强调,用户构造函数会导致(取决于编译器)不必要的临时对象被创建,这将会在一些情况下降低性能。当依赖大型对象(例如地球系统模式中封装模型数组的对象)的自定义构造函数时,或者当需要在耗时的循环中重复重新初始化对象时,建议谨慎使用(更好的是基准测试)。在本书其余部分的代码示例中,我们将使用这两种方法。

Listing 3.29的第二个问题是,缺乏访问向量分量的机制,Lisiting 3.30中更新的派生类型定义没有解决这个问题。我们可以通过添加两个类型绑定函数轻松解决这个问题,如下所示。这些方法也称为访问器(accessor)方法(或getters,因为它们的名称通常是通过连接“get”和组件的名称来形成的)。此外,我们还重新介绍了getMagnitude函数: 

 6 module Vec2d_class
 7   implicit none
 8   private ! Make module-entities "private" by default.
 9 
10   type, public :: Vec2d ! DT explicitly declared "public"
11      private  ! Make internal data "private" by default.
12      real :: mU = 0., mV = 0.
13    contains
14      private  ! Make methods "private" by default.
15      procedure, public :: init => initVec2d
16      procedure, public :: getU => getUVec2d
17      procedure, public :: getV => getVVec2d
18      procedure, public :: getMagnitude => getMagnitudeVec2d
19   end type Vec2d
20 
21   ! Generic IFACE, for type-overloading
22   ! (to implement user-defined CTOR)
23   interface Vec2d
24      module procedure createVec2d
25   end interface Vec2d
26 
27 contains
28   type(Vec2d) function createVec2d( u, v ) ! CTOR
29     real, intent(in) :: u, v
30     createVec2d%mU = u
31     createVec2d%mV = v
32   end function createVec2d
33 
34   subroutine initVec2d( this, u, v ) ! init-subroutine
35     class(Vec2d), intent(inout) :: this
36     real, intent(in) :: u, v
37     ! copy-over data inside the object
38     this%mU = u
39     this%mV = v
40   end subroutine initVec2d
41 
42   real function getUVec2d( this ) ! accessor-method (GETter)
43     class(Vec2d), intent(in) :: this
44     getUVec2d = this%mU ! direct-access IS allowed here
45   end function getUVec2d
46 
47   real function getVVec2d( this ) ! accessor-method (GETter)
48     class(Vec2d), intent(in) :: this
49     getVVec2d = this%mV
50   end function getVVec2d
51 
52   real function getMagnitudeVec2d( this ) result(mag)
53     class(Vec2d), intent(in) :: this
54     mag = sqrt( this%mU**2 + this%mV**2 )
55   end function getMagnitudeVec2d
56 end module Vec2d_class

 Listing 3.32 src/Chapter3/dt_accessors.f90 (节选) 

派生类型可以被用于public-版本中(但需要注意从A%mU变成A%getU(),对于v也是如此):

67   ! Accessing components of DT through methods (type-bound procedures).
68   write(*, '(3(a,1x,f0.3))') "A%U =", A%getU(), &
69        ", A%V =", A%getV(), ", A%magnitude =", A%getMagnitude()

Listing 3.33 src/Chapter3/dt_accessors.f90 (节选) 

 

posted @ 2022-04-02 20:58  chinagod  阅读(506)  评论(0编辑  收藏  举报