介面

Julia 中許多功能和可擴充性來自非正式介面的集合。透過擴充特定方法來處理自訂類型,該類型的物件不僅會接收那些功能,而且還可以在撰寫為通用建構在那些行為之上的其他方法中使用。

反覆運算

必要方法簡要說明
iterate(iter)傳回第一個項目和初始狀態的元組,或如果為空則傳回 nothing
iterate(iter, state)傳回下一項和下一個狀態的元組,或如果沒有剩餘項目,則傳回nothing
重要的選用方法預設定義簡要說明
Base.IteratorSize(IterType)Base.HasLength()適當的Base.HasLength()Base.HasShape{N}()Base.IsInfinite()Base.SizeUnknown()之一
Base.IteratorEltype(IterType)Base.HasEltype()適當的Base.EltypeUnknown()Base.HasEltype()之一
eltype(IterType)Anyiterate()傳回的元組第一個項目的類型
length(iter)(未定義)項目的數量(如果已知)
size(iter, [dim])(未定義)每個維度中項目的數量(如果已知)
Base.isdone(iter[, state])遺失迭代器完成的快速路徑提示。應為有狀態迭代器定義,否則isempty(iter)可能會呼叫iterate(iter[, state])並變更迭代器。
IteratorSize(IterType)傳回的值必要的方法
Base.HasLength()length(iter)
Base.HasShape{N}()length(iter)size(iter, [dim])
Base.IsInfinite()()
Base.SizeUnknown()()
IteratorEltype(IterType)傳回的值必要的方法
Base.HasEltype()eltype(IterType)
Base.EltypeUnknown()()

順序迭代由iterate函數實作。Julia 迭代器不會在迭代物件時變更物件,而是從物件外部追蹤迭代狀態。iterate 的傳回值永遠是值和狀態的元組,或如果沒有剩餘元素,則傳回nothing。狀態物件會在下次迭代傳回 iterate 函數,通常被視為可迭代物件的實作細節。

定義此函數的任何物件都是可迭代的,且可用於許多依賴於迭代的函數中。由於語法

for item in iter   # or  "for item = iter"
    # body
end

轉換為

next = iterate(iter)
while next !== nothing
    (item, state) = next
    # body
    next = iterate(iter, state)
end

一個簡單的範例是具有定義長度的平方數可迭代序列

julia> struct Squares
           count::Int
       end

julia> Base.iterate(S::Squares, state=1) = state > S.count ? nothing : (state*state, state+1)

僅使用iterate定義,Squares類型已經相當強大。我們可以迭代所有元素

julia> for item in Squares(7)
           println(item)
       end
1
4
9
16
25
36
49

我們可以使用許多與可迭代物件一起運作的內建方法,例如insum

julia> 25 in Squares(10)
true

julia> sum(Squares(100))
338350

我們可以延伸更多方法,以提供 Julia 有關此可迭代集合的更多資訊。我們知道Squares序列中的元素將永遠是Int。透過延伸eltype方法,我們可以將該資訊提供給 Julia,並協助它在更複雜的方法中建立更專業的程式碼。我們也知道序列中的元素數量,因此我們也可以延伸length

julia> Base.eltype(::Type{Squares}) = Int # Note that this is defined for the type

julia> Base.length(S::Squares) = S.count

現在,當我們要求 Julia 將所有元素collect到陣列中時,它可以預先配置正確大小的Vector{Int},而不是天真地將每個元素push!Vector{Any}

julia> collect(Squares(4))
4-element Vector{Int64}:
  1
  4
  9
 16

雖然我們可以依賴於一般實作,但我們也可以延伸特定方法,在這些方法中我們知道有一個更簡單的演算法。例如,有一個公式可以計算平方和,因此我們可以使用效能更高的解法來覆寫一般迭代版本

julia> Base.sum(S::Squares) = (n = S.count; return n*(n+1)*(2n+1)÷6)

julia> sum(Squares(1803))
1955361914

這是整個 Julia Base 中非常常見的模式:一組小型的必要方法定義一個非正式介面,可啟用許多更花俏的行為。在某些情況下,當類型知道可以在其特定情況下使用更有效率的演算法時,就會希望進一步專業化這些額外行為。

