AnyMath 文档

构造函数

构造函数脚注: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中的任何其他函数一样,它的整体行为是由其方法的组合行为定义的。 因此,您可以通过简单地定义新方法来向构造函数添加功能。 例如,假设您要为 [医]脚 只接受一个参数并使用给定值的对象 酒吧巴兹 菲尔兹。 这很简单:

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

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

你也可以添加一个零参数 [医]脚 构造函数方法,为两者提供默认值 酒吧巴兹 字段:

julia> Foo() = Foo(0)
Foo

julia> Foo()
Foo(0, 0)

这里零参数构造函数方法调用单参数构造函数方法,后者又调用自动提供的双参数构造函数方法。 由于很快就会变得清晰的原因,像这样声明为普通方法的其他构造函数方法被称为_outer_构造函数方法。 外部构造函数方法只能通过调用另一个构造函数方法来创建新实例,例如自动提供的默认构造函数。

内部构造函数方法

虽然外部构造函数方法成功地解决了为构造对象提供额外方便方法的问题,但它们未能解决本章介绍中提到的另外两个用例:强制不变量和允许构造自引用对象。 对于这些问题,需要_inner_构造函数方法。 内部构造函数方法就像外部构造函数方法,除了两个区别:

  1. 它在类型声明的块内部声明,而不是像普通方法一样在它之外声明。

  2. 它可以访问一个特殊的本地存在的函数,称为 新的创建块类型的对象。

例如,假设你想声明一个持有一对实数的类型,受第一个数字不大于第二个数字的约束。 人们可以这样声明:

julia> struct OrderedPair
           x::Real
           y::Real
           OrderedPair(x,y) = x > y ? error("out of order") : new(x,y)
       end

现在 订购的,订购的 对象只能构造为 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

如果类型被声明 可变的,您可以访问并直接更改字段值以违反此不变量。 当然,不请自来地捣乱对象的内部是不好的做法。 您(或其他人)也可以在以后的任何时候提供额外的外部构造函数方法,但是一旦声明了类型,就没有办法添加更多的内部构造函数方法。 由于外部构造函数方法只能通过调用其他构造函数方法来创建对象,因此最终必须调用一些内部构造函数才能创建对象。 这保证了声明类型的所有对象都必须通过调用该类型提供的内部构造函数方法之一来存在,从而赋予类型的固定条件一定程度的强制性。

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

julia> struct Foo
           bar
           baz
           Foo(bar,baz) = new(bar,baz)
       end

本声明与较早定义的 [医]脚 没有显式内部构造函数方法的类型。 以下两种类型是等效的-一种具有默认构造函数,另一种具有显式构造函数:

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)

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

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

初始化不完整

仍然没有解决的最后一个问题是自引用对象的构建,或者更一般地说,递归数据结构。 由于根本的困难可能不会立即明显,让我们简单地解释一下。 考虑以下递归类型声明:

julia> mutable struct SelfReferential
           obj::SelfReferential
       end

这种类型可能看起来足够无害,直到人们考虑如何构造它的实例。 如果 a 是一个实例 鹿self-referential,然后可以通过调用创建第二个实例:

julia> b = SelfReferential(a)

但是,当没有实例存在时,如何构造第一个实例以提供其有效值 反对 菲尔德? 唯一的解决方案是允许创建一个未完全初始化的实例 鹿self-referential 没有签名的 反对 字段,并将该不完整实例用作 反对 另一实例的字段,诸如例如自身。

为了允许创建未完全初始化的对象,Julia允许 新的函数以少于类型具有的字段数调用,返回未指定字段未初始化的对象。 然后,内部构造函数方法可以使用不完整的对象,在返回它之前完成其初始化。 例如,这里是定义 鹿self-referential 类型,这次使用零参数内部构造函数返回具有 反对 指向自己的字段:

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
           data
           Incomplete() = new()
       end

julia> z = Incomplete();

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

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

这就避免了不断检查 null 价值观。 但是,并非所有对象字段都是引用。 Julia认为某些类型是"普通数据",这意味着它们的所有数据都是自包含的,并且不引用其他对象。 普通数据类型由基本类型组成(例如 Int型)和其他普通数据类型的不可变结构(另请参阅: 等位,等位, 等位类型). 纯数据类型的初始内容未定义:

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

