接口

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])missing迭代器完成的快速路径提示。应为有状态迭代器定义,否则 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 循环中使用,因为语法

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 基础库中非常常见的一种模式:一小组必需方法定义了一个非正式接口,该接口使许多更高级的功能成为可能。在某些情况下,类型希望在知道可以使用更有效的算法时额外专门化这些额外行为。

通过迭代 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!定义多维和非标量索引赋值
迭代根据标量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数组,因此我们必须手动定义ndims(A)维度的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,该数组使用上面描述的基本索引方法填充。但是,在实现数组包装器时,您通常希望结果也进行包装。

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

除了上面所有可迭代和可索引方法之外,这些类型还可以相互交互并使用为AbstractArrays在 Julia Base 中定义的大多数方法。

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)根据axes(x)声明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处理,使用Broadcasted对象作为其第一个参数。

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.*xBroadcasted(+, 5, Broadcasted(*, 2, x))瞬时表示。这对用户来说是不可见的,因为它通过对copy的调用立即实现,但正是这个容器为自定义类型作者的广播的可扩展性提供了基础。然后,内置的广播机制将根据参数确定结果类型和大小,分配它,然后最终使用默认的copyto!(::AbstractArray, ::Broadcasted)方法将Broadcasted对象的实现复制到其中。内置的回退broadcastbroadcast!方法也类似地构造操作的瞬态Broadcasted表示,以便它们可以遵循相同的代码路径。这允许自定义数组实现提供自己的copyto!专门化来自定义和优化广播。这再次由计算出的广播样式决定。这是操作中如此重要的一部分,以至于它被存储为Broadcasted类型的第一个类型参数,从而允许分派和专门化。

对于某些类型,跨嵌套广播级别“融合”操作的机制不可用或可以通过增量方式更高效地完成。在这种情况下,您可能需要或希望将x .* (x .+ 1)评估为如果它被写成broadcast(*, x, broadcast(+, x, 1)),其中内部操作在处理外部操作之前进行评估。这种急切的操作直接受一些间接操作的支持;而不是直接构造Broadcasted对象,Julia 将融合的表达式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)相同,默认情况下与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

重要的是,在getpropertysetproperty!内部使用getfieldsetfield,而不是点语法,因为点语法会使函数递归,这会导致类型推断问题。我们现在可以尝试新的功能

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

julia> p.x
4.949747468305833

julia> p.y = 4.0
4.0

julia> p.r
6.363961030678928

最后,值得注意的是,像这样添加实例属性在 Julia 中很少见,并且通常只有在有充分理由的情况下才应这样做。