反向順序中反覆運算集合時,通常允許反覆運算 Iterators.reverse(iterator) 來反覆運算。然而,若要實際支援反向順序反覆運算,反覆運算類型 T 需要為 Iterators.Reverse{T} 實作 iterate。(給定 r::Iterators.Reverse{T},類型為 T 的底層反覆運算為 r.itr。)在我們的 Squares 範例中,我們將實作 Iterators.Reverse{Squares} 方法

julia> Base.iterate(rS::Iterators.Reverse{Squares}, state=rS.itr.count) = state < 1 ? nothing : (state*state, state-1)

julia> collect(Iterators.reverse(Squares(4)))
4-element Vector{Int64}:
 16
  9
  4
  1

索引

要實作的方法簡要說明
getindex(X, i)X[i],索引元素存取
setindex!(X, v, i)X[i] = v,索引指派
firstindex(X)第一個索引,用於 X[begin]
lastindex(X)最後一個索引,用於 X[end]

對於上述的 Squares 可迭代物件,我們可以輕鬆地透過平方計算序列的第 i 個元素。我們可以將此公開為索引表達式 S[i]。若要選擇此行為,Squares 只需要定義 getindex

julia> function Base.getindex(S::Squares, i::Int)
           1 <= i <= S.count || throw(BoundsError(S, i))
           return i*i
       end

julia> Squares(100)[23]
529

此外,若要支援語法 S[begin]S[end],我們必須定義 firstindexlastindex,分別指定第一個和最後一個有效的索引

julia> Base.firstindex(S::Squares) = 1

julia> Base.lastindex(S::Squares) = length(S)

julia> Squares(23)[end]
529

例如,對於多維度的 begin/end 索引,例如 a[3, begin, 7],您應該定義 firstindex(a, dim)lastindex(a, dim)(分別預設為對 axes(a, dim) 呼叫 firstlast)。

不過,請注意,上述定義了具有單個整數索引的 getindex。使用非 Int 進行索引會引發 MethodError,表示沒有匹配的方法。為了支援使用 Int 範圍或向量的索引,必須撰寫個別的方法

julia> Base.getindex(S::Squares, i::Number) = S[convert(Int, i)]

julia> Base.getindex(S::Squares, I) = [S[i] for i in I]

julia> Squares(10)[[3,4.,5]]
3-element Vector{Int64}:
  9
 16
 25

雖然這開始支援更多 內建類型支援的索引運算,但仍有許多行為遺漏。隨著我們新增行為,這個 Squares 序列看起來越來越像一個向量。我們可以正式將它定義為 AbstractArray 的子類型,而不是自己定義所有這些行為。

抽象陣列

要實作的方法簡要說明
size(A)傳回包含 A 維度的元組
getindex(A, i::Int)(如果為 IndexLinear) 線性標量索引
getindex(A, I::Vararg{Int, N})(如果為 IndexCartesian,其中 N = ndims(A)) N 維標量索引
選用方法預設定義簡要說明
IndexStyle(::Type)IndexCartesian()傳回 IndexLinear()IndexCartesian()。請參閱以下說明。
setindex!(A, v, i::Int)(如果為 IndexLinear) 標量索引指定
setindex!(A, v, I::Vararg{Int, N})(如果為 IndexCartesian,其中 N = ndims(A)) N 維標量索引指定
getindex(A, I...)根據標量 getindex 定義多維和非標量索引
setindex!(A, X, I...)根據標量 setindex! 定義多維和非標量索引指定
iterate根據標量 getindex 定義反覆運算
length(A)prod(size(A))元素數量
similar(A)similar(A, eltype(A), size(A))傳回一個可變陣列,具有相同的形狀和元素類型
similar(A, ::Type{S})similar(A, S, size(A))傳回一個可變陣列,具有相同的形狀和指定的元素類型
similar(A, dims::Dims)similar(A, eltype(A), dims)傳回一個可變陣列,具有相同的元素類型和大小 dims
similar(A, ::Type{S}, dims::Dims)Array{S}(undef, dims)傳回一個可變陣列,具有指定的元素類型和大小
非傳統索引預設定義簡要說明
axes(A)map(OneTo, size(A))傳回一個有效的索引 AbstractUnitRange{<:Integer} 的元組。軸應該是它們自己的軸,也就是說 axes.(axes(A),1) == axes(A) 應該成立。
similar(A, ::Type{S}, inds)similar(A, S, Base.to_shape(inds))傳回一個可變陣列,具有指定的索引 inds(見下方)
similar(T::Union{Type,Function}, inds)T(Base.to_shape(inds))傳回一個類似於 T 的陣列,具有指定的索引 inds(見下方)