julia> HasPlain()
HasPlain(438103441441)

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

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

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

与从构造函数返回的不完整对象一样,如果 完成_me 或任何其callees试图访问 数据资料 领域的 懒惰 对象在初始化之前,会立即抛出错误。

参数构造函数

参数类型为构造函数故事添加了一些皱纹。 召回自 参数类型默认情况下,参数复合类型的实例可以使用显式给定的类型参数或由给构造函数的参数类型隐含的类型参数来构造。 以下是一些例子:

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>点(1,2.5)##隐式T ##
错误:MethodError:no method matching Point(::Int64,::Float64)
类型 `点` 存在,但在尝试构造它时没有为这种参数类型组合定义方法。

最接近的候选人是:
  点(::T,::T)其中T<:Real at none:2

朱莉娅>点{Int64}(1,2)##显式T ##
点{Int64}(1, 2)

朱莉娅>点{Int64}(1.0,2.5)##显式T ##
错误:InexactError:Int64(2.5)
[医]堆垛机:
[...]

朱莉娅>点{Float64}(1.0,2.5)##显式T ##
点{Float64}(1.0, 2.5)

朱莉娅>点{Float64}(1,2)##显式T ##
点{Float64}(1.0, 2.0)

如您所见,对于具有显式类型参数的构造函数调用,参数将转换为隐含的字段类型: 点{Int64}(1,2) 有效,但是 点{Int64}(1.0,2.5) 提出一个 N.恐怖,恐怖转换时 2.5Int64. 当类型由构造函数调用的参数隐含时,如 点(1,2),那么参数的类型必须一致-否则 T 无法确定-但任何一对具有匹配类型的实参都可以给泛型 构造函数。

这里真正发生的是 , 点{Float64}点{Int64} 都是不同的构造函数。 事实上, 点{T} 是每种类型的不同构造函数 T. 没有任何显式提供的内部构造函数,复合类型的声明 点{T<:Real} 自动提供内部构造函数, 点{T},对于每种可能的类型 T<:真实,这就像非参数默认内部构造函数一样。 它还提供了一个单一的一般外 接受实参对的构造函数,这些实参必须是相同类型的。 构造函数的此自动提供等效于以下显式声明:

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)结构体 块。 另一方面,外部构造函数声明为通用定义了一个方法 构造函数,仅适用于相同实数类型的值对。 此声明使构造函数调用没有显式类型参数,如 点(1,2)点(1.0,2.5),工作。 由于方法声明将参数限制为相同类型,因此调用如下 点(1,2.5),具有不同类型的参数,导致"无方法"错误。

假设我们想调用构造函数 点(1,2.5) 通过"提升"整数值来工作 1 到浮点值 1.0. 实现此目的的最简单方法是定义以下附加的外部构造函数方法:

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

此方法使用 转换/转换显式转换的函数 x漂浮64然后将构造委托给通用构造函数,用于两个参数都是 漂浮64. 有了这个方法定义,以前是一个 方法;方法现在成功创建一个点的类型 点{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:
[...]

有关使所有此类调用合理工作的更一般方法,请参阅 转换和推广。 冒着破坏悬念的风险,我们可以在这里揭示,所需要的只是以下外部方法定义来对一般进行所有调用 构造函数的工作正如人们所期望的那样:

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

推广 函数将其所有参数转换为通用类型-在这种情况下 漂浮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中默认提供的隐式类型参数构造函数相当严格,但可以很容易地使它们以更轻松但明智的方式表现。 此外,由于构造函数可以利用类型系统、方法和多个分派的所有功能,因此定义复杂的行为通常非常简单。

案例研究:理性

也许将所有这些部分连接在一起的最佳方法是呈现一个参数化复合类型及其构造方法的真实世界示例。 为此,我们实现了我们自己的有理数类型 我们的传统,类似于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))
我们的传统

julia>⊘(n::Integer,d::Integer)=OurRational(n,d)
⊘(带1个方法的泛型函数)

julia>⊘(x::OurRational,y::Integer)=x.num⊘(x.den&ast;y)
⊘(具有2个方法的泛型函数)

