Julia/型別入門
本節介紹型別,下一節介紹函式和方法,建議同時閱讀,因為這兩個主題緊密相連。
資料元素有不同的形狀和大小,這些被稱為型別。
考慮以下數值:一個浮點數,一個有理數和一個整數
0.5 1//2 1
對於我們人類來說,很容易毫不費力地將這些數字加起來,但是計算機將無法使用簡單的加法例程將所有三個值加起來,因為它們的型別不同。用於新增有理數的程式碼必須考慮分子和分母,而用於新增整數的程式碼則不會。計算機可能需要將其中兩個值轉換為與第三個值相同的型別 - 通常整數和有理數將首先轉換為浮點數 - 然後將三個浮點數加在一起。
這種型別轉換顯然需要時間。因此,為了編寫真正快速的程式碼,您需要確保您不會讓計算機浪費時間,不斷地將值從一種型別轉換為另一種型別。當 Julia 編譯您的原始碼(每當您首次評估函式時就會發生)時,您提供的任何型別指示都允許編譯器生成更高效的可執行程式碼。
型別轉換的另一個問題是在某些情況下您會損失精度 - 將有理數轉換為浮點數可能會損失一些精度。
Julia 設計者的官方說法是型別是可選的。換句話說,如果您不想擔心型別(並且如果您不介意您的程式碼執行速度比可能慢),那麼您可以忽略它們。但是您會在錯誤訊息和文件中遇到它們,因此您最終必須處理它們...
折衷方案是在編寫頂層程式碼時不考慮型別,但是,當您想要加快程式碼速度時,找出程式花費最多時間的瓶頸,並在該區域清理型別。
關於 Julia 的型別系統,有很多需要了解的地方,因此官方文件確實是最適合的地方。但這裡簡要概述一下。
在 Julia 中,型別以樹狀結構組織成層次結構。
在樹的根部,我們有一個名為Any的特殊型別,所有其他型別都直接或間接地與其連線。非正式地說,我們可以說Any型別有子型別。它的子型別被稱為Any的子型別。而子型別的超型別是Any。(但是請注意,型別之間的層次關係是顯式宣告的,而不是由相容的結構隱含的。)
我們可以透過檢視 Number 型別來看到 Julia 型別層次結構的一個很好的示例。

