風格指南
以下各節說明慣用語法 Julia 編碼風格的幾個面向。這些規則都不是絕對的;它們只是幫助您熟悉此語言和在各種替代設計中進行選擇的建議。
縮排
每個縮排層級使用 4 個空格。
撰寫函式,而非僅是指令碼
將程式碼寫成頂層的一系列步驟是快速開始解決問題的方法,但您應盡快嘗試將程式分為函式。函式更易於重複使用和測試,並釐清正在執行的步驟及其輸入和輸出。此外,由於 Julia 編譯器的工作方式,函式中的程式碼往往執行得比頂層程式碼快很多。
另外值得強調的是,函式應採用引數,而非直接對全域變數進行運算(除了常數,例如 pi
)。
避免撰寫過於具體的類型
程式碼應盡可能通用。不要撰寫
Complex{Float64}(x)
最好使用可用的通用函數
complex(float(x))
第二個版本會將 x
轉換為適當的類型,而不是總是相同的類型。
此樣式點特別與函數參數相關。例如,如果參數實際上可以是任何整數,則不要宣告參數的類型為 Int
或 Int32
,而應使用抽象類型 Integer
表示。事實上,在許多情況下,您可以完全省略參數類型,除非需要與其他方法定義區分,因為如果傳遞的類型不支援任何必要的運算,則無論如何都會擲出 MethodError
。(這稱為 鴨子型別。)
例如,考慮以下函數 addone
的定義,它會傳回其參數加一
addone(x::Int) = x + 1 # works only for Int
addone(x::Integer) = x + oneunit(x) # any integer type
addone(x::Number) = x + oneunit(x) # any numeric type
addone(x) = x + oneunit(x) # any type supporting + and oneunit
addone
的最後一個定義處理任何支援 oneunit
(在與 x
相同的類型中傳回 1,這避免了不需要的類型提升)和使用這些參數的 +
函數的類型。要了解的重點是定義 僅有 一般 addone(x) = x + oneunit(x)
沒有效能損失,因為 Julia 會根據需要自動編譯特殊版本。例如,當您第一次呼叫 addone(12)
時,Julia 會自動編譯一個特殊 addone
函數,適用於 x::Int
參數,並將對 oneunit
的呼叫替換為內聯值 1
。因此,上面 addone
的前三個定義與第四個定義完全重複。
在呼叫方處理過多的參數多樣性
不要使用
function foo(x, y)
x = Int(x); y = Int(y)
...
end
foo(x, y)
使用
function foo(x::Int, y::Int)
...
end
foo(Int(x), Int(y))
這是較佳的風格,因為 foo
實際上並不接受所有類型的數字;它實際上需要 Int
。
這裡的一個問題是,如果一個函數本質上需要整數,那麼最好強制呼叫者決定如何轉換非整數(例如,向下取整或向上取整)。另一個問題是,宣告更具體的類型會為未來的函數定義留下更多「空間」。
將 !
附加到會修改其引數的函數名稱
不要使用
function double(a::AbstractArray{<:Number})
for i = firstindex(a):lastindex(a)
a[i] *= 2
end
return a
end
使用
function double!(a::AbstractArray{<:Number})
for i = firstindex(a):lastindex(a)
a[i] *= 2
end
return a
end
Julia Base 在整個過程中都使用此慣例,並包含具有複製和修改形式的函數範例(例如,sort
和 sort!
),以及其他僅修改的函數(例如,push!
、pop!
、splice!
)。此類函數通常也會傳回修改後的陣列以供方便。
與 IO 相關或使用亂數產生器 (RNG) 的函數是值得注意的例外:由於這些函數幾乎總是必須變異 IO 或 RNG,因此以 !
結尾的函數用於表示變異,而非變異 IO 或推進 RNG 狀態。例如,rand(x)
會變異 RNG,而 rand!(x)
會變異 RNG 和 x
;類似地,read(io)
會變異 io
,而 read!(io, x)
會變異兩個引數。
避免奇怪的類型 Union
例如 Union{Function,AbstractString}
的類型通常表示某些設計可以更簡潔。
避免精緻的容器類型
建構像下列的陣列通常沒有多大幫助
a = Vector{Union{Int,AbstractString,Tuple,Array}}(undef, n)
在這種情況下,Vector{Any}(undef, n)
會更好。對特定用途進行註解(例如 a[i]::Int
)比嘗試將多種替代方案打包到一個類型中對編譯器更有幫助。
偏好使用匯出的方法而非直接欄位存取
慣用的 Julia 程式碼通常應將模組的匯出方法視為其類型的介面。物件的欄位通常被視為實作細節,使用者程式碼僅應在聲明為 API 時直接存取它們。這有幾個好處
- 套件開發人員可以更自由地變更實作,而不會中斷使用者程式碼。
- 方法可以傳遞給高階建構,例如
map
(例如map(imag, zs)
),而不是[z.im for z in zs]
)。 - 方法可以在抽象類型上定義。
- 方法可以描述可以在不同類型之間共用的概念性運算(例如
real(z)
適用於複數或四元數)。
Julia 的調度系統鼓勵這種樣式,因為 play(x::MyType)
僅在該特定類型上定義 play
方法,讓其他類型擁有自己的實作。
類似地,非匯出函式通常是內部的,並會變更,除非文件另有說明。名稱有時會加上 _
字首(或字尾)以進一步暗示某個東西是「內部」或實作細節,但這並非規則。
此規則的反例包括 NamedTuple
、RegexMatch
、StatStruct
。
使用與 Julia base/
一致的命名慣例
- 模組和類型名稱使用大寫字母和駝峰式大小寫:
module SparseArrays
、struct UnitRange
。 - 函數使用小寫字母(
maximum
、convert
),且在可讀取的情況下,將多個單字壓縮在一起(isequal
、haskey
)。必要時,使用底線作為單字分隔符號。底線也用於表示概念的組合(remotecall_fetch
作為fetch(remotecall(...))
的更有效率的實作)或作為修飾詞。 - 至少變更一個引數的函數以
!
結尾。 - 簡潔性很重要,但避免縮寫(
indexin
而不是indxin
),因為很難記住特定單字是否以及如何縮寫。
如果函數名稱需要多個單字,請考慮它是否可能表示多個概念,並且可能最好將其拆分成多個部分。
撰寫具有與 Julia Base 類似的引數順序的函數
一般而言,Base 函式庫對函數使用下列引數順序,視情況而定
函數參數。將函數參數放在最前面允許使用
do
區塊來傳遞多行匿名函數。I/O 串流。將
IO
物件指定在最前面允許將函數傳遞給函數,例如sprint
,例如sprint(show, x)
。正在變異的輸入。例如,在
fill!(x, v)
中,x
是正在變異的物件,它出現在要插入到x
中的值之前。類型。傳遞類型通常表示輸出將具有給定的類型。在
parse(Int, "1")
中,類型出現在要解析的字串之前。有許多這樣的範例,其中類型出現在最前面,但值得注意的是,在read(io, String)
中,IO
參數出現在類型之前,這與這裡概述的順序一致。未變異的輸入。在
fill!(x, v)
中,v
沒有 正在變異,它出現在x
之後。鍵。對於關聯式集合,這是鍵值對的鍵。對於其他索引集合,這是索引。
值。對於關聯式集合,這是鍵值對的值。在像
fill!(x, v)
這樣的案例中,這是v
。其他所有。任何其他參數。
變數參數。這是指可以在函數呼叫結尾無限列出的參數。例如,在
Matrix{T}(undef, dims)
中,維度可以作為Tuple
給出,例如Matrix{T}(undef, (1,2))
,或作為Vararg
給出,例如Matrix{T}(undef, 1, 2)
。關鍵字參數。在 Julia 中,關鍵字參數在函數定義中必須放在最後;它們在此處列出只是為了完整性。
絕大多數函數不會採用上面列出的每一種參數;這些數字僅表示應對函數的任何適用參數使用的優先順序。
當然有一些例外。例如,在 convert
中,類型應始終排在第一位。在 setindex!
中,值出現在索引之前,以便可以將索引作為變參提供。
在設計 API 時,儘可能遵守此一般順序可能會為函數使用者提供更一致的體驗。
不要過度使用 try-catch
避免錯誤優於依賴捕捉錯誤。
不要對條件使用括號
Julia 不需要在 if
和 while
中的條件周圍加上括號。撰寫
if a == b
而不是
if (a == b)
不要過度使用 ...
拼接函數參數可能會令人上癮。不要使用 [a..., b...]
,而只需使用 [a; b]
,它已經連接陣列。collect(a)
優於 [a...]
,但由於 a
已經是可迭代的,因此通常甚至可以不對其進行任何操作,也不要將其轉換為陣列。
不要使用不必要的靜態參數
函數簽章
foo(x::T) where {T<:Real} = ...
應寫成
foo(x::Real) = ...
尤其是當函式主體中未用到 T
時。即使有用到 T
,也可以在方便時以 typeof(x)
取代。效能上沒有差異。請注意,這並非針對靜態參數的一般性注意事項,而僅針對不需要使用靜態參數的情況。
另外請注意,容器類型,特別是可能需要在函式呼叫中使用類型參數。請參閱常見問題集 避免使用具有抽象容器的欄位 以取得更多資訊。
避免混淆某個項目是實例還是類型
類似下列的定義集會造成混淆
foo(::Type{MyType}) = ...
foo(::MyType) = foo(MyType)
決定要將有問題的概念寫成 MyType
還是 MyType()
,並堅持使用。
建議的樣式是預設使用實例,只有在有必要解決某些問題時,才加入涉及 Type{MyType}
的方法。
如果某個類型實際上是列舉,則應將其定義為單一(理想上是不可變的結構或基本類型),並將列舉值設為其實例。建構函式和轉換可以檢查值是否有效。此設計優於將列舉設為抽象類型,並將「值」設為子類型。
不要過度使用巨集
請注意巨集何時可以實際上成為函式。
在巨集中呼叫 eval
是特別危險的警示標誌;這表示巨集只有在頂層呼叫時才會運作。如果此類巨集改寫成函式,它自然會存取它需要的執行時間值。
不要在介面層級公開不安全的作業
如果您有一個類型使用原生指標
mutable struct NativeType
p::Ptr{UInt8}
...
end
請勿撰寫下列類型的定義
getindex(x::NativeType, i) = unsafe_load(x.p, i)
問題在於此類型的使用者可以撰寫 x[i]
而不會發現該作業是不安全的,然後容易受到記憶體錯誤的影響。
此類函式應該檢查作業以確保其安全,或在名稱中某處加上 unsafe
以提醒呼叫者。
不要覆載基本容器類型的函式
可以撰寫下列類型的定義
show(io::IO, v::Vector{MyType}) = ...
這將提供使用特定新元素類型的向量自訂顯示。雖然很誘人,但應避免這麼做。問題在於使用者會預期像 Vector()
這類眾所周知的類型以特定方式運作,而過度自訂其行為會讓它更難以使用。
避免類型盜用
「類型盜用」是指在您未定義的類型上擴充或重新定義 Base 或其他套件中的方法。在極端情況下,您可能會使 Julia 崩潰(例如,如果您的方法擴充或重新定義導致無效的輸入傳遞給 ccall
)。類型盜用會使程式碼推理複雜化,並可能引入難以預測和診斷的不相容性。
舉例來說,假設您想要在模組中定義符號的乘法
module A
import Base.*
*(x::Symbol, y::Symbol) = Symbol(x,y)
end
問題在於現在任何使用 Base.*
的其他模組也會看到此定義。由於 Symbol
是在 Base 中定義的,並且由其他模組使用,這可能會意外地改變不相關程式碼的行為。這裡有幾個替代方案,包括使用不同的函式名稱,或將 Symbol
包裝在您定義的另一種類型中。
有時,耦合套件可能會從定義中分離出功能來進行類型盜用,特別是在套件是由合作作者設計,且定義是可以重複使用的。例如,一個套件可能會提供一些用於處理色彩的類型;另一個套件可以定義這些類型的函式,以啟用色彩空間之間的轉換。另一個範例可能是充當某些 C 程式碼的薄包裝器的套件,另一個套件可能會盜用該套件來實作更高級別、友善的 Julia API。
小心類型相等
您通常會想要使用 isa
和 <:
來測試類型,而不是 ==
。只有在與已知的具體類型(例如 T == Float64
)進行比較,或您真的、真的知道自己在做什麼時,檢查類型是否完全相等才有意義。
不要為已命名函式 f
寫入瑣碎的匿名函式 x->f(x)
由於高階函式通常會呼叫匿名函式,因此很容易得出這樣的結論:這是可取的,甚至是有必要的。但是,任何函式都可以直接傳遞,而無需「包裝」在匿名函式中。不要撰寫 map(x->f(x), a)
,請撰寫 map(f, a)
。
在可能的情況下,避免在一般程式碼中使用浮點數作為數字文字
如果你撰寫處理數字的一般程式碼,而且預期會執行許多不同的數字類型引數,請嘗試使用數字類型文字,透過提升盡可能少地影響引數。
例如,
julia> f(x) = 2.0 * x
f (generic function with 1 method)
julia> f(1//2)
1.0
julia> f(1/2)
1.0
julia> f(1)
2.0
雖然
julia> g(x) = 2 * x
g (generic function with 1 method)
julia> g(1//2)
1//1
julia> g(1/2)
1.0
julia> g(1)
2
如你所見,我們使用 Int
文字的第二個版本保留了輸入引數的類型,而第一個版本則沒有。這是因為例如 promote_type(Int, Float64) == Float64
,而且提升會隨著乘法而發生。類似地,Rational
文字比 Float64
文字更少破壞類型,但比 Int
更具破壞性
julia> h(x) = 2//1 * x
h (generic function with 1 method)
julia> h(1//2)
1//1
julia> h(1/2)
1.0
julia> h(1)
2//1
因此,儘可能使用Int
字面值,對於字面非整數數字,使用Rational{Int}
,以便於使用您的程式碼。