Engee 文档

构造函数

构造函数脚注:1[虽然术语"构造函数"通常意味着创建某种类型的对象的整个函数,但它往往略微偏离了对该术语的这种严格理解,并且指的是特定的构造 在这种情况下,通常从上下文中可以明显看出构造函数方法的含义,而不是构造函数,特别是当某个构造函数与其他构造函数相反突出显示时。]--这些是创建新对象的函数,特别是实例 复合类型。 在Julia中,类型对象也充当构造函数:当作为函数应用于参数元组时,它们会创建自己的实例。 当我们第一次熟悉复合类型时,已经简单地提到了这一点。 例如:

julia> struct Foo
           bar
           baz
       end

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

julia> foo.bar
1

julia> foo.baz
2

绑定字段值足以创建许多类型的实例。 但是,在某些情况下,创建复合对象时需要附加功能。 有时需要通过检查或转换参数来应用不变量。 递归数据结构,特别是那些可以自引用的数据结构,往往不能直接创建:它们必须首先在不完整的状态下创建,然后在单独的动作中以编程方式修改,使其 有时在不指定所有字段或使用其他类型的情况下创建对象只是很方便。 Julia的对象创建系统支持所有这些和其他场景。

外部构造函数方法

构造函数与Julia中的任何其他函数类似,因为它的行为通常由其方法的累积行为决定。 因此,要扩展构造函数的功能,定义新方法就足够了。 假设您想为`Foo`对象添加一个构造函数方法,该方法只接受一个参数,并为`bar`字段和`baz`字段使用提供的值。 这样做并不难。

julia> Foo(x) = Foo(x,x)
Foo

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

您还可以为`Foo`添加不带参数的构造函数方法,它为`bar`和`baz`字段提供默认值。:

julia> Foo() = Foo(0)
Foo

julia> Foo()
Foo(0, 0)

没有参数的构造函数的此方法调用具有一个参数的构造函数的方法,而该参数又调用具有两个参数的自动提供的方法。 由于很快就会变得清晰的原因,声明为常规方法的附加构造函数方法(如在本例中)被称为external_。 外部构造函数方法只能通过调用另一个构造函数方法来创建实例,例如自动提供的构造函数方法。

构造函数的内部方法

虽然构造函数的外部方法成功地应对了在创建对象时提供额外便利的任务,但它们不允许解决本章介绍部分中提到的另外两个任务:应用不变量和创 要实现这些任务,您需要构造函数的内部方法。 构造函数的内部方法类似于外部方法,但它有两个区别。

  1. 它在类型声明块内部声明,而不是在它外部声明,就像常规方法一样。

  2. 它可以访问一个名为 `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

如果类型被声明为可变,则可以直接更改字段的值以打破此固定条件。 但是,当然,不建议干扰对象的内部结构。 您(或其他人)可以稍后定义额外的外部构造函数方法,但在声明类型后,您不能再添加内部构造函数方法。 由于外部构造函数方法只能通过调用其他构造函数方法来创建对象,因此最终必须调用某些内部构造函数方法才能创建对象。 因此,可以保证声明类型的任何对象只能作为调用类型构造函数的内部方法之一的结果而创建,这确保了在某种程度上应用类型不变量。

如果至少定义了一个内部构造函数方法,则不提供默认构造函数方法:假定您已提供了所有必要的功能。 默认构造函数类似于构造函数自己的内部方法,它将对象的所有字段作为参数(如果相应字段具有类型,则必须是正确的类型),将它们传递给`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

朱莉娅>T1(1)
T1(1)

朱莉娅>T2(1)
T2(1)

朱莉娅>T1(1.0)
T1(1)

朱莉娅>T2(1.0)
T2(1)

建议提供尽可能少的内部构造函数方法,即仅提供那些显式接受所有参数并对错误和转换执行必要检查的方法。 提供默认值或执行额外转换的辅助构造函数方法应实现为调用内部构造函数来完成主要工作的外部构造函数。 这样的划分通常是很自然的。

初始化不完整

最后一个有待解决的任务是创建自引用对象,或者更一般地说,递归数据结构。 由于主要的困难可能并不明显,让我们来看一下。 让我们采取以下递归类型更新。

julia> mutable struct SelfReferential
           obj::SelfReferential
       end

在创建它的实例之前,这种类型可能看起来不起眼。 如果`a`是’SelfReferential’的实例,则可以使用以下调用创建第二个实例:

julia> b = SelfReferential(a)