Number型別是Any的直接子型別。要檢視Number的超型別是什麼,我們可以使用supertype()函式
julia> supertype(Number) Any
但是我們也可以嘗試找到Number的子型別(Number的子型別,因此是Any的孫型別)。為此,我們可以使用subtypes()函式
julia> subtypes(Number)
2-element Array{Union{DataType, UnionAll},1}:
Complex
Real
我們可以觀察到Number有兩個子型別:Complex和Real。對於數學家來說,實數和複數都是數字。作為一般規則,Julia 的型別層次結構反映了現實世界的層次結構。
例如,如果Jaguar和Lion都是 Julia 型別,那麼如果它們的超型別是Feline,這將很自然。我們將有
julia> abstract type Feline end
julia> mutable struct Jaguar <: Feline end
julia> mutable struct Lion <: Feline end
julia> subtypes(Feline)
2-element Array{Any,1}:
Jaguar
Lion
Julia 中的每個物件(非正式地,這意味著您可以放入 Julia 變數中的所有內容)都有一種型別。但並非所有型別都可以有相應的物件(該型別的例項)。唯一可以擁有例項的型別被稱為具體型別。這些型別不能有任何子型別。可以擁有子型別的型別(例如Any、Number)被稱為抽象型別。因此,我們不能擁有Number型別的物件,因為它是一個抽象型別。換句話說,只有型別樹的葉子是具體型別,可以被例項化。
如果我們不能建立抽象型別的物件,那麼它們為什麼有用呢?有了它們,我們可以編寫程式碼,使該程式碼對所有子型別通用。例如,假設我們編寫一個函式,該函式期望一個Number型別的變數
#this function gets a number, and returns the same number plus one
function plus_one(n::Number)
return n + 1
end
在這個例子中,函式期望一個變數n。n的型別必須是Number的子型別(直接或間接),如::語法所示(但現在不要擔心語法)。這意味著什麼?無論n的型別是Int(整數)還是Float64(浮點數),函式plus_one()都將正常工作。此外,plus_one()將無法與任何不是Number的子型別的型別(例如文字字串、陣列)一起工作。
我們可以將具體型別分為兩類:原始(或基本)型別和複雜(或複合)型別。原始型別是構建塊,通常硬編碼到 Julia 的核心,而複合型別將許多其他型別組合在一起以表示更高階的資料結構。
您可能會看到以下原始型別
- 基本整數和浮點數型別(帶符號和無符號):
Int8、UInt8、Int16、UInt16、Int32、UInt32、Int64、UInt64、Int128、UInt128、Float16、Float32和Float64 - 更高階的數字型別:
BigFloat、BigInt - 布林值和字元型別:
Bool和Char - 文字字串型別:
String
Rational是一個複合型別的簡單示例,用於表示分數。它由兩個部分組成,一個分子和一個分母,它們都是整數(Int型別)。
Julia 提供了兩個用於瀏覽型別層次結構的函式:subtypes()和supertype()。
julia> subtypes(Integer)
4-element Array{Union{DataType, UnionAll},1}:
BigInt
Bool
Signed
Unsigned
julia> supertype(Float64)
AbstractFloat
sizeof()函式告訴您此型別的一個專案佔用了多少位元組
julia> sizeof(BigFloat) 32 julia> sizeof(Char) 4
如果您想知道您可以將多大的數字放入特定型別,則這兩個函式很有用
julia> typemax(Int64) 9223372036854775807 julia> typemin(Int32) -2147483648
Julia 基本系統中有超過 340 種類型。您可以使用以下函式調查型別層次結構
function showtypetree(T, level=0)
println("\t" ^ level, T)
for t in subtypes(T)
showtypetree(t, level+1)
end
end
showtypetree(Number)
它為不同的 Number 型別生成類似於以下內容的結果
julia> showtypetree(Number) Number Complex Real AbstractFloat BigFloat Float16 Float32 Float64 Integer BigInt Bool Signed Int128 Int16 Int32 Int64 Int8 Unsigned UInt128 UInt16 UInt32 UInt64 UInt8 Irrational Rational
這表明,例如,Real數字的四個主要子型別:AbstractFloat、Integer、Rational和Irrational,如樹形圖所示。