如果一個類型被定義為 AbstractArray 的子類型,它將繼承一組非常豐富的行為,包括建立在單元素存取之上的迭代和多維索引。請參閱 陣列手冊頁面Julia Base 部分 以取得更多支援的方法。

定義 AbstractArray 子類型的關鍵部分是 IndexStyle。由於索引是陣列中如此重要的部分,而且經常出現在熱迴圈中,因此讓索引和索引指定盡可能有效率非常重要。陣列資料結構通常以兩種方式之一定義:它最有效率地使用一個索引(線性索引)存取其元素,或者它本質上使用為每個維度指定的索引存取元素。這兩種方式被 Julia 識別為 IndexLinear()IndexCartesian()。將線性索引轉換為多重索引子腳本通常非常昂貴,因此這提供了一種基於特質的機制,以針對所有陣列類型啟用有效率的通用程式碼。

此區別決定類型必須定義哪些標量索引方法。IndexLinear() 陣列很簡單:只需定義 getindex(A::ArrayType, i::Int)。當陣列隨後使用多維度索引集進行索引時,後備 getindex(A::AbstractArray, I...) 會有效率地將索引轉換為一個線性索引,然後呼叫上述方法。另一方面,IndexCartesian() 陣列需要為每個支援的維度定義方法,其具有 ndims(A) Int 索引。例如,SparseArrays 標準函式庫模組中的 SparseMatrixCSC 僅支援兩個維度,因此它僅定義 getindex(A::SparseMatrixCSC, i::Int, j::Int)setindex! 也是如此。

回到上述的平方序列,我們可以改將其定義為 AbstractArray{Int, 1} 的子類型

julia> struct SquaresVector <: AbstractArray{Int, 1}
           count::Int
       end

julia> Base.size(S::SquaresVector) = (S.count,)

julia> Base.IndexStyle(::Type{<:SquaresVector}) = IndexLinear()

julia> Base.getindex(S::SquaresVector, i::Int) = i*i

請注意,指定 AbstractArray 的兩個參數非常重要;第一個定義 eltype,第二個定義 ndims。該超類型和這三個方法是 SquaresVector 成為可迭代、可索引且完全功能的陣列所需的全部

julia> s = SquaresVector(4)
4-element SquaresVector:
  1
  4
  9
 16

julia> s[s .> 8]
2-element Vector{Int64}:
  9
 16

julia> s + s
4-element Vector{Int64}:
  2
  8
 18
 32

julia> sin.(s)
4-element Vector{Float64}:
  0.8414709848078965
 -0.7568024953079282
  0.4121184852417566
 -0.2879033166650653

作為一個更複雜的範例,讓我們在 Dict 的基礎上定義我們自己的玩具 N 維稀疏陣列類型

julia> struct SparseArray{T,N} <: AbstractArray{T,N}
           data::Dict{NTuple{N,Int}, T}
           dims::NTuple{N,Int}
       end

julia> SparseArray(::Type{T}, dims::Int...) where {T} = SparseArray(T, dims);

julia> SparseArray(::Type{T}, dims::NTuple{N,Int}) where {T,N} = SparseArray{T,N}(Dict{NTuple{N,Int}, T}(), dims);

julia> Base.size(A::SparseArray) = A.dims

julia> Base.similar(A::SparseArray, ::Type{T}, dims::Dims) where {T} = SparseArray(T, dims)

julia> Base.getindex(A::SparseArray{T,N}, I::Vararg{Int,N}) where {T,N} = get(A.data, I, zero(T))

julia> Base.setindex!(A::SparseArray{T,N}, v, I::Vararg{Int,N}) where {T,N} = (A.data[I] = v)

請注意,這是一個 IndexCartesian 陣列,因此我們必須手動定義 getindexsetindex! 在陣列的維度。與 SquaresVector 不同,我們能夠定義 setindex!,因此我們可以變異陣列

