本文轉載自【微信公眾號:雲深之無跡,ID:TT1827652464】經微信公眾號授權轉載,如需轉載與原文作者聯繫
建設者
構造函數[1]是創建新對象的函數,特別是Composite Types的實例。在Julia中,類型對象還充當構造函數:它們在作為參數應用於元組時會創建自己的新實例。引入複合類型時,已經簡要提到了這一點。例如:
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
一個參數並為
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)
此處,零參數構造函數方法調用單參數構造函數方法,後者又調用自動提供的兩參數構造函數方法。由於很快就會明白的原因,像這樣的聲明為普通方法的其他構造方法稱為外部構造方法。外部構造器方法只能通過調用另一個構造器方法(例如自動提供的默認方法)來創建新實例。
內部構造方法
儘管外部構造方法成功解決了提供額外的便利方法來構造對象的問題,但它們未能解決本章引言中提到的其他兩個用例:強制不變式,並允許構造自引用對象。對於這些問題,需要內部構造方法。內部構造方法非常類似於外部構造方法,但有兩個區別:
它是在類型聲明的塊內部聲明的,而不是像普通方法在其外部聲明的那樣。它可以訪問一個特殊的本地存在的函數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] OrderedPair(::Int64, ::Int64) at ./none:4
如果聲明了類型
mutable
,則可以進入並直接更改欄位值以違反此不變式,但是不請自來處理對象的內部結構被認為是較差的形式。您(或其他人)也可以在以後的任何時候提供其他外部構造函數方法,但是一旦聲明了類型,就無法添加更多內部構造函數方法。由於外部構造函數方法只能通過調用其他構造函數方法來創建對象,因此最終必須調用某些內部構造函數來創建對象。這保證了必須通過調用隨該類型提供的內部構造方法之一來實現已聲明類型的所有對象,從而在某種程度上強制了類型的不變量。
如果定義了任何內部構造函數方法,則不會提供默認的構造函數方法:假定您已為自己提供了所需的所有內部構造函數。默認構造函數等效於編寫自己的內部構造函數方法,該方法將對象的所有欄位作為參數(如果對應的欄位具有類型,則約束為正確的類型),並將它們傳遞給
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)
最好提供儘可能少的內部構造方法:僅那些顯式接受所有參數並強制進行基本錯誤檢查和轉換的方法。應提供提供默認值或輔助轉換的其他便捷構造函數方法,作為調用內部構造函數進行繁重工作的外部構造函數。這種分離通常是很自然的。
不完整的初始化
最後一個尚未解決的問題是構造自引用對象,或更一般地說,構造遞歸數據結構。由於基本困難可能不會立即顯現,所以讓我們簡要地解釋一下。考慮以下遞歸類型聲明:
julia> mutable struct SelfReferential
obj::SelfReferential
end
在人們考慮如何構造它的實例之前,這種類型可能看起來足夠無害。如果
a
是的實例
SelfReferential
,則可以通過調用創建第二個實例:
julia> b = SelfReferential(a)
但是,當沒有實例提供其
obj
欄位的有效值時,該如何構造第一個實例?唯一的解決方案是允許創建
SelfReferential
帶有未分配
obj
欄位的未完全初始化的實例,並將該不完整的實例用作
obj
另一個實例(例如其自身)的欄位的有效值。
為了允許創建未完全初始化的對象,Julia允許
new
使用少於類型具有的欄位數的函數來調用該函數,並返回未初始化未指定欄位的對象。然後,內部構造函數方法可以使用不完整的對象,在返回之前完成其初始化。例如,在這裡,我們在定義
SelfReferential
類型時遇到了另一個難題,使用零參數內部構造函數返回實例,該實例具有
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
這避免了需要連續檢查
null
值。但是,並非所有對象欄位都是引用。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
與從構造函數返回的不完整對象一樣,如果對象的
complete_me
任何一個或任何一個被調用者在初始化之前嘗試訪問
xx
該
Lazy
對象的欄位,則將立即引發錯誤。
參數構造器
參數類型為構造函數增加了一些麻煩。從參數類型回想起,默認情況下,可以使用顯式給定的類型參數或給定構造函數的參數類型所隱含的類型參數來構造參數複合類型的實例。這裡有些例子:
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.5
到
Int64
。當構造函數調用的參數隱含類型時,如中所述
Point(1,2)
,則參數的類型必須一致(否則
T
無法確定),但是可以將具有匹配類型的任何一對實參提供給通用
Point
構造函數。
真正發生的是
Point
,
Point{Float64}
並且
Point{Int64}
都是不同的構造函數。實際上,
Point{T}
每種類型都有一個獨特的構造函數
T
。如果沒有任何顯式提供的內部構造函數,則複合類型的聲明會
Point{T<:Real}
自動
Point{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);
注意,每個定義看起來都像它處理的構造函數調用的形式。該調用
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
。實現此目的的最簡單方法是定義以下其他外部構造方法:
julia> Point(x::Int64, y::Float64) = Point(convert(Float64,x),y);
如果兩個參數都為,則此方法使用該
convert()
函數顯式轉換
x
為
Float64
,然後將構造委託給常規構造函數
Float64
。通過此方法定義,以前
MethodError
成功的現在可以成功創建類型的點
Point{Float64}
:
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
有關使所有此類調用合理運行的更一般的方法,請參見轉化和升級。冒著破壞懸念的危險,我們可以在這裡揭示出,只需進行以下外部方法定義即可使對通用
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中默認提供的隱式類型參數構造函數相當嚴格,但有可能使它們以更輕鬆但更明智的方式輕鬆地表現。而且,由於構造函數可以利用類型系統,方法和多重調度的所有功能,因此定義複雜的行為通常非常簡單。
案例研究:理性
將所有這些部分聯繫在一起的最好方法可能是提供一個真實的參數複合類型及其構造方法的示例。為此,這是的(略有修改)開頭
rational.jl
,它實現了Julia的有理數:
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::T
和
den::T
指示
OurRational{T}
對象中保存的數據是一對類型為的整數
T
,一個代表有理值的分子,另一個代表其分母。
現在事情變得有趣了。
OurRational
有一個內部構造函數方法,該方法檢查
num
和
den
都不為零,並確保每個有理數都使用非負分母以「最低項」構造。這是通過將給定的分子和分母值除以使用該
gcd
函數計算出的最大公除數來實現的。由於
gcd
返回其參數的最大公約數,且符號與第一個參數匹配(
den
此處),因此在該除法之後,
den
可以確保的新值是非負的。因為這是的唯一內部構造函數
OurRational
,所以我們可以確定
OurRational
對象始終以這種標準化形式構造。
OurRational
為了方便起見,還提供了幾種外部構造方法。第一個是「標準」通用構造函數,
T
當它們具有相同的類型時,它們將從分子和分母的類型推斷出類型參數。第二種適用於給定的分子和分母值具有不同類型的情況:它將它們提升為公共類型,然後將構造委託給外部構造函數以獲取匹配類型的參數。第三個外部構造函數通過提供值
1
作為分母,將整數值轉換為有理數。
遵循外部構造函數的定義,我們為
//
運算符提供了許多方法,這些方法提供了用於編寫有理數的語法。在這些定義之前,
//
是一個完全未定義的運算符,僅包含語法,沒有意義。之後,它的行為就與Rational Numbers中描述的一樣-它的整個行為在以下幾行中定義。第一個也是最基本的定義
a//b
是
OurRational
通過將
OurRational
構造函數應用於
a
或
b
當它們是整數時來構造a 。當的操作數之一
//
已經是一個有理數時,我們為所得比率構建稍微不同的新有理;這種行為實際上與有理數與整數的除法相同。最後,申請
//
複數的整數值創建了一個實例
Complex{OurRational}
–一個複數,其實部和虛部都是有理數:
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
,但是不是真的從一個「轉換」
Int
到
Array
。
僅外部構造函數
如我們所見,典型的參數類型具有內部構造函數,這些構造函數在已知類型參數時被調用;例如它們適用於
Point{Int}
但不適用於
Point
。(可選)可以添加自動確定類型參數的外部構造函數,例如,
Point{Int}
從call 構造a
Point(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
is時
Int32
,我們希望
S
成為
Int64
。因此,我們要避免允許用戶構造類型實例的接口
SummedArray{Int32,Int32}
。一種實現方法是僅為提供一個構造函數
SummedArray
,但在
type
定義塊內部以禁止生成默認構造函數:
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{}
在可能的情況下,要自動從要構造的類型派生參數。