Fortran笔记 派生类型-整理版

以下为整理后的笔记,英文原文 Introduction to Modern Fortran for the Earth System Sciences

英文翻译 https://www.cnblogs.com/jiangleads/articles/16089427.html

派生数据类型

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

定义派生类型

派生类型的定义的方式为 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
21
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.27 src/Chapter3/dt_basic_demo.f90   

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

  • 首先,它由一个声明部分(specification part)(上面的第10行),指定了数据的部分。

在本例中,每个Vec2D实体有两个real-类型变量。假设myVec是一种Vec2D类型的变量,我们可以通过myVec%mU和myVec2D%mV来访问这两个分量。

  • 其次,有一个contains声明,将过程部分(procedures part)(上面的第12行)分隔开来。

这并不完全和module一样,因为在这里仅有procedure部分出现——procedure的具体实现在出现在代码的其他位置。Fortran2003标准支持上述的这种过程(也叫类型绑定过程(type-bound procedures)或方法(methods))。上述方法的接口需要是显式的(所以它既可以是模块过程(module procdures),如本例;也可以是具有接口块(interface-block)的外部过程(external procedures))。

在我们定义的最初版本的Vec2D中,只有一个方法(method)——函数getMagnitude,它是函数getMagnitudeVec2D(第16-19行)的别名。函数getMagnitudeVec2D的定义看起来和正常函数一样,除了第17行,虚参(this)的声明语句为class(Vec2D)之外。这个参数,又叫做传递对象虚参(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,需要注意的是,默认情况下,它不会出现在调用行,因为它会被编译器默认加入。通常情况下,它对应于实际程序定义中的第一个参数。然而,对于ListOfBindAttrs的设置,需要好好地调试:

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

最后,请注意,传递对象虚参的名字可以随便起。通常,为了和其他面向对象语言保持一致,约定俗成的名称是this

 

使用派生类型

定义常量和变量

包含了派生类型的模块可被程序使用,用来定义变量和常量,如下:

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) ! 声明变量时候就进行初始化。使用隐式构造函数(implicit constructors)
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 (节选)  

 

变量的初始化

有两种方式

1. 可以直接在声明变量时候进行初始化(第27行)。PS:对于常量,这是必须的(第28行)

2. 在声明变量时候也可以不赋初值(隐式初始化,第26行)。对于派生型的变量,可以在定义派生类型时候就指定初值(mU=mV=0——参见Listing 3.27中的第10行)。

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

 

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

final :: ListOfProcedures

 

派生类型的方法调用

调用方式:对象名%方法名(参数)。

例如,在Listing 3.28的第32-34行中,我们称之为Vec2D的getMagnitude方法。虽然在本例中似乎没有为方法指定任何参数(在getMagnitude()的括号里面没有值),但我们从方法的定义中知道,应该有一个参数——this由编译器悄悄的添加了(接收A、B和C作为实际参数,分别参见第32、33和34行)。

 

变量访问控制和信息隐藏

信息隐藏。使用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语句(数据和方法具有独立的访问控制),改变了默认策略。然而,访问限制确实带来了一小部分成本:现在我们有责任设计与派生类型交互的适当机制。

注意,当我们设置了数据成员为private之后,编译器将不能提供隐式构造函数。此时,默认的初始值已经给指定,不能再进行修改。

解决以上问题,有两种方式:1 定义一个自定义构造函数,2 声明一个普通的类型绑定过程

1. 定义一个自定义构造函数

Fortran中实现这一点的机制与其他语言相比有所不同,构造函数与类型的绑定是通过一个命名的接口块(在Fortran中也称为“generic interface")实现的。该接口块的名称是我们希望构造的派生类型。

 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      ! . . . more methods (ommitted in this example) . . .
17   end type Vec2D
18 
19 ! Generic IFACE, for type-overloading
20 ! (to implement user-defined CTOR)
21   interface Vec2d    !通过命名的接口块,实现构造函数和类型的绑定
22      module procedure createVec2d   ! 译者总结:interface中的过程需要加module procedure关键字
23   end interface Vec2d
24 
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 end module Vec2d_class

使用上述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 = createVec2D(u=1.1, v=9.4)
51 
52   ! Separate call to CTOR.
53   A = Vec2D(u=1.1, v=9.4)
54 end program

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

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

因此,用户构造函数的唯一好处是他们的语法更加方便,并且,作为函数,他们可以被直接在表达式中使用

2. 声明一个普通的类型绑定过程(方法)

(在本例中,名为init)。它通过参数接受初始化数据,并相应地修改对象的状态。与自定义构造函数相比,它的优点是不需要制作对象的临时副本,缺点是调用语法不太方便。

 7 module Vec2D_class
 8   implicit none
 9   private ! Make module-entities "private" by default.
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
25 
26 contains
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

使用上述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)
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 (节选)  

使用类型绑定过程(使用“init”子程序——参见第56行)同样存在以下限制:它们既不能用于定义基于派生类型的常量,也不能在变量的声明行进行初始化。唯一允许的用法是将初始化的语句放在(子)程序的声明部分之外。

访问派生类型变量的分量

可以通过添加两个类型绑定函数(方法),实现访问派生类型变量的分量,如下所示。

这些方法也称为访问器(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 

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 (节选) 

 

译者总结:

  1. 为了实现对数据访问权限的控制,可以设置private属性。派生类型设置为private的代价是:不能定义基于该类型的常量。 
  2. 区分类型绑定过程(process)和函数的方法:在定义语句,括号里有this的,是方法(类型绑定过程);没有this的,是函数(如用户构造函数)。
  3. 在本例中,在命名的接口块中的procedure前面需要加module
posted @ 2022-04-02 21:00  chinagod  阅读(1031)  评论(0编辑  收藏  举报