构造函数
构造函数 [1] 是用来创建新对象 – 确切地说,是创建 Composite Type 的实例,的函数。在 Julia 中,类型对象也同时充当构造函数的角色:它们可以被当作函数应用到参数元组上来创建自己的新实例。这一点在介绍复合类型(Composite Types)时已经大致谈过了。例如:
julia> struct Foo
bar
baz
end
julia> foo = Foo(1, 2)
Foo(1, 2)
julia> foo.bar
1
julia> foo.baz
2
对很多类型来说,创建新对象时只需要为它们的所有字段绑定上值就足够产生新实例了。然而,在某些情形下,创建复合对象需要更多的功能。有时必须通过检查或改变参数来确保不变性。Recursive data structures, 特别是那些可能引用自身的数据结构,它们需要首先被不完整地构造,然后作为创建对象的单独步骤, 通过编程的方式完成补全,否则它们不能被干净地构造。这时,能够用比字段少的参数或者 不同类型的参数来创建对象就很方便。Julia 的对象构造系统解决了所有这些问题。
[1]
命名法:虽然术语“构造函数”通常是指构造一个类型的对象的整个函数,但通常会略微滥用术语将特定的构造方法称为“构造函数”。这种情况下,通常可以从上下文中清楚地看出术语是用于表示“构造方法”而不是“构造函数”,因为它通常用于从所有构造方法中挑出构造函数的特定方法的场合。
外部构造方法
构造函数与 Julia 中的其他任何函数一样,其整体行为由其各个方法的组合行为定义。因此,你可以通过简单地定义新方法来向构造函数添加功能。例如,假设你想为 Foo 对象添加一个构造方法,该方法只接受一个参数并用该参数同时绑定为 bar 和 baz 字段的值。这很简单:
julia> Foo(x) = Foo(x,x)
Foo
julia> Foo(1)
Foo(1, 1)
julia> Foo() = Foo(0)
Foo
julia> Foo()
Foo(0, 0)
这里零参数构造方法调用的单参数方法,单参数构造方法又调用了自动提供的双参数构造方法。 像这样附加的以普通函数形式声明的构造方法被称为 外部 构造方法,这样称呼的原因马上就会清楚。 外部构造方法只能通过调用其他构造方法来创建新实例,比如自动提供的默认构造方法。
内部构造方法
尽管外部构造方法成功地为构造对象提供了额外的便利,它无法解决另外两个在本章导言里提到的另外 两种用例:确保不变性和允许创建引用自身的对象。因此,我们需要 内部 构造方法。内部构造方法 和外部构造方法很相像,但有两点不同:
- 内部构造方法在类型声明内部声明,而不是和普通方法一样在外部。
内部构造方法能够访问一个特殊的局部存在的函数, 称为 new ,这个函数能够创建该类型的对象。
例如, 假设你要声明一个保存一对实数的类型,但要约束第一个数不大于第二个数。你可以像这样声明它 :
julia> struct OrderedPair
x::Real
y::Real
OrderedPair(x,y) = x > y ? error("out of order") : new(x,y)
end
现在 OrderedPair 对象只能在 x <= y 时构造:
julia> OrderedPair(1, 2)
OrderedPair(1, 2)
julia> OrderedPair(2,1)
ERROR: out of order
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] OrderedPair(::Int64, ::Int64) at ./none:4
[3] top-level scope
如果类型被声明为 mutable,你就能获取并直接修改字段的值来破坏不变性,但不请自来弄乱 对象内部被认为是不好的形式。你 (或者其他人)可以在以后任何时候提供附加的外部构造方法, 但一旦一个类型已经被声明了,就没有办法来添加更多的内部构造方法了。因为外部构造方法只能通过 调用其他的构造方法来构造创建对象,所以最终构造对象的一定是某个内部构造函数。这保证了 声明过的类型的所有对象必须通过调用随类型提供的内部构造方法之一而存在,从而在某种程度上 保证了类型的不变性。
只要定义了任何一个内部构造方法,就不会再提供默认的构造方法:Julia 假定你已经为自己 提供了所需的所有内部构造方法。默认构造方法等效于一个你自己编写的内部构造函数方法, 该方法将所有对象的字段作为参数(如果相应的字段具有类型,则约束为正确的类型), 并将它们传递给 new,返回结果对象:
julia> struct Foo
bar
baz
Foo(bar,baz) = new(bar,baz)
end
这个声明与前面没有显式内部构造方法的 Foo 类型的定义效果相同。 以下两个类型是等价的 – 一个具有默认构造方法,另一个具有显式构造方法:
julia> struct T1
x::Int64
end
julia> struct T2
x::Int64
T2(x) = new(x)
end
julia> T1(1)
T1(1)
julia> T2(1)
T2(1)
julia> T1(1.0)
T1(1)
julia> T2(1.0)
T2(1)
提供尽可能少的内部构造函数方法被认为是一种很好的形式:只有那些显式地接受所有参数并执行必要的错误检查和转换的构造函数。提供默认值或辅助转换的额外方便构造函数方法应该作为外部构造函数,调用内部构造函数来进行重升。这种分离通常是很自然的。
不完全初始化(Incomplete Initialization)
最后一个尚未解决的问题是自引用对象的构造,或者更广泛地说,是递归数据结构。由于根本的困难可能不是很明显,让我们简要解释一下。请考虑下面的递归类型声明:
julia> mutable struct SelfReferential
obj::SelfReferential
end
在考虑如何构造一个实例之前,这种类型可能是无害的。如果一个实例是Selfreferential的实例,那么可以通过调用创建第二个实例:
julia> b = SelfReferential(a)
但是,当一个实例不存在作为其obj字段的有效值时,如何构造第一个实例?唯一的解决方案是允许使用未赋值的obj字段创建一个不完全初始化的自定义实例,并将该不完整的实例用作另一个实例的obj字段的有效值,例如,它本身。
为了允许创建未完全初始化的对象,Julia允许调用新函数的字段数少于该类型所具有的字段数,返回一个具有未初始化的未指定字段的对象。然后,内部构造函数方法可以使用不完整的对象,在返回之前完成它的初始化。例如,在定义自交类型时,我们又遇到了另一个漏洞,即一个零参数内部构造函数返回实例,其中包含指向自己的obj字段:
julia> mutable struct SelfReferential
obj::SelfReferential
SelfReferential() = (x = new(); x.obj = x)
end
我们可以验证此构造函数是否工作并构造了实际上是自引用的对象:
julia> x = SelfReferential();
julia> x === x
true
julia> x === x.obj
true
julia> x === x.obj.obj
true
虽然从内部构造函数返回完全初始化的对象通常是一个好主意,但不完全初始化的对象可以返回:
julia> mutable struct Incomplete
xx
Incomplete() = new()
end
julia> z = Incomplete();
虽然允许您创建具有未初始化字段的对象,但对未初始化引用的任何访问都是一个即时错误:
julia> z.xx
ERROR: UndefRefError: access to undefined reference
这就避免了不断检查空值的需要。然而,并不是所有的对象字段都是引用的。Julia认为有些类型是“普通数据”,这意味着它们的所有数据都是自包含的,并且不引用其他对象。普通数据类型由原语类型(例如int)和其他普通数据类型的不可变结构组成。普通数据类型的初始内容是未定义的:
julia> struct HasPlain
n::Int
HasPlain() = new()
end
julia> HasPlain()
HasPlain(438103441441)
普通数据类型数组具有相同的行为。
您可以将不完整的对象从内部构造函数传递给其他函数,以委派它们的完成:
julia> mutable struct Lazy
xx
Lazy(v) = complete_me(new(), v)
end
与从构造函数返回的不完整对象一样,如果Compleme或其调用的任何调用在初始化延迟对象之前试图访问其xx字段,则会立即引发错误。
参数构造器(Parametric Constructors)
参数类型给构造函数的故事增加了一些皱纹。回想一下参数类型,默认情况下,参数组合类型的实例可以使用显式给定的类型参数或构造器参数类型所隐含的类型参数来构造。下面是一些示例:
julia> struct Point{T<:Real}
x::T
y::T
end
julia> Point(1,2) ## implicit T ##
Point{Int64}(1, 2)
julia> Point(1.0,2.5) ## implicit T ##
Point{Float64}(1.0, 2.5)
julia> Point(1,2.5) ## implicit T ##
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
Closest candidates are:
Point(::T<:Real, ::T<:Real) where T<:Real at none:2
julia> Point{Int64}(1, 2) ## explicit T ##
Point{Int64}(1, 2)
julia> Point{Int64}(1.0,2.5) ## explicit T ##
ERROR: InexactError: Int64(Int64, 2.5)
Stacktrace:
[...]
julia> Point{Float64}(1.0, 2.5) ## explicit T ##
Point{Float64}(1.0, 2.5)
julia> Point{Float64}(1,2) ## explicit T ##
Point{Float64}(1.0, 2.0)
如您所见,对于具有显式类型参数的构造函数调用,参数被转换为隐含字段类型:Point{int 64}(1,2)有效,但Point{int 64}(1.0,2.5)在将2.5转换为int 64时会引发不精确的恐惧。具有匹配类型的参数可以提供给泛型点构造函数。
实际上,点{t}是每个类型T的不同构造函数。实际上,点{t}是每个类型T的不同构造函数,没有任何显式提供的内部构造函数,复合类型点{t<:Real}的声明自动提供了一个内部构造函数点{t},对于每种可能的类型t<:true,它的行为就像非参数默认的内部构造函数那样。它还提供了一个普通的外部点构造函数。这需要一对实数,这些参数必须是同一类型的。这种自动提供的构造函数相当于以下显式声明:
julia> struct Point{T<:Real}
x::T
y::T
Point{T}(x,y) where {T<:Real} = new(x,y)
end
julia> Point(x::T, y::T) where {T<:Real} = Point{T}(x,y);
请注意,每个定义看起来都像它处理的构造函数调用的形式。调用Point{int 64}(1,2)将在结构块内调用定义Point{t}(x,y)。而外部构造函数声明则为通用的点构造函数定义了一个方法,该方法只适用于同一实类型的值对。该声明使构造函数调用没有显式类型参数,例如Point(1,2)和Point(1.0,2.5),因为这样做是有效的。方法声明将参数限制为相同类型的参数,调用点(1,2.5),具有不同类型的参数,从而导致“无方法”错误。
假设我们希望通过将整数值1“提升”到浮点值1.0来使构造函数调用Point(1,2.5)工作。实现这一目标的最简单的方法是定义以下附加的外部构造函数方法:
julia> Point(x::Int64, y::Float64) = Point(convert(Float64,x),y);
该方法使用convert函数显式地将x转换为float64,然后在两个参数都是float64的情况下将构造委托给通用构造函数。用这个方法定义,以前的MeoDoRror现在成功地创建了一个类型点的点{FLUAT64 }:
julia> Point(1,2.5)
Point{Float64}(1.0, 2.5)
julia> typeof(ans)
Point{Float64}
然而,其他类似的调用仍然不起作用:
julia> Point(1.5,2)
ERROR: MethodError: no method matching Point(::Float64, ::Int64)
Closest candidates are:
Point(::T<:Real, !Matched::T<:Real) where T<:Real at none:1
更一般的方法是让所有这些呼叫都能正常工作,看看转换和升级。冒着破坏挂起的风险,我们可以在这里揭示,只需要以下外部方法定义来使对通用点构造函数的所有调用按预期工作:
julia> Point(x::Real, y::Real) = Point(promote(x,y)...);
Process函数将其所有参数转换为公共类型-在本例中为Float 64。使用此方法定义,点构造函数按照数值运算符喜欢的方式提升其参数,并适用于所有类型的实数:
julia> Point(1.5,2)
Point{Float64}(1.5, 2.0)
julia> Point(1,1//2)
Point{Rational{Int64}}(1//1, 1//2)
julia> Point(1.0,1//2)
Point{Float64}(1.0, 0.5)
因此,虽然默认情况下在Julia中提供的隐式类型参数构造函数是相当严格的,但可以使它们以一种更轻松但更明智的方式运行。此外,由于构造函数可以利用类型系统、方法和多重分派的所有功能,因此定义复杂的行为通常非常简单。
案例研究:理性(Case Study: Rational)
也许将所有这些片段结合在一起的最好方法是提供一个参数组合类型及其构造方法的真实示例。为此,我们实现了自己的RationalNumber类型ourRational,类似于Julia的内置Rational类型,在rital.jl中定义:
julia> struct OurRational{T<:Integer} <: Real
num::T
den::T
function OurRational{T}(num::T, den::T) where T<:Integer
if num == 0 && den == 0
error("invalid rational: 0//0")
end
g = gcd(den, num)
num = div(num, g)
den = div(den, g)
new(num, den)
end
end
julia> OurRational(n::T, d::T) where {T<:Integer} = OurRational{T}(n,d)
OurRational
julia> OurRational(n::Integer, d::Integer) = OurRational(promote(n,d)...)
OurRational
julia> OurRational(n::Integer) = OurRational(n,one(n))
OurRational
julia> ⊘(n::Integer, d::Integer) = OurRational(n,d)
⊘ (generic function with 1 method)
julia> ⊘(x::OurRational, y::Integer) = x.num ⊘ (x.den*y)
⊘ (generic function with 2 methods)
julia> ⊘(x::Integer, y::OurRational) = (x*y.den) ⊘ y.num
⊘ (generic function with 3 methods)
julia> ⊘(x::Complex, y::Real) = complex(real(x) ⊘ y, imag(x) ⊘ y)
⊘ (generic function with 4 methods)
julia> ⊘(x::Real, y::Complex) = (x*y') ⊘ real(y*y')
⊘ (generic function with 5 methods)
julia> function ⊘(x::Complex, y::Complex)
xy = x*y'
yy = real(y*y')
complex(real(xy) ⊘ yy, imag(xy) ⊘ yy)
end
⊘ (generic function with 6 methods)
第一行-structourRational{t<:整型}<:true-声明我们的Rational接受整数类型的一个类型参数,并且本身是一个实类型。字段声明num:t和den:t表示保存在我们的Rational{t}对象中的数据是一对t类型的整数,一个表示Rational值的数字,另一个表示它的分母。
现在事情变得有趣了。我们的Rational有一个内部构造器方法,它检查num和den都不是零,并确保每个Rational都用一个非负分母的“最低项”构造。这是通过将给定的分子和分母值除以它们的最大公因子来实现的,这是使用gcd函数计算的。因为gcd返回其参数的最大公共因子与符号匹配。第一个参数(这里的DEN),在这种划分之后,DEN的新值肯定是非否定的。因为这是我们Rational的唯一内部构造器,所以我们可以确定我们的Rational对象总是以这种规范化的形式构造的。
为方便起见,我们还提供了几种外部构造函数方法。第一种是“标准”一般构造函数,当分子和分母的类型相同时,它从分子和分母的类型推断类型参数t。第二种方法适用于给定的分子和分母值有不同类型的情况:它将它们提升为公共类型,然后将构造委托给外部构造函数以获取匹配类型的参数。第三,外部构造函数通过提供1作为分母值,将整数值转换为理性主义。
按照外部构造函数定义,我们为⊘操作符定义了许多方法,它为编写理性主义提供了语法(例如1⊘2)。朱莉娅的Rational类型为此使用/运算符。在这些定义之前,⊘是一个完全未定义的操作符,只具有语法和意义。之后,它的行为就像用有理数描述的一样-它的整个行为都被定义了。在这几行中,第一个也是最基本的定义就是在a和b是整数的情况下,将我们的Rational构造函数应用到a和b上,从而构造一个⊘b。当⊘的一个操作数已经是有理数时,我们为结果的比率构造一个新的Rational;这种行为实际上与有理数的除法相同。最后,
julia> z = (1 + 2im) ⊘ (1 - 2im);
julia> typeof(z)
Complex{OurRational{Int64}}
julia> typeof(z) <: Complex{OurRational}
false
因此,尽管⊘操作符通常返回一个ourRational实例,如果它的任何一个参数都是复整数,它将返回一个复杂{ourRational}的实例,而不是。感兴趣的读者应该考虑仔细阅读Rational.jl的其余部分:它是短的,自含的,并且实现了一个完整的基本Julia类型。
外部构造函数(Outer-only constructors)
正如我们已经看到的,典型的参数类型有内部构造函数,当类型参数已知时调用;例如,它们适用于点{int},但不适用于点。另外,可以添加自动确定类型参数的外部构造函数,例如从调用点(1,2)构造点{int}。外部构造函数调用内部构造函数来完成创建实例的核心工作。然而,在某些情况下,不希望这样做。提供内部构造函数,以便不能手动请求特定类型的参数。
例如,假设我们定义了一种类型,它存储一个向量以及它的和的精确表示:
julia> struct SummedArray{T<:Number,S<:Number}
data::Vector{T}
sum::S
end
julia> SummedArray(Int32[1; 2; 3], Int32(6))
SummedArray{Int32,Int32}(Int32[1, 2, 3], 6)
问题是我们希望s是一个比t更大的类型,这样我们就可以用较少的信息损失来和许多元素。例如,当t是int 32时,我们希望s是int 64。因此,我们希望避免一个允许用户构造类型summedArray{int 32,int 32}的实例的接口。一种方法是只为summedArray提供一个构造函数,但是在structDefinition块中,以抑制默认构造函数的生成:
julia> struct SummedArray{T<:Number,S<:Number}
data::Vector{T}
sum::S
function SummedArray(a::Vector{T}) where T
S = widen(T)
new{T,S}(a, sum(S, a))
end
end
julia> SummedArray(Int32[1; 2; 3], Int32(6))
ERROR: MethodError: no method matching SummedArray(::Array{Int32,1}, ::Int32)
Closest candidates are:
SummedArray(::Array{T,1}) where T at none:5
此构造函数将由语法summedArray(A)调用。语法新{t,s}允许为要构造的类型指定参数,即此调用将返回一个和数组{t,s}。任何构造函数定义都可以使用新的{t,s},但为了方便,新{}的参数将在可能时从正在构造的类型中自动导出。
完结于 2018-08–31 21:12