Julia(建设者)

2021-04-14 13:38:43 浏览数 (3)

建设者

构造函数[1]是创建新对象的函数,特别是Composite Types的实例。在Julia中,类型对象还充当构造函数:它们在作为参数应用于元组时会创建自己的新实例。引入复合类型时,已经简要提到了这一点。例如:

代码语言:javascript复制
julia> struct Foo
           bar
           baz
       end

julia> foo = Foo(1, 2)
Foo(1, 2)

julia> foo.bar
1

julia> foo.baz
2

对于许多类型,创建实例只需通过将其字段值绑定在一起来形成新对象。但是,在某些情况下,创建复合对象时需要更多功能。有时必须通过检查参数或对其进行转换来强制执行不变量。递归数据结构,尤其是那些可能是自引用的数据结构,通常必须先以不完整的状态创建,然后以编程方式更改为完整的结构,才能与对象创建分开的一个步骤来进行干净的构造。有时,使用比字段少或少的参数类型构造对象是很方便的。Julia的对象构造系统可以解决所有这些情况,甚至更多。

[1]

命名法:虽然术语“构造函数”通常是指构造一个类型的对象的整个函数,但通常会略微滥用术语,并将特定的构造方法称为“构造函数”。在这种情况下,从上下文中通常可以清楚地看到,该术语用于表示“构造函数方法”而不是“构造函数”,特别是因为它通常用于从所有函数中选出构造函数的特定方法的意义。其他。

外部构造方法

构造器与Julia中的其他任何函数一样,其总体行为由其方法的组合行为定义。因此,您可以通过简单地定义新方法来向构造函数添加功能。例如,假设您要为仅添加Foo一个参数并为barbaz字段使用给定值的对象添加构造函数方法。这很简单:

代码语言:javascript复制
julia> Foo(x) = Foo(x,x)
Foo

julia> Foo(1)
Foo(1, 1)

您还可以添加一个零参数Foo构造函数方法,该方法为barbaz字段提供默认值:

代码语言:javascript复制
julia> Foo() = Foo(0)
Foo

julia> Foo()
Foo(0, 0)

此处,零参数构造函数方法调用单参数构造函数方法,后者又调用自动提供的两参数构造函数方法。由于很快就会明白的原因,像这样的声明为普通方法的其他构造方法称为外部构造方法。外部构造器方法只能通过调用另一个构造器方法(例如自动提供的默认方法)来创建新实例。

内部构造方法

尽管外部构造方法成功解决了提供额外的便利方法来构造对象的问题,但它们未能解决本章引言中提到的其他两个用例:强制不变式,并允许构造自引用对象。对于这些问题,需要内部构造方法。内部构造方法非常类似于外部构造方法,但有两个区别:

  1. 它是在类型声明的块内部声明的,而不是像普通方法在其外部声明的那样。
  2. 它可以访问一个特殊的本地存在的函数new,该函数创建块类型的对象。

例如,假设一个人想要声明一个包含一对实数的类型,但要遵守第一个数字不大于第二个数字的约束。可以这样声明:

代码语言:javascript复制
julia> struct OrderedPair
           x::Real
           y::Real
           OrderedPair(x,y) = x > y ? error("out of order") : new(x,y)
       end

现在OrderedPair只能按照以下方式构造对象x <= y

代码语言:javascript复制
julia> OrderedPair(1, 2)
OrderedPair(1, 2)

julia> OrderedPair(2,1)
ERROR: out of order
Stacktrace:
 [1] OrderedPair(::Int64, ::Int64) at ./none:4

如果声明了类型mutable,则可以进入并直接更改字段值以违反此不变式,但是不请自来处理对象的内部结构被认为是较差的形式。您(或其他人)也可以在以后的任何时候提供其他外部构造函数方法,但是一旦声明了类型,就无法添加更多内部构造函数方法。由于外部构造函数方法只能通过调用其他构造函数方法来创建对象,因此最终必须调用某些内部构造函数来创建对象。这保证了必须通过调用随该类型提供的内部构造方法之一来实现已声明类型的所有对象,从而在某种程度上强制了类型的不变量。