但是,如果还没有可以作为`obj`字段的值传递的实例,如何创建第一个实例? 唯一的解决方案是允许使用未指定的`obj`字段创建`SelfReferential`的未完全初始化实例,并将此不完整实例用作另一个或同一实例的`obj`字段的值,即本身。

要创建未完全初始化的对象,Julia支持调用函数 new'字段比类型少。 对于返回的对象,不会初始化未设置的字段。 然后,构造函数的内部方法可以使用此不完整对象在返回之前完成其初始化。 例如,这里是另一个尝试定义类型’SelfReferential,这次使用没有参数的内部构造函数,它返回一个实例,其中`obj’字段指向自己。:

julia> mutable struct SelfReferential
           obj::SelfReferential
           SelfReferential() = (x = new(); x.obj = x)
       end

我们可以确保这个构造函数工作并创建自引用的对象。:

julia> x = SelfReferential();

朱莉娅>x===x
真的

julia>x===x.obj
真的

julia>x===x.obj。反对
真的

虽然通常希望从内部构造函数返回完全初始化的对象,但可以返回未完全初始化的对象。:

julia> mutable struct Incomplete
           data
           Incomplete() = new()
       end

julia> z = Incomplete();

虽然可以使用未初始化的字段创建对象,但尝试访问未初始化的引用会立即导致错误。:

julia> z.data
ERROR: UndefRefError: access to undefined reference

这消除了不断检查空值的需要。 但是,并非对象的所有字段都是链接。 Julia中的某些类型被认为是"简单"的。 这意味着它们包含独立的数据,不引用其他对象。 简单数据类型包括原始类型(例如,'Int'`和由其他简单类型组成的不可变结构(另请参阅: '轨道', 'isbitstype')。 最初,简单数据类型的内容是未定义的:

julia> struct HasPlain
           n::Int
           HasPlain() = new()
       end

julia> HasPlain()
HasPlain(438103441441)

简单数据类型的数组的行为方式相同。

您可以将不完整的对象从内部构造函数传递给应该完成其创建的其他函数。:

julia> mutable struct Lazy
           data
           Lazy(v) = complete_me(new(), v)
       end

就像构造函数返回的不完整对象的情况一样,如果"complete_me"函数或它调用的任何函数试图访问尚未初始化的"Lazy"对象的"data"字段,则会立即发生错误。

参数构造函数

参数类型为设计方案增加了许多细微差别。 正如你从章节中记得的那样 参数类型,默认情况下,参数复合类型的实例可以使用显式指定的类型参数创建,也可以使用从传递给构造函数的参数类型派生的类型参数创建。 以下是一些例子:

julia> struct Point{T<:Real}
           x::T
           y::T
       end

julia> Point(1,2) ## неявный тип T ##
Point{Int64}(1, 2)

julia> Point(1.0,2.5) ## неявный тип T ##
Point{Float64}(1.0, 2.5)

julia> Point(1,2.5) ## неявный тип T ##
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
The type `Point` exists, but no method is defined for this combination of argument types when trying to construct it.

Closest candidates are:
  Point(::T, ::T) where T<:Real at none:2

julia> Point{Int64}(1, 2) ## явный тип T ##
Point{Int64}(1, 2)

julia> Point{Int64}(1.0,2.5) ## явный тип T ##
ERROR: InexactError: Int64(2.5)
Stacktrace:
[...]

julia> Point{Float64}(1.0, 2.5) ## явный тип T ##
Point{Float64}(1.0, 2.5)

julia> Point{Float64}(1,2) ## явный тип T ##
Point{Float64}(1.0, 2.0)

如您所见,当使用显式指定的类型参数调用构造函数时,参数将转换为隐含的字段类型:'Point'调用。{Int64}(1,2)'工作,但'点{Int64}(1.0,2.5)转换类型中的值`2.5`时 'Int64'会导致错误 'InexactError'。 如果类型是从构造函数调用的参数派生的,如`Point(1,2),则参数的类型必须一致,否则无法确定类型`T`。 但是,任何一对适当类型的实参都可以传递给通用构造函数’Point'。

原因是’点`,'点{Float64}'和'点{Int64}'是不同的构造函数。 实际上'点{T}'是每种类型的`T’的单独构造函数。 如果没有显式定义内部构造函数,则在声明复合类型’Point{T<:Real}'内部构造函数'点{T}`自动为每种可能的`T<:Real`类型提供。 它的工作方式与默认的非参数内部构造函数相同。 此外,还提供了一个常见的外部构造函数’Point',它接受一对必须是相同类型的实参。 这些自动提供的构造函数等效于以下声明。

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);

请注意,每个定义看起来像一个构造函数调用,它处理。 调用'点{Int64}(1,2)点的定义有关{T}(x,y)结构块内。 另一方面,声明外部构造函数为泛型"点"构造函数定义一个方法,该方法仅适用于相同实数类型的值对。 此声明负责在没有显式指定类型参数的情况下进行构造函数调用,例如`Point(1,2)`或`Point(1.0,2.5)'。 由于根据方法声明,参数必须是相同类型的,因此具有不同类型参数的调用,例如`Point(1,2.5),将导致错误"方法丢失"。

假设我们希望构造函数调用’Point(1,2.5)`通过将整数值`1`"推进"到浮点值`1.0’来执行。 最简单的方法是定义以下额外的外部构造函数方法。

julia> Point(x::Int64, y::Float64) = Point(convert(Float64,x),y);

此方法使用函数 convert将’x’显式转换为类型 'Float64',然后将创建实例的进一步工作转移到为两个参数都是类型的情况而设计的通用构造函数 'Float64'。 使用此方法定义,其中之前导致错误 'MethodError',类型为`Point'的点现在正在成功创建{Float64}`:

julia> p = Point(1,2.5)
Point{Float64}(1.0, 2.5)

julia> typeof(p)
Point{Float64}

但是,其他类似的呼叫仍然不起作用。:

julia> Point(1.5,2)
ERROR: MethodError: no method matching Point(::Float64, ::Int64)
The type `Point` exists, but no method is defined for this combination of argument types when trying to construct it.

Closest candidates are:
  Point(::T, !Matched::T) where T<:Real
   @ Main none:1
  Point(!Matched::Int64, !Matched::Float64)
   @ Main none:1

Stacktrace:
[...]

有关确保所有此类调用正确执行的更通用方法,请参阅章节 转型及推广#conversion-and-promotion【转型升级】。 让我们稍微揭示一下阴谋:为了使所有对公共构造函数`Point`的调用都能正确执行,外部方法的以下定义就足够了:

julia> Point(x::Real, y::Real) = Point(promote(x,y)...);

在这种情况下,`promote’函数将所有参数转换为通用类型 'Float64'。 使用此方法定义’Point’构造函数以与数值运算符相同的方式推进其参数,例如 '+',并适用于所有类型的实数:

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中默认提供的具有隐式类型参数的构造函数相当严格,但可以很容易地重新定义它们,以便它们更灵活地工作,但在合理的范围内。 此外,由于设计人员可以使用类型系统、方法和多个调度的所有功能,因此定义复杂的行为模式通常非常简单。

例子:有理数

要将所有这些片段放在一起,最好采用参数化复合类型及其构造函数方法的真实示例。 要做到这一点,我们实现了我们自己的有理数类型,OurRational,类似于内置的Julia类型。 '理性`,其定义在https://github.com/JuliaLang/julia/blob/master/base/rational.jl ['理性。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
               num = flipsign(num, den)
               den = flipsign(den, den)
               g = gcd(num, den)
               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)