julia>⊘(x::Integer,y::OurRational)=(x&ast;y.den)⊘y.num
⊘(3个方法的泛型函数)

julia>⊘(x::Complex,y::Real)=complex(real(x)⊘y,x(x)⊘y)
⊘(4个方法的泛型函数)

julia>⊘(x::Real,y::Complex)=(x&ast;y')⊘real(y&ast;y')
⊘(5个方法的泛型函数)

julia>函数⊘(x::复杂,y::复杂)
           xy=x&ast;y'
           yy=real(y&ast;y')
           复杂(real(xy)÷yy,imag(xy)÷yy)
       结束
⊘(具有6个方法的泛型函数)

第一行 — 构建我们的结构{T<:Integer} <:真实 --声明 我们的传统 接受一个整数类型的类型参数,并且它本身是一个实数类型。 字段声明 num::Tden::T 表明数据保存在一个 我们的传统{T} 对象是一对类型的整数 T,一个代表理性值的分子,另一个代表其分母。

现在事情变得有趣了。 我们的传统 有一个内部构造函数方法,它检查 num书房 不是都为零,并确保每个理性都以非负分母的"最低项"构建。 这是通过首先翻转分子和分母的符号来完成的,如果分母是负的。 然后,两者都除以它们的最大公约数(gcd 总是返回一个非负数,无论其参数的符号如何)。 因为这是唯一的内部构造函数 我们的传统,我们可以肯定 我们的传统 对象总是以这种规范化的形式构造。

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

按照外部构造函数定义,我们为 运算符,它提供了编写理据的语法(例如 1 ⊘ 2). 朱莉娅的 理性的 类型使用 //运营商为此目的。 在这些定义之前, 是一个完全未定义的运算符,只有语法,没有任何意义。 之后,它的行为就像描述的那样 有理数-它的整个行为在这几行中定义。 注意中缀使用 工作是因为Julia有一组被识别为中缀运算符的符号。 第一个也是最基本的定义只是使 a>b 构建一个 我们的传统 通过应用 我们的传统 构造函数 ab 当它们是整数时。 当操作数之一 已经是一个有理数,我们为所得比率构造一个新的有理略有不同;这种行为实际上与用整数划分一个有理相同。 最后,申请 到复杂的积分值创建一个实例 综合体{<:OurRational} --实部和虚部为理的复数:

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

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

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

因此,虽然 运算符通常返回 我们的传统,如果它的任何一个参数是复杂的整数,它将返回一个 综合体{<:OurRational} 相反。 感兴趣的读者应该考虑细读其余的https://github.com/JuliaLang/julia/blob/master/base/rational.jl[脧锚脧赂`理性。jl`]:它很短,自包含,并实现了整个基本的Julia类型。

仅外部构造函数

正如我们所看到的,一个典型的参数类型有内部构造函数,当类型参数已知时被调用;例如,它们适用于 点{Int} 但不是为了 . 或者,可以添加自动确定类型参数的外部构造函数,例如构造 点{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,这样我们就可以用较少的信息损失对许多元素进行求和。 例如,当 TInt32,我们希望 S 要成为 Int64. 因此,我们希望避免使用允许用户构造类型实例的接口 [医]摘要阵列{Int32,Int32}. 这样做的一种方法是只为 [医]摘要阵列,但在 结构体 用于禁止生成默认构造函数的定义块:

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:
[...]

此构造函数将由语法调用 摘要阵列(a). 语法 新的{T,S} 允许为要构造的类型指定参数,即此调用将返回 [医]摘要阵列{T,S}. 新的{T,S} 可以在任何构造函数定义中使用,但为了方便起见,参数为 新{} 在可能的情况下,自动从正在构造的类型派生。

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

任何类型的对象可能是 使可调用通过定义一个方法。 这包括类型,即类型的对象 类型;而构造函数实际上可以被看作只是可调用类型的对象。 例如,有许多方法定义在 布尔 和它的各种超类型:

julia> methods(Bool)
# 10 methods for type constructor:
  [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>(::类型{S})()=S(8)#复盖之前的构造方法

朱莉娅>S()
S(8)