julia> A = SparseArray(Float64, 3, 3)
3×3 SparseArray{Float64, 2}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> fill!(A, 2)
3×3 SparseArray{Float64, 2}:
 2.0  2.0  2.0
 2.0  2.0  2.0
 2.0  2.0  2.0

julia> A[:] = 1:length(A); A
3×3 SparseArray{Float64, 2}:
 1.0  4.0  7.0
 2.0  5.0  8.0
 3.0  6.0  9.0

AbstractArray 進行索引的結果本身可以是一個陣列(例如,當按 AbstractRange 進行索引時)。AbstractArray 後備方法使用 similar 分配適當大小和元素類型的 Array,該 Array 使用上述描述的基本索引方法填入。但是,在實作陣列封裝器時,您通常希望結果也能被封裝

julia> A[1:2,:]
2×3 SparseArray{Float64, 2}:
 1.0  4.0  7.0
 2.0  5.0  8.0

在此範例中,透過定義 Base.similar(A::SparseArray, ::Type{T}, dims::Dims) where T 來建立適當的包裝陣列,即可完成此操作。(請注意,儘管 similar 支援 1 個和 2 個引數形式,但大多數情況下,您只需要專門化 3 個引數形式即可。)為使此方法運作,SparseArray 必須是可變的(支援 setindex!)。為 SparseArray 定義 similargetindexsetindex! 也能讓您 copy 陣列

julia> copy(A)
3×3 SparseArray{Float64, 2}:
 1.0  4.0  7.0
 2.0  5.0  8.0
 3.0  6.0  9.0

除了上述所有可迭代和可索引的方法外,這些類型還能互相互動,並使用 Julia Base 中為 AbstractArrays 定義的大部分方法

julia> A[SquaresVector(3)]
3-element SparseArray{Float64, 1}:
 1.0
 4.0
 9.0

julia> sum(A)
45.0

如果您正在定義允許非傳統索引(從 1 以外的數字開始的索引)的陣列類型,您應該專門化 axes。您也應該專門化 similar,以便 dims 引數(通常是 Dims 大小元組)可以接受 AbstractUnitRange 物件,可能是您自己設計的範圍類型 Ind。如需更多資訊,請參閱 具有自訂索引的陣列

跨步陣列

要實作的方法簡要說明
strides(A)傳回每個維度中相鄰元素之間在記憶體中的距離(以元素數為單位),作為元組。如果 AAbstractArray{T,0},這應該會傳回一個空元組。
Base.unsafe_convert(::Type{Ptr{T}}, A)傳回陣列的原生位址。
Base.elsize(::Type{<:A})傳回陣列中連續元素之間的跨步。
選用方法預設定義簡要說明
stride(A, i::Int)strides(A)[i]傳回維度 k 中相鄰元素之間在記憶體中的距離(以元素數為單位)。

跨步陣列是 AbstractArray 的子類型,其條目以固定的跨步儲存在記憶體中。只要陣列的元素類型與 BLAS 相容,跨步陣列就能利用 BLAS 和 LAPACK 常式,以執行更有效率的線性代數常式。使用者定義跨步陣列的一個典型範例,就是將標準 Array 包裝在額外的結構中。

警告:如果底層儲存實際上並非跨步,請勿實作這些方法,因為這可能會導致不正確的結果或區段錯誤。

以下是幾個範例,用來展示哪些類型的陣列是跨步的,哪些不是

1:5   # not strided (there is no storage associated with this array.)
Vector(1:5)  # is strided with strides (1,)
A = [1 5; 2 6; 3 7; 4 8]  # is strided with strides (1,4)
V = view(A, 1:2, :)   # is strided with strides (1,4)
V = view(A, 1:2:3, 1:2)   # is strided with strides (2,4)
V = view(A, [1,2,4], :)   # is not strided, as the spacing between rows is not fixed.

自訂廣播

要實作的方法簡要說明
Base.BroadcastStyle(::Type{SrcType}) = SrcStyle()SrcType 的廣播行為
Base.similar(bc::Broadcasted{DestStyle}, ::Type{ElType})輸出容器的配置
選用方法
Base.BroadcastStyle(::Style1, ::Style2) = Style12()混合樣式的優先順序規則
Base.axes(x)x 的索引宣告,根據 axes(x)
Base.broadcastable(x)x 轉換為具有 axes 且支援索引的物件
繞過預設機制
Base.copy(bc::Broadcasted{DestStyle})broadcast 的自訂實作
Base.copyto!(dest, bc::Broadcasted{DestStyle})broadcast! 的自訂實作,專門針對 DestStyle
Base.copyto!(dest::DestType, bc::Broadcasted{Nothing})broadcast! 的自訂實作,專門針對 DestType
Base.Broadcast.broadcasted(f, args...)覆寫融合運算式中的預設延遲行為
Base.Broadcast.instantiate(bc::Broadcasted{DestStyle})覆寫延遲廣播的軸線計算