在第一行--'构造我们的结构{T<:Integer} <:Real`--声明类型’OurRational’接受整数类型的单个参数,并且本身是实数类型。 在字段`num::T`和`den::T’的声明中,报告在对象'OurRational{T}'存储一对类型为`T`的整数,其中一个表示有理值的分子,另一个表示分母。

它变得更有趣。 'OurRational’类型有一个单一的内部构造函数方法,可以确保`num`和`den`的值在同一时间不为零,并且任何有理数都被减少到具有非负分母的不可约形式。 要做到这一点,首先分子和分母的符号是相反的,如果分母是负的。 然后将分子和分母除以最大公因数(`gcd’总是返回一个非负数,无论参数的符号如何)。 由于这是`OurRational`类型的唯一内部构造函数,我们可以确定’OurRational`对象将始终以这种规范化的形式创建。

为了方便起见,'OurRational’类型还提供了许多外部构造函数方法。 第一个是一个"标准"泛型构造函数,它从分子和分母的类型输出类型为`T`的参数,如果它们是相同类型的。 第二个用于分子和分母值属于不同类型时:它将它们提升为通用类型,然后将创建委托给专用于适当类型参数的外部构造函数。 第三个外部构造函数使用值`1`作为分母将整数值转换为有理数。

根据外部构造函数的定义,我们为运算符`⊘定义了许多方法,它提供了编写有理数的语法(例如,`1⊘2)。 Julia’Rational’类型为此使用运算符 //. 在引入这些定义之前’⊘'是一个完全不确定的运算符,具有语法但没有意义。 在他们介绍之后,它开始工作,如本节所述 有理数-它的所有行为都在这几行中定义。 请注意,中缀使用'⊘'是有效的,因为Julia有一组被识别为中缀运算符的字符。 第一个也是最基本的定义允许您使用表达式`a⊘b`创建`OurRational’的实例。 要做到这一点,OurRational`构造函数应用于`a`和`b,如果它们是整数。 如果'⊘'的一个操作数已经是一个有理数,那么为这个分数创建一个新的有理数会有点不同:实际上,这个操作与将一个有理数除以一个整数是相同的。 最后,当将运算符'⊘'应用于复杂整数时,会创建一个`Complex’的实例。{<:OurRational}'是一个复数,其实部和虚部都是理性的:

julia> z = (1 + 2im) ⊘ (1 - 2im);

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

julia> typeof(z) <: Complex{<:OurRational}
true

因此,运算符'⊘'通常返回类型`OurRational’的实例,但如果其任何参数是复杂整数,则返回’Complex’的实例。{<:OurRational}`. 我们建议好奇的读者研究这个模块到最后. https://github.com/JuliaLang/julia/blob/master/base/rational.jl ['理性。jl']:它相当简洁,孤立,并且包含Julia基类型的完整实现。

完全外部构造函数

正如您已经知道的那样,标准参数类型具有内部构造函数,这些构造函数在类型参数已知时被调用,即它们应用于'Point{Int}',但不是’指向'。 如有必要,您可以添加自动定义类型参数的外部构造函数,例如,通过创建`Point'。{Int}'调用’点(1,2)'时。 要创建实例,外部构造函数调用内部构造函数。 但是,在某些情况下,不希望提供内部构造函数,以便无法手动请求某些类型参数。

假设我们定义向量存储的类型以及其总和的精确表示。:

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’是一个类型 Int32,类型`S’应该是 'Int64'。 因此,我们需要阻止用户创建’SummedArray’类型的实例。{Int32,Int32}`. 一种方法是仅为`SummedArray`提供构造函数,并禁止在`struct`定义块内使用默认构造函数。:

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(::Vector{Int32}, ::Int32)
The type `SummedArray` exists, but no method is defined for this combination of argument types when trying to construct it.

Closest candidates are:
  SummedArray(::Vector{T}) where T
   @ Main none:4

Stacktrace:
[...]

当使用语法`SummedArray(a)时,将调用此构造函数。 语法是’new{T,S}'允许您为正在创建的类型指定参数,这意味着此调用将返回’SummedArray{T,S}. '新{T,S}`可以在任何构造函数定义中使用,但为了方便起见,如果可能的话,`new{}'参数会自动继承自正在创建的类型。

构造函数只是可调用的对象。

任何类型的对象都可以是 使其可调用通过定义一个方法。 这包括类型,即类型的对象 '类型'。 事实上,构造函数可以被认为只是类型的可调用对象。 例如,有许多方法为`Bool`及其各种超类型定义。:

julia> methods(Bool)
# 10 методов для конструктора типов:
  [1] Bool(x::BigFloat)
     @ Base.MPFR mpfr.jl:393
  [2] Bool(x::Float16)
     @ Base float.jl:338
  [3] Bool(x::Rational)
     @ Base rational.jl:138
  [4] Bool(x::Real)
     @ Base float.jl:233
  [5] (dt::Type{<:Integer})(ip::Sockets.IPAddr)
     @ Sockets ~/tmp/jl/jl/julia-nightly-assert/share/julia/stdlib/v1.11/Sockets/src/IPAddr.jl:11
  [6] (::Type{T})(x::Enum{T2}) where {T<:Integer, T2<:Integer}
     @ Base.Enums Enums.jl:19
  [7] (::Type{T})(z::Complex) where T<:Real
     @ Base complex.jl:44
  [8] (::Type{T})(x::Base.TwicePrecision) where T<:Number
     @ Base twiceprecision.jl:265
  [9] (::Type{T})(x::T) where T<:Number
     @ boot.jl:894
 [10] (::Type{T})(x::AbstractChar) where T<:Union{AbstractChar, Number}
     @ char.jl:50

构造函数的通常语法与类函数对象的语法完全相同,因此尝试使用每种语法定义方法将导致第一个方法被下一个方法复盖。:

julia> struct S
           f::Int
       end

julia> S() = S(7)
S

julia> (::Type{S})() = S(8)  # переопределяет предыдущий метод конструктора

julia> S()
S(8)