Fortran笔记 派生类型-整理版
以下为整理后的笔记,英文原文 Introduction to Modern Fortran for the Earth System Sciences,
英文翻译
派生数据类型
在数值模式中,会经常要用到一些实体(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)的情形下很有用(参见)。
最后,请注意,传递对象虚参的名字可以随便起。通常,为了和其他面向对象语言保持一致,约定俗成的名称是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 (节选)
译者总结:
- 为了实现对数据访问权限的控制,可以设置private属性。派生类型设置为private的代价是:不能定义基于该类型的常量。
- 区分类型绑定过程(process)和函数的方法:在定义语句,括号里有this的,是方法(类型绑定过程);没有this的,是函数(如用户构造函数)。
- 在本例中,在命名的接口块中的procedure前面需要加module