廣播是由對 broadcastbroadcast! 的明確呼叫觸發,或由「點」運算(例如 A .+ bf.(x, y))隱含觸發。任何具有 axes 且支援索引的物件都可以作為廣播中的引數,而預設情況下,結果會儲存在 Array 中。這個基本架構可以在三個主要方面進行擴充

  • 確保所有引數都支援廣播
  • 針對給定的引數集選擇適當的輸出陣列
  • 針對給定的引數集選擇有效的實作

並非所有類型都支援 axes 和索引,但許多類型允許在廣播中使用。針對每個要廣播的引數呼叫 Base.broadcastable 函數,允許它傳回支援 axes 和索引的不同內容。預設情況下,這是所有 AbstractArrayNumber 的身分函數,它們已支援 axes 和索引。

如果一個類型打算作為「0 維度純量」(單一物件)而不是廣播的容器,則應定義下列方法

Base.broadcastable(o::MyType) = Ref(o)

將引數封裝在 0 維度 Ref 容器中。例如,此類封裝方法定義給類型本身、函數、特殊單例,例如 missingnothing,以及日期。

自訂的陣列類型可以專門化 Base.broadcastable 來定義其形狀,但它們應遵循慣例,即 collect(Base.broadcastable(x)) == collect(x)。一個值得注意的例外是 AbstractString;字串為特殊情況,即使它們是其字元的可迭代集合,但它們在廣播目的中仍表現為純量(請參閱 字串 以取得更多資訊)。

後續兩個步驟(選擇輸出陣列和實作)取決於為一組給定的引數確定單一解答。廣播必須採用所有不同類型的引數,並將它們簡化為只有一個輸出陣列和一個實作。廣播將這個單一解答稱為「樣式」。每個可廣播物件都有其自己的偏好樣式,並使用類似提升的系統將這些樣式組合成單一解答,也就是「目標樣式」。

廣播樣式

Base.BroadcastStyle 是所有廣播樣式衍生的抽象類型。當用作函數時,它有兩種可能的形式,單元(單一引數)和二元。單元變體表示您打算實作特定的廣播行為和/或輸出類型,並且不希望依賴預設後備 Broadcast.DefaultArrayStyle

若要覆寫這些預設值,您可以為您的物件定義自訂的 BroadcastStyle

struct MyStyle <: Broadcast.BroadcastStyle end
Base.BroadcastStyle(::Type{<:MyType}) = MyStyle()

在某些情況下,不必定義 MyStyle 會比較方便,這種情況下您可以利用其中一個一般廣播包裝器

  • Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.Style{MyType}() 可用於任意類型。
  • 如果 MyTypeAbstractArray,則建議使用 Base.BroadcastStyle(::Type{<:MyType}) = Broadcast.ArrayStyle{MyType}()
  • 對於僅支援特定維度的 AbstractArrays,請建立 Broadcast.AbstractArrayStyle{N} 的子類型(請見下方)。

當您的廣播運算涉及多個引數時,個別引數樣式會結合起來,以確定單一的 DestStyle,用於控制輸出容器的類型。如需更多詳細資訊,請參閱 下方

選擇適當的輸出陣列

每項廣播運算都會計算廣播樣式,以允許調度和專門化。結果陣列的實際配置由 similar 處理,並使用廣播物件作為其第一個引數。

Base.similar(bc::Broadcasted{DestStyle}, ::Type{ElType})

備用定義為

similar(bc::Broadcasted{DefaultArrayStyle{N}}, ::Type{ElType}) where {N,ElType} =
    similar(Array{ElType}, axes(bc))