如果定义了任何内部构造函数方法,则不会提供默认的构造函数方法:假定您已为自己提供了所需的所有内部构造函数。默认构造函数等效于编写自己的内部构造函数方法,该方法将对象的所有字段作为参数(如果对应的字段具有类型,则约束为正确的类型),并将它们传递给new,返回结果对象:

代码语言:javascript复制
julia> struct Foo
           bar
           baz
           Foo(bar,baz) = new(bar,baz)
       end

该声明与Foo不带显式内部构造方法的早期类型定义具有相同的作用。以下两种类型是等效的-一种具有默认构造函数,另一种具有显式构造函数:

代码语言:javascript复制
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)

最好提供尽可能少的内部构造方法:仅那些显式接受所有参数并强制进行基本错误检查和转换的方法。应提供提供默认值或辅助转换的其他便捷构造函数方法,作为调用内部构造函数进行繁重工作的外部构造函数。这种分离通常是很自然的。

不完整的初始化

最后一个尚未解决的问题是构造自引用对象,或更一般地说,构造递归数据结构。由于基本困难可能不会立即显现,所以让我们简要地解释一下。考虑以下递归类型声明:

代码语言:javascript复制
julia> mutable struct SelfReferential
           obj::SelfReferential
       end

在人们考虑如何构造它的实例之前,这种类型可能看起来足够无害。如果a是的实例SelfReferential,则可以通过调用创建第二个实例:

代码语言:javascript复制
julia> b = SelfReferential(a)

但是,当没有实例提供其obj字段的有效值时,该如何构造第一个实例?唯一的解决方案是允许创建SelfReferential带有未分配obj字段的未完全初始化的实例,并将该不完整的实例用作obj另一个实例(例如其自身)的字段的有效值。

为了允许创建未完全初始化的对象,Julia允许new使用少于类型具有的字段数的函数来调用该函数,并返回未初始化未指定字段的对象。然后,内部构造函数方法可以使用不完整的对象,在返回之前完成其初始化。例如,在这里,我们在定义SelfReferential类型时遇到了另一个难题,使用零参数内部构造函数返回实例,该实例具有obj指向自身的字段:

代码语言:javascript复制
julia> mutable struct SelfReferential
           obj::SelfReferential
           SelfReferential() = (x = new(); x.obj = x)
       end

我们可以验证此构造函数可以工作并构造实际上是自引用的对象:

代码语言:javascript复制
julia> x = SelfReferential();

julia> x === x
true

julia> x === x.obj
true

julia> x === x.obj.obj
true

尽管从内部构造函数返回完全初始化的对象通常是一个好主意,但是可以返回未完全初始化的对象:

代码语言:javascript复制
julia> mutable struct Incomplete
           xx
           Incomplete() = new()
       end

julia> z = Incomplete();

虽然允许您使用未初始化的字段创建对象,但是对未初始化引用的任何访问都是一个立即错误:

代码语言:javascript复制
julia> z.xx
ERROR: UndefRefError: access to undefined reference

这避免了需要连续检查null值。但是,并非所有对象字段都是引用。Julia认为某些类型是“普通数据”,这意味着它们的所有数据都是自包含的,不引用其他对象。普通数据类型由基本类型(例如Int)和其他普通数据类型的不可变结构组成。普通数据类型的初始内容是不确定的:

代码语言:javascript复制
julia> struct HasPlain
           n::Int
           HasPlain() = new()
       end

julia> HasPlain()
HasPlain(438103441441)

普通数据类型的数组表现出相同的行为。

您可以将不完整的对象从内部构造函数传递给其他函数,以委托其完成:

代码语言:javascript复制
julia> mutable struct Lazy
           xx
           Lazy(v) = complete_me(new(), v)
       end

与从构造函数返回的不完整对象一样,如果对象的complete_me任何一个或任何一个被调用者在初始化之前尝试访问xxLazy对象的字段,则将立即引发错误。

参数构造器

参数类型为构造函数增加了一些麻烦。从参数类型回想起,默认情况下,可以使用显式给定的类型参数或给定构造函数的参数类型所隐含的类型参数来构造参数复合类型的实例。这里有些例子:

