風格指南

以下各節說明慣用語法 Julia 編碼風格的幾個面向。這些規則都不是絕對的;它們只是幫助您熟悉此語言和在各種替代設計中進行選擇的建議。

縮排

每個縮排層級使用 4 個空格。

撰寫函式,而非僅是指令碼

將程式碼寫成頂層的一系列步驟是快速開始解決問題的方法,但您應盡快嘗試將程式分為函式。函式更易於重複使用和測試,並釐清正在執行的步驟及其輸入和輸出。此外,由於 Julia 編譯器的工作方式,函式中的程式碼往往執行得比頂層程式碼快很多。

另外值得強調的是,函式應採用引數,而非直接對全域變數進行運算(除了常數,例如 pi)。

避免撰寫過於具體的類型

程式碼應盡可能通用。不要撰寫

Complex{Float64}(x)

最好使用可用的通用函數

complex(float(x))

第二個版本會將 x 轉換為適當的類型,而不是總是相同的類型。

此樣式點特別與函數參數相關。例如,如果參數實際上可以是任何整數,則不要宣告參數的類型為 IntInt32,而應使用抽象類型 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 在整個過程中都使用此慣例,並包含具有複製和修改形式的函數範例(例如,sortsort!),以及其他僅修改的函數(例如,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 方法,讓其他類型擁有自己的實作。

類似地,非匯出函式通常是內部的,並會變更,除非文件另有說明。名稱有時會加上 _ 字首(或字尾)以進一步暗示某個東西是「內部」或實作細節,但這並非規則。

此規則的反例包括 NamedTupleRegexMatchStatStruct

使用與 Julia base/ 一致的命名慣例

  • 模組和類型名稱使用大寫字母和駝峰式大小寫:module SparseArraysstruct UnitRange
  • 函數使用小寫字母(maximumconvert),且在可讀取的情況下,將多個單字壓縮在一起(isequalhaskey)。必要時,使用底線作為單字分隔符號。底線也用於表示概念的組合(remotecall_fetch 作為 fetch(remotecall(...)) 的更有效率的實作)或作為修飾詞。
  • 至少變更一個引數的函數以 ! 結尾。
  • 簡潔性很重要,但避免縮寫(indexin 而不是 indxin),因為很難記住特定單字是否以及如何縮寫。

如果函數名稱需要多個單字,請考慮它是否可能表示多個概念,並且可能最好將其拆分成多個部分。

撰寫具有與 Julia Base 類似的引數順序的函數

一般而言,Base 函式庫對函數使用下列引數順序,視情況而定

  1. 函數參數。將函數參數放在最前面允許使用 do 區塊來傳遞多行匿名函數。

  2. I/O 串流。將 IO 物件指定在最前面允許將函數傳遞給函數,例如 sprint,例如 sprint(show, x)

  3. 正在變異的輸入。例如,在 fill!(x, v) 中,x 是正在變異的物件,它出現在要插入到 x 中的值之前。

  4. 類型。傳遞類型通常表示輸出將具有給定的類型。在 parse(Int, "1") 中,類型出現在要解析的字串之前。有許多這樣的範例,其中類型出現在最前面,但值得注意的是,在 read(io, String) 中,IO 參數出現在類型之前,這與這裡概述的順序一致。

  5. 未變異的輸入。在 fill!(x, v) 中,v 沒有 正在變異,它出現在 x 之後。

  6. 。對於關聯式集合,這是鍵值對的鍵。對於其他索引集合,這是索引。

  7. 。對於關聯式集合,這是鍵值對的值。在像 fill!(x, v) 這樣的案例中,這是 v

  8. 其他所有。任何其他參數。

  9. 變數參數。這是指可以在函數呼叫結尾無限列出的參數。例如,在 Matrix{T}(undef, dims) 中,維度可以作為 Tuple 給出,例如 Matrix{T}(undef, (1,2)),或作為 Vararg 給出,例如 Matrix{T}(undef, 1, 2)

  10. 關鍵字參數。在 Julia 中,關鍵字參數在函數定義中必須放在最後;它們在此處列出只是為了完整性。

絕大多數函數不會採用上面列出的每一種參數;這些數字僅表示應對函數的任何適用參數使用的優先順序。

當然有一些例外。例如,在 convert 中,類型應始終排在第一位。在 setindex! 中,值出現在索引之前,以便可以將索引作為變參提供。

在設計 API 時,儘可能遵守此一般順序可能會為函數使用者提供更一致的體驗。

不要過度使用 try-catch

避免錯誤優於依賴捕捉錯誤。

不要對條件使用括號

Julia 不需要在 ifwhile 中的條件周圍加上括號。撰寫

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},以便於使用您的程式碼。