不過,如有需要,你可以針對任何或所有這些參數進行專門化。最後一個參數 bc 是(潛在融合)廣播運算的惰性表示,一個 Broadcasted 物件。對於這些目的,包裝器最重要的欄位是 fargs,分別描述函數和參數清單。請注意,參數清單可以(而且通常會)包含其他巢狀 Broadcasted 包裝器。

舉一個完整的範例,假設你建立了一個類型 ArrayAndChar,用來儲存陣列和單一字元

struct ArrayAndChar{T,N} <: AbstractArray{T,N}
    data::Array{T,N}
    char::Char
end
Base.size(A::ArrayAndChar) = size(A.data)
Base.getindex(A::ArrayAndChar{T,N}, inds::Vararg{Int,N}) where {T,N} = A.data[inds...]
Base.setindex!(A::ArrayAndChar{T,N}, val, inds::Vararg{Int,N}) where {T,N} = A.data[inds...] = val
Base.showarg(io::IO, A::ArrayAndChar, toplevel) = print(io, typeof(A), " with char '", A.char, "'")

你可能希望廣播保留 char「元資料」。我們首先定義

Base.BroadcastStyle(::Type{<:ArrayAndChar}) = Broadcast.ArrayStyle{ArrayAndChar}()

這表示我們也必須定義一個對應的 similar 方法

function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{ArrayAndChar}}, ::Type{ElType}) where ElType
    # Scan the inputs for the ArrayAndChar:
    A = find_aac(bc)
    # Use the char field of A to create the output
    ArrayAndChar(similar(Array{ElType}, axes(bc)), A.char)
end

"`A = find_aac(As)` returns the first ArrayAndChar among the arguments."
find_aac(bc::Base.Broadcast.Broadcasted) = find_aac(bc.args)
find_aac(args::Tuple) = find_aac(find_aac(args[1]), Base.tail(args))
find_aac(x) = x
find_aac(::Tuple{}) = nothing
find_aac(a::ArrayAndChar, rest) = a
find_aac(::Any, rest) = find_aac(rest)

根據這些定義,可以得到以下行為

julia> a = ArrayAndChar([1 2; 3 4], 'x')
2×2 ArrayAndChar{Int64, 2} with char 'x':
 1  2
 3  4

julia> a .+ 1
2×2 ArrayAndChar{Int64, 2} with char 'x':
 2  3
 4  5

julia> a .+ [5,10]
2×2 ArrayAndChar{Int64, 2} with char 'x':
  6   7
 13  14

使用自訂實作擴充廣播

一般而言,廣播運算會以一個延遲的 Broadcasted 容器表示,其中包含要套用的函數及其參數。這些參數本身可能是更巢狀的 Broadcasted 容器,形成一個要評估的大型表達式樹。Broadcasted 容器的巢狀樹是由隱含的點語法直接建構的;例如,5 .+ 2.*x 會暫時表示為 Broadcasted(+, 5, Broadcasted(*, 2, x))。這對使用者來說是看不見的,因為它會立即透過呼叫 copy 來實現,但正是這個容器為自訂型別的作者提供了廣播可擴充性的基礎。內建的廣播機制會根據參數來決定結果型別和大小,分配它,然後最後將 Broadcasted 物件的實現複製到其中,並使用預設的 copyto!(::AbstractArray, ::Broadcasted) 方法。內建的後備 broadcastbroadcast! 方法會以類似的方式建構一個暫時性的 Broadcasted 運算表示,以便它們可以遵循相同的程式碼路徑。這允許自訂陣列實作提供自己的 copyto! 專門化,以自訂和最佳化廣播。這再次由計算出的廣播樣式決定。這是運算中如此重要的部分,因此它會儲存在 Broadcasted 型別的第一個型別參數中,允許進行分派和專門化。

對於某些類型,用於「融合」嵌套廣播層級運算的機器不可用,或以遞增方式執行會更有效率。在這種情況下,您可能需要或希望將 x .* (x .+ 1) 評估為 broadcast(*, x, broadcast(+, x, 1)),其中在處理外部運算之前會評估內部運算。此類急切運算直接受到一點間接作用的支援;Julia 沒有直接建構 Broadcasted 物件,而是將融合的運算式 x .* (x .+ 1) 降低為 Broadcast.broadcasted(*, x, Broadcast.broadcasted(+, x, 1))。現在,預設情況下,broadcasted 只會呼叫 Broadcasted 建構函式來建立融合運算式樹的延遲表示法,但您可以選擇針對特定函式和引數組合覆寫它。