代码语言:javascript复制
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, !Matched::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()
Stacktrace:
 [1] convert(::Type{Int64}, ::Float64) at ./float.jl:679
 [2] Point{Int64}(::Float64, ::Float64) at ./none:2

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{Int64}(1,2)工作,但Point{Int64}(1.0,2.5)提出了一个InexactError转换时2.5Int64。当构造函数调用的参数隐含类型时,如中所述Point(1,2),则参数的类型必须一致(否则T无法确定),但是可以将具有匹配类型的任何一对实参提供给通用Point构造函数。

真正发生的是PointPoint{Float64}并且Point{Int64}都是不同的构造函数。实际上,Point{T}每种类型都有一个独特的构造函数T。如果没有任何显式提供的内部构造函数,则复合类型的声明会Point{T<:Real}自动Point{T}为每个可能的类型提供一个内部构造函数T<:Real,其行为类似于非参数默认内部构造函数。它还提供了一个单一的通用外部Point构造函数,该构造函数接受成对的实参,并且该实参必须具有相同的类型。这种自动提供的构造函数等效于以下显式声明:

代码语言:javascript复制
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{Int64}(1,2)Point{T}(x,y)type块内调用定义。另一方面,外部构造函数声明为通用Point构造函数定义了一个方法,该方法仅适用于相同实型值对。此声明使没有显式类型参数(如Point(1,2)和)的构造函数调用Point(1.0,2.5)起作用。由于方法声明将参数限制为相同Point(1,2.5)类型,因此使用不同类型的参数进行的like之类的调用会导致“无方法”错误。

假设我们想Point(1,2.5)通过将整数值“提升” 1为浮点值来使构造函数调用起作用1.0。实现此目的的最简单方法是定义以下其他外部构造方法:

代码语言:javascript复制
julia> Point(x::Int64, y::Float64) = Point(convert(Float64,x),y);

如果两个参数都为,则此方法使用该convert()函数显式转换xFloat64,然后将构造委托给常规构造函数Float64。通过此方法定义,以前MethodError成功的现在可以成功创建类型的点Point{Float64}

代码语言:javascript复制
julia> Point(1,2.5)
Point{Float64}(1.0, 2.5)

julia> typeof(ans)
Point{Float64}

但是,其他类似的调用仍然不起作用:

代码语言:javascript复制
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

有关使所有此类调用合理运行的更一般的方法,请参见转化和升级。冒着破坏悬念的危险,我们可以在这里揭示出,只需进行以下外部方法定义即可使对通用Point构造函数的所有调用均按预期进行:

代码语言:javascript复制
julia> Point(x::Real, y::Real) = Point(promote(x,y)...);

promote函数将其所有参数转换为通用类型-在这种情况下为Float64。使用此方法定义,Point构造函数以与数字运算符相同的方式提升其参数 ,并适用于各种实数:

代码语言:javascript复制
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中默认提供的隐式类型参数构造函数相当严格,但有可能使它们以更轻松但更明智的方式轻松地表现。而且,由于构造函数可以利用类型系统,方法和多重调度的所有功能,因此定义复杂的行为通常非常简单。

案例研究:理性

将所有这些部分联系在一起的最好方法可能是提供一个真实的参数复合类型及其构造方法的示例。为此,这是的(略有修改)开头rational.jl,它实现了Julia的有理数:

代码语言:javascript复制
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)

第一行– struct OurRational{T<:Integer} <: Real–声明OurRational采用整数类型的一个类型参数,并且其本身是实型。字段声明num::Tden::T指示OurRational{T}对象中保存的数据是一对类型为的整数T,一个代表有理值的分子,另一个代表其分母。

现在事情变得有趣了。OurRational有一个内部构造函数方法,该方法检查numden都不为零,并确保每个有理数都使用非负分母以“最低项”构造。这是通过将给定的分子和分母值除以使用该gcd函数计算出的最大公除数来实现的。由于gcd返回其参数的最大公约数,且符号与第一个参数匹配(den此处),因此在该除法之后,den可以确保的新值是非负的。因为这是的唯一内部构造函数OurRational,所以我们可以确定OurRational对象始终以这种标准化形式构造。

