Julia参数化复合类型

类型参数化

对于有成员结构的类型,往往需要限定内部成员的类型。但很多时候,在结构内部进行类型限定时会使复合类型的适用性受到限制。为了不放弃类型的限定条件,又适应灵活的类型使用场景,便有了对类型进行参数化的需求。

例如,需要定义一个坐标点类型,会有一维、二维、三维等情况;而每一维的类型可以是Int8、Int64,也可以是Float32、Float64等。如果有严格的类型限定,需要针对不同的类组合定义很多具体的复合类型,而且一旦需要为这些类型定义操作函数,也会需要众多的实现方法。这种做法不但导致代码量骤增,维护起来也很麻烦。

为此,我们需要对概念进行抽象。例如,可定义一个无维度限定的名为Pointy的抽象类型,为所有的点类型定义统一的接口函数;然后再定义一维点Point1D、二维点Point2D及三维点Point3D等复合类型,针对不同维度定义相应的操作方法。在确定成员类型时,将成员的类型作为参数,交由外部调用方控制。

参数化复合类型

1.定义
若要对复合类型实现参数化,只需声明时在类型名称之后附加类型参数(简称类参)列表即可,如下所示:

        [mutable] struct类型名{T1, T2, ...}
          成员字段1::T1
          成员字段2::T2
          # 其他成员定义
        end

其中,mutable为可选;类参需在紧跟类型名的大括号{}内列出(类型名后不能有空格);内部的成员字段若要限定类型,便可以在类参表中选择一个恰当的类参使用。当然,也可以不借助类参,仍可以使用某个明确的类型对其单独地限定,而不使用类参,或者干脆不限定类型。

在对参数化的复合类型实例化时,类型实参需是已经存在的类型,可以是元类型、抽象类型,甚或某种复合类型等。

让我们继续前述坐标点的例子。先尝试对一维点类型Point1D实现参数化,如下:

julia> mutable struct Point1D{T}
          x::T       # 限定成员类型为T
        end

此后,通过给定具体的类参T,便可基于上面的参数化类型定义出具体的复合类型(为方便,称之为具象化类型),例如:
Julia参数化复合类型

其中,打印的类型信息中多出了{Int32}{Real}标识,表达了参数化类型被具象化之后的实际类型,遵循着类参提供时的语法格式。在上例中,类参T被具体的实参Int32或Real类型替代,生成了两种具象化类型。

对于得到的每个具象化类型,实际上“复制”了其参数化版本的结构,并同时将其中的类参落实为某个确切的类型,从而得到一个新的类型。如对于上例中的Point1D{Int32},实际上得到了内部结构如下的复合类型:

Point1D{Int32} <: Any
  x::Int32

因为x在参数化类型中被限定为类参T,所以在得到的具象化类型中,x的类型被实际限定为了Int32类型。

从某个角度来说,因为类参的扩展性及具象时的多样性,声明的参数化类型其实相当于一个名称相同的类型族。而且,在类型关系上,定义出的这些具象化类型均是参数化类型的子类型,即:
Julia参数化复合类型

不过,“族”中的各具象化类型之间并不存在父子关系,即使它们的类参之间存在父子关系。例如:

julia> Point1D{Int32} <: Point1D{Real}
false

julia> Point1D{Real} <: Point1D{Int32}
false

虽然其中的Real是Int32的父类型,但对应的具象化类型不会自然地延续这种关系。显而易见,各具象化类型也不满足“相等”或“相同”的判断,例如:

julia> Point1D{Int32} == Point1D{Real}
false

即类型参数不同,对应的具象类型便不同。

参数化类型与具象化类型的关系,可以类比于DataType与Int32、Real等类型之间的关系。每个具象化类型都是参数化类型的实例,而且也都是独立的类型。

2.实例化
事实上,不论是Point1D还是某个其他的具象化类型,我们都能够发现Julia内部为其提供的默认构造方法中形如以下类型:

(::Type{Point1D})(x::T) where T

看起来与前述那种无类参的复合类型有不少差异:一个是类型名被::Type{}结构所封装,另一个是其中出现了where T结构。这样的结构我们会在后文详细介绍,姑且暂时忽略它,便可发现这仍是一种函数的形式,所以依然能够以类似普通复合类型的方式对其进行实例化,例如:

julia> p1 = Point1D(1.1)
Point1D{Float64}(1.1)

不难理解,输入的参数1.1用于给成员x赋值,因为其被自动识别为Float64类型,所以上例中这种没有明确给定类参的创建方法,会自动将T具象化为Float64类型。若查看上例中成员x的值与类型,可发现:
Julia参数化复合类型

其中,x确实为提供的值,而其类型也与提供值的类型一致。

如果我们变换提供值的类型,那么信息也会随之而变,例如:
Julia参数化复合类型