舉例來說,內建的 AbstractRange 物件使用此機器來最佳化廣播運算式的部分,這些部分可以純粹根據起點、步長和長度(或終點)急切評估,而不是計算每個單一元素。就像所有其他機器一樣,broadcasted 也會計算並公開其引數的組合廣播樣式,因此,您不必針對 broadcasted(f, args...) 進行專門化,而是可以針對任何樣式、函式和引數組合對 broadcasted(::DestStyle, f, args...) 進行專門化。

例如,下列定義支援範圍的否定

broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::OrdinalRange) = range(-first(r), step=-step(r), length=length(r))

延伸原地廣播

可以透過定義適當的 copyto!(dest, bc::Broadcasted) 方法來支援原地廣播。由於您可能想要針對 destbc 的特定子類型進行專門化,因此,為了避免套件之間的歧義,我們建議採用下列慣例。

如果您希望針對特定樣式 DestStyle 進行專門化,請定義一個方法,如下所示

copyto!(dest, bc::Broadcasted{DestStyle})

選擇性地,使用此表單,您也可以針對 dest 的類型進行專門化。

如果您反而想要針對目標類型 DestType 進行專門化,而不針對 DestStyle 進行專門化,那麼您應該定義一個具有下列簽章的方法

copyto!(dest::DestType, bc::Broadcasted{Nothing})

這利用了 copyto! 的備用實作,將包裝器轉換為 Broadcasted{Nothing}。因此,針對 DestType 的特化優先權低於針對 DestStyle 特化的函式。

類似地,您可以使用 copy(::Broadcasted) 函式完全覆寫非原位廣播。

處理 Broadcasted 物件

當然,為了實作這樣的 copycopyto! 函式,您必須使用 Broadcasted 包裝器來計算每個元素。有兩種主要方法可以做到這一點

  • Broadcast.flatten 將潛在的巢狀運算重新計算為單一函式和引數的平面清單。您必須自行實作廣播形狀規則,但在某些情況下這可能會有用。
  • 迭代 axes(::Broadcasted)CartesianIndices,並使用索引和產生的 CartesianIndex 物件來計算結果。

撰寫二元廣播規則

優先權規則由二元 BroadcastStyle 呼叫定義

Base.BroadcastStyle(::Style1, ::Style2) = Style12()

其中 Style12 是您想要為涉及 Style1Style2 引數的輸出選擇的 BroadcastStyle。例如,

Base.BroadcastStyle(::Broadcast.Style{Tuple}, ::Broadcast.AbstractArrayStyle{0}) = Broadcast.Style{Tuple}()

表示 Tuple「勝過」零維陣列(輸出容器將會是元組)。值得注意的是,您不需要(也不應該)定義此呼叫的兩個引數順序;定義一個就足夠了,無論使用者以何種順序提供引數。

對於 AbstractArray 類型,定義 BroadcastStyle 會取代後備選項,Broadcast.DefaultArrayStyleDefaultArrayStyle 和抽象超類型 AbstractArrayStyle 會將維度儲存為類型參數,以支援具有固定維度需求的特殊陣列類型。

DefaultArrayStyle 會「輸給」任何其他已定義的 AbstractArrayStyle,原因在於下列方法

BroadcastStyle(a::AbstractArrayStyle{Any}, ::DefaultArrayStyle) = a
BroadcastStyle(a::AbstractArrayStyle{N}, ::DefaultArrayStyle{N}) where N = a
BroadcastStyle(a::AbstractArrayStyle{M}, ::DefaultArrayStyle{N}) where {M,N} =
    typeof(a)(Val(max(M, N)))

除非您想為兩個或多個非 DefaultArrayStyle 類型建立優先順序,否則您不需要撰寫二進位 BroadcastStyle 規則。

如果您的陣列類型確實有固定維度需求,則您應該將 AbstractArrayStyle 子類型化。例如,稀疏陣列程式碼有下列定義

struct SparseVecStyle <: Broadcast.AbstractArrayStyle{1} end
struct SparseMatStyle <: Broadcast.AbstractArrayStyle{2} end
Base.BroadcastStyle(::Type{<:SparseVector}) = SparseVecStyle()
Base.BroadcastStyle(::Type{<:SparseMatrixCSC}) = SparseMatStyle()