我們已經看到,如果您沒有指定型別,Julia 會盡力找出您在程式碼中放置的內容的型別
julia> collect(1:10)
10-element Array{Int64,1}:
1
2
3
4
5
6
7
8
9
10
julia> collect(1.0:10)
10-element Array{Float64,1}:
1.0
2.0
3.0
4.0
5.0
6.0
7.0
8.0
9.0
10.0
我們還看到您可以為新的空陣列指定型別
julia> fill!(Array{String}(undef, 3), "Julia")
3-element Array{String,1}:
"Julia"
"Julia"
"Julia"
對於變數,您可以指定其值必須具有的型別。由於技術原因,您無法在 REPL 的頂層執行此操作 - 您只能在定義內部執行此操作。語法使用::語法,這意味著“是型別”。所以
function f(x::Int64)
表示函式f有一個方法,它接受一個引數x,該引數預計是 Int64。見函式.
這是一個關於Julia程式碼效能如何受到變數型別選擇的示例。這是一些用於探索Collatz猜想的程式碼。
function chain_length(n, terms)
length = 0
while n != 1
if haskey(terms, n)
length += terms[n]
break
end
if n % 2 == 0 # is n even?
n /= 2
else
n = 3n + 1
end
length += 1
end
return length
end
function main()
ans = 0
limit = 1_000_000
score = 0
terms = Dict() # define a dictionary
for i in 1:limit
terms[i] = chain_length(i, terms)
if terms[i] > score
score = terms[i]
ans = i
end
end
return ans
end
我們可以使用@time宏來計時(儘管BenchmarkTools包提供了更好的基準測試工具)。
julia> @time main() 2.634295 seconds (17.95 M allocations: 339.074 MiB, 13.50% gc time)
有兩行程式碼阻止函式“型別穩定”。這些是編譯器無法使用最佳和最有效型別來完成手頭任務的地方。你能找出它們嗎?
第一個是將n除以2,在測試n是否為偶數之後。n最初是一個整數,但/除法運算子始終返回一個浮點數。Julia編譯器無法生成純整數程式碼或純浮點數程式碼,必須在每個階段決定使用哪一種。結果,編譯後的程式碼不像它本可以的那樣快或簡潔。
第二個問題是這裡的字典定義。它是在沒有型別資訊的情況下定義的,因此鍵和值可以是任何型別的。雖然這通常是可以的,但在這種型別的任務中,在迴圈中頻繁訪問,維護鍵和值可能存在不同型別的額外任務會使程式碼更加複雜。
julia> Dict()
Dict{Any, Any}()
如果我們告訴Julia編譯器這個字典只包含整數(這是一個好的假設),編譯後的程式碼將更加高效,並且型別穩定。
所以,在將n /= 2更改為n ÷= 2,以及terms = Dict()更改為terms = Dict{Int, Int}()之後,我們預計編譯器會生成更有效的程式碼,實際上它更快了。
Julia> @time main() 0.450561 seconds (54 allocations: 65.170 MiB, 19.33% gc time)
你可以從編譯器獲得一些關於程式碼中可能存在由於型別不穩定而導致問題的提示。例如,對於此函式,你可以輸入@code_warntype main()並查詢以紅色突出顯示的專案或“Any”。
建立型別
[edit | edit source]在Julia中,程式設計師可以非常輕鬆地建立新的型別,並受益於與原生型別(由Julia的建立者建立的型別)相同的效能和語言級整合。
抽象型別
[edit | edit source]假設我們要建立一個抽象型別。為此,我們使用Julia的關鍵字abstract,後跟要建立的型別的名稱。
abstract type MyAbstractType end
預設情況下,你建立的型別是Any的直接子型別。
julia> supertype(MyAbstractType) Any
你可以使用<:運算子更改此設定。例如,如果你希望你的新抽象型別成為Number的子型別,你可以宣告
abstract type MyAbstractType2 <: Number end
現在,我們得到
julia> supertype(MyAbstractType2) Number
請注意,在同一個Julia會話中(不退出REPL或結束指令碼),無法重新定義型別。這就是為什麼我們必須建立一個名為MyAbstractType2的型別。
具體型別和複合型別
[edit | edit source]你可以建立新的複合型別。為此,使用struct或mutable struct關鍵字,它們的語法與宣告超型別相同。新型別可以包含多個欄位,物件在其中儲存值。例如,讓我們定義一個具體型別,它是MyAbstractType的子型別。
mutable struct MyType <: MyAbstractType
foo
bar::Int
end
我們剛剛建立了一個名為MyType的複合結構體型別,它是MyAbstractType的子型別,具有兩個欄位:foo可以是任何型別的,bar是Int型別的。
我們如何建立一個MyType物件?預設情況下,Julia會自動建立一個**建構函式**,一個返回該型別物件的函式。該函式與型別的名稱相同,並且函式的每個引數都對應於每個欄位。在這個例子中,我們可以透過輸入以下內容來建立一個新物件:
julia> x = MyType("Hello World!", 10)
MyType("Hello World!", 10)
這將建立一個MyType物件,將Hello World!分配給foo欄位,將10分配給bar欄位。我們可以使用**點**符號訪問x的欄位。
julia> x.foo "Hello World!" julia> x.bar 10
此外,我們可以輕鬆地更改可變結構體的欄位值。
julia> x.foo = 3.0 3.0 julia> x.foo 3.0
請注意,由於我們在建立型別定義時沒有指定foo的型別,因此我們可以隨時更改其型別。這與嘗試更改x.bar欄位的型別(根據MyType的定義,我們指定它為Int)不同。
julia> x.bar = "Hello World!" LoadError: MethodError: Cannot `convert` an object of type String to an object of type Int64 This may have arisen from a call to the constructor Int64(...), since type constructors fall back to convert methods.
錯誤訊息告訴我們Julia無法更改x.bar的型別。這保證了型別穩定的程式碼,並且可以在程式設計時提供更好的效能。作為一個性能提示,在定義型別時指定欄位型別通常是一個好習慣。
預設建構函式用於簡單情況,在這種情況下,你鍵入類似於**typename(field1, field2)**的內容來生成型別的新例項。但是,有時你在構造新例項時想要做更多的事情,例如檢查傳入的值。為此,你可以使用內部建構函式,即型別定義內的函式。下一節將展示一個實際示例。
示例:英鎊貨幣
[edit | edit source]這是一個關於如何建立一個簡單的複合型別來處理老式英鎊貨幣的示例。在英國看到光明並引入十進位制貨幣之前,貨幣系統使用英鎊、先令和便士,其中一英鎊包含20先令,一先令包含12便士。這被稱為£sd或LSD系統(拉丁語為Librae,Solidii,Denarii,因為該系統起源於羅馬帝國)。
要定義合適的型別,請開始一個新的複合型別宣告。
struct LSD
為了包含以英鎊、先令和便士表示的價格,這個新型別應該包含三個欄位:英鎊、先令和便士。
pounds::Int
shillings::Int
pence::Int
重要的任務是建立一個**建構函式**。它與型別的名稱相同,並接受三個值作為引數。經過一些對無效值的檢查後,特殊的new()函式將建立一個包含傳入值的新物件。請記住,我們仍然在type定義中——這是一個內部建構函式。
function LSD(a,b,c)
if a < 0 || b < 0 || c < 0
error("no negative numbers")
end
if c > 12 || b > 20
error("too many pence or shillings")
end
new(a, b, c)
end
現在我們可以完成型別定義。
end
以下是完整的型別定義。
struct LSD
pounds::Int
shillings::Int
pence::Int
function LSD(a, b, c)
if a < 0 || b < 0
error("no negative numbers")
end
if c > 12 || b > 20
error("too many pence or shillings")
end
new(a, b, c)
end
end
現在可以建立儲存老式英鎊價格的新物件。你可以使用它的名稱(它呼叫建構函式)來建立一個這種型別的新物件。
julia> price1 = LSD(5, 10, 6) LSD(5, 10, 6) julia> price2 = LSD(1, 6, 8) LSD(1, 6, 8)
你無法建立錯誤的價格,因為建構函式中添加了簡單的檢查。
julia> price = LSD(1, 0, 13) ERROR: too many pence or shillings Stacktrace: [1] LSD(::Int64, ::Int64, ::Int64)
如果你檢查我們建立的其中一個價格“物件”的欄位。
julia> fieldnames(typeof(price1))
3-element Array{Symbol,1}:
:pounds
:shillings
:pence
你可以看到三個欄位,它們正在儲存值。
julia> price1.pounds 5 julia> price1.shillings 10 julia> price1.pence 6
接下來的任務是使這個新型別像其他Julia物件一樣執行。例如,我們無法新增兩個價格。
julia> price1 + price2 ERROR: MethodError: no method matching +(::LSD, ::LSD) Closest candidates are: +(::Any, ::Any, ::Any, ::Any...) at operators.jl:420
並且輸出可以肯定地得到改進。
julia> price2 LSD(5, 10, 6)
Julia已經具有新增函式(+),其中定義了針對許多型別的物件的方法。以下程式碼添加了另一種可以處理兩個LSD物件的方法。
function Base.:+(a::LSD, b::LSD)
newpence = a.pence + b.pence
newshillings = a.shillings + b.shillings
newpounds = a.pounds + b.pounds
subtotal = newpence + newshillings * 12 + newpounds * 240
(pounds, balance) = divrem(subtotal, 240)
(shillings, pence) = divrem(balance, 12)
LSD(pounds, shillings, pence)
end
這個定義教Julia如何處理新的LSD物件,並向+函式添加了一種新方法,該方法接受兩個LSD物件,將它們加在一起,併產生一個包含總和的新LSD物件。
現在你可以新增兩個價格。
julia> price1 + price2 LSD(6,17,2)
這確實是將LSD(5,10,6)和LSD(1,6,8)相加的結果。
下一個要解決的問題是LSD物件不美觀的顯示方式。這可以透過新增一個新方法來解決,但這次是新增給show()函式,該函式屬於Base環境。
function Base.show(io::IO, money::LSD)
print(io, "£$(money.pounds).$(money.shillings)s.$(money.pence)d")
end
在這裡,io是當前由所有show()方法使用的輸出通道。我們添加了一個簡單的表示式,它以適當的標點符號和分隔符顯示欄位值。
julia> println(price1 + price2) £6.17s.2d
julia> show(price1 + price2 + LSD(0,19,11) + LSD(19,19,6)) £27.16s.7d
你可以新增一個或多個別名,它們是特定型別的替代名稱。由於Price比LSD更能表達含義,因此我們將建立一個有效的替代方案。
julia> const Price=LSD LSD julia> show(Price(1, 19, 11)) £1.19s.11d
到目前為止,一切都很好,但這些LSD物件還沒有完全開發。如果你想做減法、乘法和除法,你必須為這些函式定義額外的處理LSD的方法。減法很容易,只需要對先令和便士進行一些調整,所以我們現在就跳過它,但乘法呢?將價格乘以數字涉及兩種型別的物件,一種是價格/LSD物件,另一種是——嗯,任何正實數都應該是可能的。
function Base.:*(a::LSD, b::Real)
if b < 0
error("Cannot multiply by a negative number")
end
totalpence = b * (a.pence + a.shillings * 12 + a.pounds * 240)
(pounds, balance) = divrem(totalpence, 240)
(shillings, pence) = divrem(balance, 12)
LSD(pounds, shillings, pence)
end
就像我們新增到Base的+函式中的+方法一樣,這個為Base的*函式定義的新*方法專門用於將價格乘以數字。對於第一次嘗試,它執行得很好。
julia> price1 * 2 £11.1s.0d julia> price1 * 3 £16.11s.6d julia> price1 * 10 £55.5s.0d julia> price1 * 1.5 £8.5s.9d julia> price3 = Price(0,6,5) £0.6s.5d julia> price3 * 1//7 £0.0s.11d
然而,一些失敗是不可避免的。我們沒有考慮到非常老式的便士分數:半便士和法令。
julia> price1 * 0.25
ERROR: InexactError()
Stacktrace:
[1] convert(::Type{Int64}, ::Float64) at ./float.jl:675
[2] LSD(::Float64, ::Float64, ::Float64) at ./REPL[36]:40
[3] *(::LSD, ::Float64) at ./REPL[55]:10
(答案應該是£1.7s.7½d。不幸的是,我們的LSD型別不允許便士分數。)
但是還有一個更緊迫的問題。目前,您必須先給出價格,然後給出乘數;反過來就不行。
julia> 2 * price1 ERROR: MethodError: no method matching *(::Int64, ::LSD) Closest candidates are: *(::Any, ::Any, ::Any, ::Any...) at operators.jl:420 *(::Number, ::Bool) at bool.jl:106 ...
這是因為,雖然 Julia 可以找到一個匹配 (a::LSD, b::Number) 的方法,但它卻找不到反過來的方法:(a::Number, b::LSD)。但新增它非常容易。
function Base.:*(a::Number, b::LSD)
b * a
end
這為 Base 的 * 函式添加了另一個方法。
julia> price1 * 2 £11.1s.0d
julia> 2 * price1 £11.1s.0d
julia> for i in 1:10
println(price1 * i)
end
£5.10s.6d
£11.1s.0d
£16.11s.6d
£22.2s.0d
£27.12s.6d
£33.3s.0d
£38.13s.6d
£44.4s.0d
£49.14s.6d
£55.5s.0d
現在,價格看起來就像 19 世紀一家老式的英國商店,真是太棒了!
如果您想檢視到目前為止為這種老式的英鎊型別添加了多少個方法,請使用 methodswith() 函式。
julia> methodswith(LSD)
4-element Array{Method,1}:
*(a::LSD, b::Real) at In[20]:4
*(a::Number, b::LSD) at In[34]:2
+(a::LSD, b::LSD) at In[13]:2
show(io::IO, money::LSD) at In[15]:2
到目前為止只有四個……您可以繼續新增方法來使該型別更通用——這取決於您或其他人如何設想使用它。例如,您可能希望新增除法和模運算方法,並對負的貨幣值進行智慧處理。
這個用於儲存英國價格的複合型別被定義為一個不可變型別。您無法在建立這些價格物件後更改它們的值。
julia> price1.pence 6 julia> price1.pence=10 ERROR: type LSD is immutable
要基於現有價格建立新價格,您需要執行以下操作:
julia> price2 = Price(price1.pounds, price1.shillings, 10) £5.10s.10d
對於這個特定的示例,這不是一個大問題,但對於很多應用程式,您可能希望修改或更新型別中某個欄位的值,而不是建立一個具有正確值的新的欄位。
對於這些情況,您需要建立一個 mutable struct。根據對型別的要求選擇 struct 或 mutable struct。
有關模組以及從其他模組匯入函式的更多資訊,請參見 模組和包。