可见在对参数化类型实例化时,可不必先对其具象化,直接提供成员值便可创建出具体的对象,而类参的实际具象类型会自动推断出来。

当然,实例化时类参的实际类型也可以不由Julia自动推断生成,而是显式地给出:

julia> p2 = Point1D{Int32}(Int64(1))   # 明确地限定提供值1为Int64类型,但类参具象为Int32
Point1D{Int32}(1)                        # 对象的实际类型取Int32

julia> typeof(p2.x)
Int32

此时成员参数的类型优先级较低,最终的类型会由类参的具象类型决定。

在此,针对参数化类型的实例化方法,做以下总结:

类型名(成员值1, 成员值2, …)               # 采用同非参数化的普通类型一致的实例化方法
类型名{T1, T2, …}(成员值1, 成员值2, …)  # 明确地列出类型实参

以上两种方式都可以对参数化的类型进行实例化。

下面我们再看具象化的另外一个特性:类似于非参数化复合类型,具象化类型的成员类型是稳定的,例如:

julia> p2.x = Int8(2)                   # 赋值为Int8类型
2

julia> typeof(p2.x)
Int32                                      # 保持不变

julia> p2.x = 1.2                        # 赋值为Float64类型
ERROR: InexactError: Int32(Int32, 1.2)

例中试图在赋值时改变x::T的类型,不过,都在可提升的情况下被隐式地转为类参限定的类型。但如果成员参数值的类型无法转换或提升为具象类型,这种改变类型的赋值操作不会成功而且会报异常。所以,在实例化时,如果采用前面第二种方法,作为整个复合类型的类参,会在类型限定方面有着更强的约束,类参限定的成员是无法改变类型的。

下面我们再看三维点的例子,其声明方法如下:

julia> mutable struct Point3D{T1, T2}
          x::T1
          y::T1
          z::T2
        end

其中有T1与T2两个类参,成员x与y均被限定为类型T1,而z则限定为另一类型T2。这意味着,成员x与y必须是相同的具体类型,而z可以是与之不同的其他类型;当然,在具象化时T1与T2一致,此时三个成员的类型便是一致的。

此时Point3D的默认构造方法形式如下:

(::Type{Point3D})(x::T1, y::T1, z::T2) where {T1, T2}

可见在参数表中,x与y的类型是相同的。若对其实例化,可得:

julia> q1 = Point3D{Int64, Float32}(1, 2, 3.1)    # T1与T2分别被具象化为Int64和Float32
Point3D{Int64, Float32}(1, 2, 3.1f0)

julia> typeof(q1.x) == typeof(q1.y) == Int64
true

julia> typeof(q1.z)
Float32                                                # 3.1被赋值为z后,被转为Float32类型

其中,x与y的类型均是T1的实参(具象类型)Int64,而z的类型则是T2的实参Float32类型。如果提供相同的T1与T2,则三者会是同样的类型,例如:

julia> q2 = Point3D{Int32, Int32}(1,2,3)
Point3D{Int32, Int32}(1, 2, 3)

julia> typeof(q2.x) == typeof(q2.y) == Int32 == typeof(q2.z)
true

可见三者虽然以不同的类参限定,但当参类相同时,实例化之后获得了相同的类型限定。

但提供的前两个成员值类型不同时,对象会创建失败,例如:

julia> Point3D(Int32(1), Int8(2), Float32(3.1))
ERROR: MethodError: no method matching Point3D(::Int32, ::Int8, ::Float32)

虽然前两者都是整型的一种,而且Int8能被提升为Int32,但因类型限定关系是通过struct传递的,所以不会成功。

事实上,有理数型与复数型都是参数化的复合类型,只不过一个是Real的子类型,另一个是Number的子类型,即:

Rational{T <: Integer} <: Real
Complex{T <: Real} <: Number

如果查看它们的内部结构,便可发现:

Rational{T<:Integer} <: Real
  num::T
  den::T

Complex{T<:Real} <: Number
  re::T
  im::T

它们都有两个成员字段,但类参却都只有一个,从而能够确保内部的两个成员必须是一致的类型。

另外,这两个类型虽然是复合类型,但都是Number的子类型,即都是数字。它们不但在数学上有着切实的意义,也确实“共享”着各种数字集合的计算规则。在Julia中,复合类型与普通类型相比,并不是非常“特别”的结构,在语法操作上完全可以同等对待。

至此,我们总结参数化复合类型的特点如下:复合类型的类参数量不限,视需要而定;内部成员可沿袭其类参作为限定类型;通过类参的组合,在成员可取任意类型的情况下,能够限定某些成员必须类型一致。

酷客网相关文章:

赞(0)

评论 抢沙发

评论前必须登录!