每當您將 AbstractArrayStyle 子類型化時,您也需要定義結合維度的規則,方法是為您的樣式建立一個建構函數,該函數會採用 Val(N) 參數。例如

SparseVecStyle(::Val{0}) = SparseVecStyle()
SparseVecStyle(::Val{1}) = SparseVecStyle()
SparseVecStyle(::Val{2}) = SparseMatStyle()
SparseVecStyle(::Val{N}) where N = Broadcast.DefaultArrayStyle{N}()

這些規則表示將 SparseVecStyle 與 0 或 1 維陣列結合會產生另一個 SparseVecStyle,將其與 2 維陣列結合會產生 SparseMatStyle,而任何更高維度的陣列都會回退到密集任意維度架構。這些規則允許廣播保留稀疏表示,以進行會產生一或二維輸出的運算,但會為任何其他維度產生 Array

實例屬性

要實作的方法預設定義簡要說明
propertynames(x::ObjType, private::Bool=false)fieldnames(typeof(x))傳回物件 x 的屬性 (x.property) 的元組。如果 private=true,也傳回預計保持為私有的屬性名稱
getproperty(x::ObjType, s::Symbol)getfield(x, s)傳回 x 的屬性 sx.s 會呼叫 getproperty(x, :s)
setproperty!(x::ObjType, s::Symbol, v)setfield!(x, s, v)x 的屬性 s 設定為 vx.s = v 會呼叫 setproperty!(x, :s, v)。應該傳回 v

有時,會希望變更最終使用者與物件欄位的互動方式。與其授予直接存取類型欄位的權限,可以使用超載 object.field 來提供使用者與程式碼之間的額外抽象層。屬性是使用者看到物件的內容,欄位則是物件實際上的內容。

預設情況下,屬性和欄位相同。不過,可以變更此行為。例如,採用 極座標 來表示平面上的一個點

julia> mutable struct Point
           r::Float64
           ϕ::Float64
       end

julia> p = Point(7.0, pi/4)
Point(7.0, 0.7853981633974483)

如上表所述,點存取 p.rgetproperty(p, :r) 相同,而 getproperty(p, :r) 預設與 getfield(p, :r) 相同

julia> propertynames(p)
(:r, :ϕ)

julia> getproperty(p, :r), getproperty(p, :ϕ)
(7.0, 0.7853981633974483)

julia> p.r, p.ϕ
(7.0, 0.7853981633974483)

julia> getfield(p, :r), getproperty(p, :ϕ)
(7.0, 0.7853981633974483)

不過,我們可能希望使用者不知道 Point 將座標儲存在 rϕ (欄位) 中,而是與 xy (屬性) 進行互動。第一欄中的方法可以定義為新增新功能

julia> Base.propertynames(::Point, private::Bool=false) = private ? (:x, :y, :r, :ϕ) : (:x, :y)

julia> function Base.getproperty(p::Point, s::Symbol)
           if s === :x
               return getfield(p, :r) * cos(getfield(p, :ϕ))
           elseif s === :y
               return getfield(p, :r) * sin(getfield(p, :ϕ))
           else
               # This allows accessing fields with p.r and p.ϕ
               return getfield(p, s)
           end
       end

julia> function Base.setproperty!(p::Point, s::Symbol, f)
           if s === :x
               y = p.y
               setfield!(p, :r, sqrt(f^2 + y^2))
               setfield!(p, :ϕ, atan(y, f))
               return f
           elseif s === :y
               x = p.x
               setfield!(p, :r, sqrt(x^2 + f^2))
               setfield!(p, :ϕ, atan(f, x))
               return f
           else
               # This allow modifying fields with p.r and p.ϕ
               return setfield!(p, s, f)
           end
       end

很重要的一點是,getfieldsetfield 必須在 getpropertysetproperty! 內部使用,而不是使用點語法,因為點語法會使函式遞迴,這可能會導致類型推論問題。現在,我們可以試用新功能

julia> propertynames(p)
(:x, :y)

julia> p.x
4.949747468305833

julia> p.y = 4.0
4.0

julia> p.r
6.363961030678928

最後,值得注意的是,在 Julia 中很少會新增此類實例屬性,而且通常只有在有充分理由時才這麼做。