OurRational为了方便起见,还提供了几种外部构造方法。第一个是“标准”通用构造函数,T当它们具有相同的类型时,它们将从分子和分母的类型推断出类型参数。第二种适用于给定的分子和分母值具有不同类型的情况:它将它们提升为公共类型,然后将构造委托给外部构造函数以获取匹配类型的参数。第三个外部构造函数通过提供值1作为分母,将整数值转换为有理数。

遵循外部构造函数的定义,我们为//运算符提供了许多方法,这些方法提供了用于编写有理数的语法。在这些定义之前,//是一个完全未定义的运算符,仅包含语法,没有意义。之后,它的行为就与Rational Numbers中描述的一样-它的整个行为在以下几行中定义。第一个也是最基本的定义a//bOurRational通过将OurRational构造函数应用于ab当它们是整数时来构造a 。当的操作数之一//已经是一个有理数时,我们为所得比率构建稍微不同的新有理;这种行为实际上与有理数与整数的除法相同。最后,申请//复数的整数值创建了一个实例Complex{OurRational}–一个复数,其实部和虚部都是有理数:

代码语言:javascript复制
julia> ans = (1   2im)//(1 - 2im);

julia> typeof(ans)
Complex{OurRational{Int64}}

julia> ans <: Complex{OurRational}
false

因此,尽管//运算符通常返回的实例OurRational,但是如果其参数之一是复数整数,它将返回的实例Complex{OurRational}。有兴趣的读者应考虑仔细阅读以下内容rational.jl:它简短,自包含,并实现了整个基本的Julia类型。

构造函数与转换

T(args...)Julia中的构造函数的实现与其他可调用对象一样:方法被添加到它们的类型中。类型的类型是Type,因此所有构造函数方法都存储在该Type类型的方法表中。这意味着您可以通过显式定义适当类型的方法来声明更灵活的构造函数,例如,抽象类型的构造函数。

但是,在某些情况下,您可以考虑向中添加方法Base.convert 而不是定义构造函数,因为convert()如果找不到匹配的构造函数,Julia将退回到调用。例如,如果不T(args...) = ...存在构造函数,Base.convert(::Type{T}, args...) = ...则调用该方法。

convert每当需要将一种类型转换为另一种类型时(例如在赋值ccall,等),Julia便会广泛使用该术语,并且通常仅在无损转换时才定义(或成功)。例如,convert(Int, 3.0)产生3,但convert(Int, 3.2)抛出InexactError。如果要为从一种类型到另一种类型的无损转换定义构造函数,则可能应该定义一个convert方法。

另一方面,如果构造函数不表示无损转换,或者根本不表示“转换”,则最好将其保留为构造函数而不是convert方法。例如,该Array{Int}()构造函数创建一个零维Array的类型Int,但是不是真的从一个“转换” IntArray

仅外部构造函数

如我们所见,典型的参数类型具有内部构造函数,这些构造函数在已知类型参数时被调用;例如它们适用于Point{Int}但不适用于Point。(可选)可以添加自动确定类型参数的外部构造函数,例如,Point{Int}从call 构造a Point(1,2)。外部构造函数调用内部构造函数来完成创建实例的核心工作。但是,在某些情况下,宁愿不提供内部构造函数,因此无法手动请求特定类型的参数。

例如,假设我们定义了一种类型,该类型存储矢量以及其和的精确表示:

代码语言:javascript复制
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,以便我们可以求和很多元素而信息损失更少。例如,当Tis时Int32,我们希望S成为Int64。因此,我们要避免允许用户构造类型实例的接口SummedArray{Int32,Int32}。一种实现方法是仅为提供一个构造函数SummedArray,但在type定义块内部以禁止生成默认构造函数:

代码语言:javascript复制
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)。该语法new{T,S}允许为要构造的类型指定参数,即此调用将返回SummedArray{T,S}new{T,S}可以在任何构造函数定义中使用,但为方便起见,new{}在可能的情况下,要自动从要构造的类型派生参数。

0 人点赞