Julia参数化抽象类型
定义
在Julia中,若要定义一个参数化的抽象类型,基本语法为:
abstract type类型名{T1, T2, ...} end
其中,“类型名”是待定义的抽象类型名称,大括号中的是各种类参列表。
注意
语法中的“类型名”与左大括号之间不能有空格。
仍继续关注坐标点的例子。假设为二维点定义Point2D类型如下:
julia> mutable struct Point2D{T}
x::T
y::T
end
之后,对其定义求向量模的计算:
mymodule(p::Point2D) = sqrt(p.x^2 + p.y^2) # 定义了名称为mymodule的函数
显然调用时需提供Point2D类型的参数才能正常获得结果,例如:
而采用Point1D等其他类型是不允许的,例如:
在以Point1D
类型作为参数调用时会报MethodError
错误,并提示未找到匹配的方法。可以想见,对Point3D也同样如此。
当然此函数的参数p也可以限定为Point2D的某个具象类型,例如:
mymodule(p::Point2D{Int64}) = sqrt(p.x^2 + p.y^2)
显然此方法限制性过大,徒增烦恼;而之前以参数化类型的名称限定p的类型更具普适性。
如果我们需要对前文中的Point1D与Point3D也提供同样的求向量模功能函数,显然需为这两个类型提供单独的定义,即再定义两个计算函数。如果应用中存在更多维的坐标类型,同时功能函数不仅仅求模值这一种,那么代码量将骤增,而且是枯燥地重复性增加,会给后续的维护带来麻烦。所以,我们需要一种抽象的坐标类型,能够承载任意维度坐标共有的计算操作。
为此定义一个名为Pointy的抽象类型,并进行参数化,如下:
julia> abstract type Pointy{T} end
如果T被指定,获得的所有具象化类型均为Pointy
的子类型,例如:
julia> Pointy{Int64} <: Pointy
true
julia> Pointy{Float64} <: Pointy
true
更为灵活的是,参数T还可以取具体的数值:
julia> Pointy{1} <: Pointy
true
这对于内部成员有元素数量可动态延展的结构(如后面介绍的数组),会非常有用。
对于声明的参数化抽象类型,不同具象类型之间同样无父子关系,例如:
julia> Pointy{Float64} <: Pointy{Real}
false
虽然Real是Float64的父类型,但以它们为基础的具象类型不具有相容性。
应用
实际上,如抽象类型或元类型那样,类型之间的继承关系是需要在声明时显式确定的。对于之前声明过的Point1D
、Point2D
与Point3D
,完全可以在声明时作为抽象坐标类型Pointy的子类型。所以将它们的声明方式修改如下:
julia> mutable struct Point1D{T} <: Pointy{T}
x::T
end
julia> mutable struct Point2D{T} <: Pointy{T}
x::T
y::T
end
julia> mutable struct Point3D{T} <: Pointy{T}
x::T
y::T
z::T
end
此时三种的参数化类型便成为父参数化类型Pointy的子类型,即:
Point1D <: Pointy
Point2D <: Pointy
Point3D <: Pointy
又因为具象类型是参数化类型的子类型,所以子参数化类型的具象类型均是父参数化类型的子类型,即如下的父子关系断言也是成立的:
Point1D{Float64} <: Pointy
Point2D{Integer} <: Pointy
但父子参数化类型的具象化类型之间仍不存在父子关系,例如:
julia> Point2D{Integer} <: Pointy{Real}
false
虽然其中的类参具备父子关系。
如此一来,我们便可以在Pointy上定义一个统一的操作函数,便能适用于三个不同维度的坐标点类型,而不需要更多针对性的重复性函数。重新定义向量求模函数如下:
function module_t(p::Pointy) # 函数的定义方式会在后面详述
m = 0
for field in fieldnames(p) # 遍历成员字段名
m += getfield(p, field)^2 # 取得该成员值并平方
end
sqrt(m) # 将多成员值平方和开方
end
该函数能够适应不同数量字段的复合类型。此时便可用其求解不同维度坐标点的模值,例如:
julia> module_t(Point1D{Int64}(1))
1.0
julia> module_t(Point2D{Float64}(2, 2))
2.8284271247461903
julia> module_t(Point3D{Complex}(1+2im, 3+4im, 5-6im))
2.9389894877329246-5.44404805351722im
可见,子类型沿袭父类型的计算规则能够给开发带来巨大的便利。
不过上例因涉及反射(Reflection)机制,未必是最佳实践,仅作为示例使用。
Type{T}
前面提及,参数化类型Type{T}
是Union及Core.TypeofBottom
的父类型。实际上,该类型同时也是DataType的父类型,即:
julia> supertype(DataType)
Type{T}
julia> supertype(Type)
Any
而Type{T}
的父类型才是Any类型。
在Julia内部,Type{T}
是一种非常特殊的参数化抽象类型,也是单例类型的一种,可以认为是类型的生成器。从定义层面讲,每个具象的T,都是Type{T}类型的唯一实例对象。为便于理解,先看一些例子:
julia> isa(Float64, Type{Float64})
true
julia> isa(Real, Type{Real})
true
julia> isa(Point2D, Type{Point2D})
true
julia> isa(Pointy, Type{Pointy})
true
但对于不一致的T,这种判断不会成立。例如:
julia> isa(Float64, Type{Real})
false
julia> isa(Point2D, Type{Pointy})
false
而且具体的值也不是Type的实例:
julia> isa(1, Type)
false
julia> isa("foo", Type)
false
可以说,若isa(A, Type{B})
(A是Type{B}的实例)成立,有且仅有一种情况:A与B完全相同。
如果结构中没有参数T, Type便是一个简单的参数抽象类型;此时,所有的类型都将是它的实例对象,也包括Type本身,即:
julia> isa(Type{Float64}, Type)
true
julia> isa(Float64, Type)
true
julia> isa(Real, Type)
true
julia> isa(DataType, Type)
true
julia> isa(Type, Type)
true
在酷客教程下文说明参数化原理时,我们还会介绍另一种Type{T}单例类型UnionAll。
酷客网相关文章:
评论前必须登录!
注册