Julia(面向对象)

2021-04-14 12:52:07 浏览数 (3)

方法

从Function回忆起,函数是一个将参数元组映射到返回值的对象,或者,如果无法返回适当的值,则抛出异常。对于不同类型的参数,相同的概念函数或操作的实现方式通常非常不同:添加两个整数与添加两个浮点数有很大不同,这两个区别都不同于将整数添加到浮点数。尽管它们的实现存在差异,但这些操作都属于“加法”的一般概念。因此,在Julia中,这些行为都属于一个对象: 函数。

为了方便顺利地使用同一概念的许多不同实现,功能不必一次全部定义,而可以通过为参数类型和计数的某些组合提供特定行为来分段定义。一种功能可能的行为的定义称为方法。到目前为止,我们仅介绍了用单个方法定义的函数示例,这些函数适用于所有类型的参数。但是,可以对方法定义的签名进行注释,以指示参数的类型以及它们的数量,并且可以提供多个方法定义。将函数应用于特定的参数元组时,将应用适用于那些参数的最特定的方法。因此,函数的整体行为是其各种方法定义的行为的拼凑而成。如果拼凑而成的设计得当,即使方法的实现可能完全不同,该函数的向外行为也将显得无缝且一致。

选择应用函数时执行哪种方法的方法称为dispatch。Julia允许调度过程根据给定参数的数量以及所有函数参数的类型来选择调用函数的方法。这与传统的面向对象的语言不同,传统的面向对象的语言仅基于第一个参数进行分配,而第一个参数通常具有特殊的参数语法,并且有时是隐式的,而不是显式地编写为参数。[1]使用函数的所有参数来选择应该调用哪个方法而不是仅调用第一个方法,称为多重调度。多重分派对于数学代码特别有用,在数学代码中,人为地认为操作“属于”一个参数比其他任何参数都没有多大意义:加法运算中的加法运算是否比它x y属于的x更多y?数学运算符的实现通常取决于其所有参数的类型。但是,即使超出数学运算范围,多次派遣最终仍是构造和组织程序的强大而便捷的范例。

[1]

例如,在C 或Java中,在类似的方法调用中obj.meth(arg1,arg2),对象obj“接收”该方法调用,并通过this关键字隐式传递给该方法,而不是作为显式方法参数传递给该方法。当当前this对象是方法调用的接收者时,可以完全省略,只写meth(arg1,arg2),而将其this隐含为接收对象。

定义方法

到目前为止,在示例中,我们仅使用具有不受约束的参数类型的单个方法定义了函数。这些函数的行为就像在传统的动态类型化语言中一样。但是,我们几乎一直不知不觉地使用了多种调度和方法:与上述 函数一样,Julia的所有标准函数和运算符都有许多方法可以根据参数类型和计数的各种可能组合来定义其行为。

定义一个函数时,可以选择使用组合类型::一节中介绍的类型断言运算符来约束它适用的参数类型:

代码语言:javascript复制
julia> f(x::Float64, y::Float64) = 2x   y
f (generic function with 1 method)

此函数定义仅适用于xy均为type值的调用Float64

代码语言:javascript复制
julia> f(2.0, 3.0)
7.0

将其应用于任何其他类型的参数将导致MethodError

代码语言:javascript复制
julia> f(2.0, 3)
ERROR: MethodError: no method matching f(::Float64, ::Int64)
Closest candidates are:
  f(::Float64, !Matched::Float64) at none:1

julia> f(Float32(2.0), 3.0)
ERROR: MethodError: no method matching f(::Float32, ::Float64)
Closest candidates are:
  f(!Matched::Float64, ::Float64) at none:1

julia> f(2.0, "3.0")
ERROR: MethodError: no method matching f(::Float64, ::String)
Closest candidates are:
  f(::Float64, !Matched::Float64) at none:1

julia> f("2.0", "3.0")
ERROR: MethodError: no method matching f(::String, ::String)

如您所见,参数必须精确地是type Float64。其他数字类型(例如整数或32位浮点值)不会自动转换为64位浮点,也不会将字符串解析为数字。因为Float64是具体类型,而具体类型不能在Julia中进行子类化,所以这样的定义只能应用于类型完全为type的参数Float64。但是,在声明的参数类型为抽象的情况下编写更通用的方法通常可能很有用:

代码语言:javascript复制
julia> f(x::Number, y::Number) = 2x - y
f (generic function with 2 methods)

julia> f(2.0, 3)
1.0

此方法定义适用于作为实例的任何一对参数Number。它们不必是同一类型,只要它们都是数字值即可。处理完全不同的数字类型的问题委托给表达式中的算术运算2x - y

要使用多种方法定义一个函数,只需简单地多次定义该函数,并使用不同数量和类型的参数即可。函数的第一个方法定义创建函数对象,随后的方法定义将新方法添加到现有函数对象。应用该函数时,将执行与参数的数量和类型匹配的最具体的方法定义。因此,以上两个方法定义共同定义f了抽象类型的所有实例对的Number行为–但具有特定于Float64值对的不同行为。如果参数之一是64位浮点数,而另一个则不是,则f(Float64,Float64)无法调用该f(Number,Number)方法,必须使用更通用的方法:

代码语言:javascript复制
julia> f(2.0, 3.0)
7.0

julia> f(2, 3.0)
1.0

julia> f(2.0, 3)
1.0

julia> f(2, 3)
1

2x y定义仅在第一种情况下使用,而2x - y其他情况下使用该定义。永远不会执行函数参数的自动转换或转换:Julia中的所有转换都是非魔术的,并且是完全明确的。但是,转换和推广表明,如何充分运用先进技术才能与魔术区分开。[克拉克61]

对于非数字值,以及少于或多于两个参数,该函数f保持未定义状态,应用该函数仍将导致MethodError

代码语言:javascript复制
julia> f("foo", 3)
ERROR: MethodError: no method matching f(::String, ::Int64)
Closest candidates are:
  f(!Matched::Number, ::Number) at none:1

julia> f()
ERROR: MethodError: no method matching f()
Closest candidates are:
  f(!Matched::Float64, !Matched::Float64) at none:1
  f(!Matched::Number, !Matched::Number) at none:1

通过在交互式会话中输入函数对象本身,可以轻松查看函数存在哪些方法:

代码语言:javascript复制
julia> f
f (generic function with 2 methods)

此输出告诉我们这f是一个具有两个方法的函数对象。要找出这些方法的签名是什么,请使用以下methods()函数:

代码语言:javascript复制
julia> methods(f)
# 2 methods for generic function "f":
f(x::Float64, y::Float64) in Main at none:1
f(x::Number, y::Number) in Main at none:1

其中显示f有两种方法,一种采用两个Float64参数,一种采用type类型的参数Number。它还指出了定义方法的文件和行号:由于这些方法是在REPL上定义的,因此我们得到了明显的行号none:1

在没有带有类型声明的情况下,默认情况下::方法参数的类型是Any默认的,这意味着它不受约束,因为Julia中的所有值都是抽象类型的实例Any。因此,我们可以这样定义一个包罗万象的方法f

代码语言:javascript复制
julia> f(x,y) = println("Whoa there, Nelly.")
f (generic function with 3 methods)

julia> f("foo", 1)
Whoa there, Nelly.

对于一对参数值,此通用方法没有其他任何可能的方法定义那么具体,因此将仅在没有其他方法定义适用的参数对上调用它。

尽管这似乎是一个简单的概念,但对值类型的多次分派可能是Julia语言最强大的核心功能。核心操作通常有数十种方法:

代码语言:javascript复制
julia> methods( )
# 180 methods for generic function " ":
 (x::Bool, z::Complex{Bool}) in Base at complex.jl:224
 (x::Bool, y::Bool) in Base at bool.jl:89
 (x::Bool) in Base at bool.jl:86
 (x::Bool, y::T) where T<:AbstractFloat in Base at bool.jl:96
 (x::Bool, z::Complex) in Base at complex.jl:231
 (a::Float16, b::Float16) in Base at float.jl:372
 (x::Float32, y::Float32) in Base at float.jl:374
 (x::Float64, y::Float64) in Base at float.jl:375
 (z::Complex{Bool}, x::Bool) in Base at complex.jl:225
 (z::Complex{Bool}, x::Real) in Base at complex.jl:239
 (x::Char, y::Integer) in Base at char.jl:40
 (c::BigInt, x::BigFloat) in Base.MPFR at mpfr.jl:303
 (a::BigInt, b::BigInt, c::BigInt, d::BigInt, e::BigInt) in Base.GMP at gmp.jl:303
 (a::BigInt, b::BigInt, c::BigInt, d::BigInt) in Base.GMP at gmp.jl:296
 (a::BigInt, b::BigInt, c::BigInt) in Base.GMP at gmp.jl:290
 (x::BigInt, y::BigInt) in Base.GMP at gmp.jl:258
 (x::BigInt, c::Union{UInt16, UInt32, UInt64, UInt8}) in Base.GMP at gmp.jl:315
...
 (a, b, c, xs...) at operators.jl:119

多次分派与灵活的参数类型系统一起使Julia具有抽象表达与实现细节分离的高级算法的能力,并且可以生成有效的专业代码来在运行时处理每种情况。

方法歧义

可以定义一组函数方法,这样就没有适用于某些参数组合的唯一最具体的方法:

代码语言:javascript复制
julia> g(x::Float64, y) = 2x   y
g (generic function with 1 method)

julia> g(x, y::Float64) = x   2y
g (generic function with 2 methods)

julia> g(2.0, 3)
7.0

julia> g(2, 3.0)
8.0

julia> g(2.0, 3.0)
ERROR: MethodError: g(::Float64, ::Float64) is ambiguous.
[...]

在这里,调用g(2.0, 3.0)可以由g(Float64, Any)g(Any, Float64)方法处理,两者都不比另一个更具体。在这种情况下,朱莉娅提出了一个MethodError而不是任意选择一种方法的方法。您可以通过为相交情况指定适当的方法来避免方法歧义:

代码语言:javascript复制
julia> g(x::Float64, y::Float64) = 2x   2y
g (generic function with 3 methods)

julia> g(2.0, 3)
7.0

julia> g(2, 3.0)
8.0

julia> g(2.0, 3.0)
10.0

建议首先定义消除歧义的方法,因为否则会暂时存在歧义(如果是暂时的话),直到定义了更具体的方法为止。

在更复杂的情况下,解决方法的歧义涉及设计的某些元素;该主题将在下面进一步探讨。

参数化方法

方法定义可以选择使类型参数限定签名:

代码语言:javascript复制
julia> same_type(x::T, y::T) where {T} = true
same_type (generic function with 1 method)

julia> same_type(x,y) = false
same_type (generic function with 2 methods)

只要两个参数具有相同的具体类型(无论是哪种类型),第一种方法都适用;而第二种方法则充当了包罗万象的角色,涵盖了所有其他情况。因此,总的来说,这定义了一个布尔函数,用于检查其两个参数是否具有相同的类型:

代码语言:javascript复制
julia> same_type(1, 2)
true

julia> same_type(1, 2.0)
false

julia> same_type(1.0, 2.0)
true

julia> same_type("foo", 2.0)
false

julia> same_type("foo", "bar")
true

julia> same_type(Int32(1), Int64(2))
false

此类定义对应于类型签名为UnionAll类型的方法(请参见UnionAll Types)。

通过分派对功能行为的这种定义在Julia中非常普遍,甚至是惯用的。方法类型参数不限于用作参数类型:它们可以在函数签名或函数主体中的任何值处使用。这是一个示例,其中方法类型参数T用作Vector{T}方法签名中参数类型的类型参数:

代码语言:javascript复制
julia> myappend(v::Vector{T}, x::T) where {T} = [v..., x]
myappend (generic function with 1 method)

julia> myappend([1,2,3],4)
4-element Array{Int64,1}:
 1
 2
 3
 4

julia> myappend([1,2,3],2.5)
ERROR: MethodError: no method matching myappend(::Array{Int64,1}, ::Float64)
Closest candidates are:
  myappend(::Array{T,1}, !Matched::T) where T at none:1

julia> myappend([1.0,2.0,3.0],4.0)
4-element Array{Float64,1}:
 1.0
 2.0
 3.0
 4.0

julia> myappend([1.0,2.0,3.0],4)
ERROR: MethodError: no method matching myappend(::Array{Float64,1}, ::Int64)
Closest candidates are:
  myappend(::Array{T,1}, !Matched::T) where T at none:1

如您所见,附加元素的类型必须与附加元素的向量的元素类型匹配,否则MethodError引发a。在以下示例中,方法类型参数T用作返回值:

代码语言:javascript复制
julia> mytypeof(x::T) where {T} = T
mytypeof (generic function with 1 method)

julia> mytypeof(1)
Int64

julia> mytypeof(1.0)
Float64

正如您可以在类型声明中将子类型约束放置在类型参数上一样(请参见Parametric Types),您也可以约束方法的类型参数:

代码语言:javascript复制
julia> same_type_numeric(x::T, y::T) where {T<:Number} = true
same_type_numeric (generic function with 1 method)

julia> same_type_numeric(x::Number, y::Number) = false
same_type_numeric (generic function with 2 methods)

julia> same_type_numeric(1, 2)
true

julia> same_type_numeric(1, 2.0)
false

julia> same_type_numeric(1.0, 2.0)
true

julia> same_type_numeric("foo", 2.0)
ERROR: MethodError: no method matching same_type_numeric(::String, ::Float64)
Closest candidates are:
  same_type_numeric(!Matched::T<:Number, ::T<:Number) where T<:Number at none:1
  same_type_numeric(!Matched::Number, ::Number) at none:1

julia> same_type_numeric("foo", "bar")
ERROR: MethodError: no method matching same_type_numeric(::String, ::String)

julia> same_type_numeric(Int32(1), Int64(2))
false

same_type_numeric函数的行为与same_type上面定义的函数非常相似,但是仅针对数字对定义。

参数方法允许使用与where用于写入类型的表达式相同的语法(请参见UnionAll Types)。如果只有一个参数,where {T}则可以省略括起来的花括号(中的),但为了清楚起见,通常首选使用花括号。多个参数可以用逗号分隔,例如where {T, S<:Real},或使用嵌套的书写where,例如where S<:Real where T

重新定义方法

重新定义方法或添加新方法时,重要的是要意识到这些更改不会立即生效。这是Julia能够静态推断和编译代码以快速运行的能力的关键,而无需通常的JIT技巧和开销。实际上,任何新的方法定义对于当前的运行时环境都是不可见的,包括任务和线程(以及任何先前定义的@generated函数)。让我们从一个示例开始,看看这意味着什么:

代码语言:javascript复制
julia> function tryeval()
           @eval newfun() = 1
           newfun()
       end
tryeval (generic function with 1 method)

julia> tryeval()
ERROR: MethodError: no method matching newfun()
The applicable method may be too new: running in world age xxxx1, while current world is xxxx2.
Closest candidates are:
  newfun() at none:1 (method too new to be called from this world context.)
 in tryeval() at none:1
 ...

julia> newfun()
1

在此示例中,请注意newfun已创建的新定义,但不能立即调用。新的全局tryeval变量立即对函数可见,因此您可以编写return newfun(不带括号)。但是您,您的任何调用者,他们调用的函数等都无法调用此新方法定义!

但是有一个例外:newfun REPL将来的调用会按预期工作,并且能够看到和调用的新定义newfun

然而,未来的呼叫tryeval将继续看到的定义newfun,因为它是在在REPL此前的说法,因此该调用之前tryeval

您可能需要自己尝试一下,以了解其工作原理。

此行为的实现是“世界年龄计数器”。这个单调增加的值会跟踪每个方法定义操作。这允许将“对给定运行时环境可见的方法定义集”描述为一个数字或“世界年龄”。它也允许仅通过比较它们的序数值来比较两个世界中可用的方法。在上面的示例中,我们看到“当前世界”(newfun()存在该方法)比tryeval启动执行时固定的任务本地“运行时世界”大一个。

有时有必要解决这个问题(例如,如果要实现上述REPL)。幸运的是,有一个简单的解决方案:使用调用函数Base.invokelatest

代码语言:javascript复制
julia> function tryeval2()
           @eval newfun2() = 2
           Base.invokelatest(newfun2)
       end
tryeval2 (generic function with 1 method)

julia> tryeval2()
2

最后,让我们看一些更复杂的示例,其中该规则起作用。定义一个函数f(x),该函数最初具有一种方法:

代码语言:javascript复制
julia> f(x) = "original definition"
f (generic function with 1 method)

开始一些其他使用的操作f(x)

代码语言:javascript复制
julia> g(x) = f(x)
g (generic function with 1 method)

julia> t = @async f(wait()); yield();

现在,我们向中添加一些新方法f(x)

代码语言:javascript复制
julia> f(x::Int) = "definition for Int"
f (generic function with 2 methods)

julia> f(x::Type{Int}) = "definition for Type{Int}"
f (generic function with 3 methods)

比较这些结果有何不同:

代码语言:javascript复制
julia> f(1)
"definition for Int"

julia> g(1)
"definition for Int"

julia> wait(schedule(t, 1))
"original definition"

julia> t = @async f(wait()); yield();

julia> wait(schedule(t, 1))
"definition for Int"

参数约束的Varargs方法

函数参数还可以用于约束可能提供给“ varargs”函数(Varargs函数)的参数数量。该符号Vararg{T,N}用于指示这种约束。例如:

代码语言:javascript复制
julia> bar(a,b,x::Vararg{Any,2}) = (a,b,x)
bar (generic function with 1 method)

julia> bar(1,2,3)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64)
Closest candidates are:
  bar(::Any, ::Any, ::Any, !Matched::Any) at none:1

julia> bar(1,2,3,4)
(1, 2, (3, 4))

julia> bar(1,2,3,4,5)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64, ::Int64, ::Int64)
Closest candidates are:
  bar(::Any, ::Any, ::Any, ::Any) at none:1

更有用的是,可以通过参数约束varargs方法。例如:

代码语言:javascript复制
function getindex(A::AbstractArray{T,N}, indexes::Vararg{Number,N}) where {T,N}

仅当indexes匹配的数目与数组的维数匹配时才调用。

关于可选参数和关键字参数的注释

如Function中简要提到的那样,可选参数作为多种方法定义的语法实现。例如,此定义:

代码语言:javascript复制
f(a=1,b=2) = a 2b

转换为以下三种方法:

代码语言:javascript复制
f(a,b) = a 2b
f(a) = f(a,2)
f() = f(1,2)

这意味着调用f()等效于call f(1,2)。在这种情况下,结果为5,因为f(1,2)调用了f上面的第一个方法。但是,并非总是如此。如果您定义第四个更专门用于整数的方法:

代码语言:javascript复制
f(a::Int,b::Int) = a-2b

然后两者的结果f()f(1,2)-3。换句话说,可选参数绑定到一个函数,而不是该函数的任何特定方法。它取决于调用哪个方法的可选参数的类型。当根据全局变量定义可选参数时,可选参数的类型甚至可能在运行时更改。

关键字参数的行为与普通的位置参数完全不同。特别是,它们不参与方法分派。仅基于位置参数来分派方法,并在识别出匹配方法后处理关键字参数。

类功能对象

方法与类型相关联,因此可以通过向其类型添加方法来使任意Julia对象成为“可调用的”。(这种“可调用的”对象有时也称为“ functors”。)

例如,您可以定义一个存储多项式系数的类型,但其行为类似于评估多项式的函数:

代码语言:javascript复制
julia> struct Polynomial{R}
           coeffs::Vector{R}
       end

julia> function (p::Polynomial)(x)
           v = p.coeffs[end]
           for i = (length(p.coeffs)-1):-1:1
               v = v*x   p.coeffs[i]
           end
           return v
       end

请注意,该函数是通过类型而不是名称来指定的。在函数体中,p将引用被调用的对象。A Polynomial可以如下使用:

代码语言:javascript复制
julia> p = Polynomial([1,10,100])
Polynomial{Int64}([1, 10, 100])

julia> p(3)
931

该机制也是Julia中类型构造函数和闭包(引用其周围环境的内部函数)如何工作的关键,这将在本手册的后面部分进行讨论。

空泛型函数

有时引入通用函数而不添加方法会很有用。这可用于将接口定义与实现分开。也可能出于文档编制或代码可读性的目的而执行此操作。语法是一个function没有参数元组的空块:

代码语言:javascript复制
function emptyfunc
end

方法设计和避免歧义

Julia的方法多态性是其最强大的功能之一,但是利用这种功能可能会带来设计挑战。特别是,在更复杂的方法层次结构中,出现歧义并不罕见。

上面指出,一个人可以解决诸如

代码语言:javascript复制
f(x, y::Int) = 1
f(x::Int, y) = 2

通过定义一个方法

代码语言:javascript复制
f(x::Int, y::Int) = 3

这通常是正确的策略;但是,在某些情况下,盲目地遵循此建议可能适得其反。特别是,泛型函数拥有的方法越多,歧义的可能性就越大。当您的方法层次结构比此简单示例复杂时,值得考虑一下替代策略。

下面我们讨论特定的挑战以及解决这些问题的一些替代方法。

元组和NTuple参数

Tuple(和NTuple)论点提出了特殊的挑战。例如,

代码语言:javascript复制
f(x::NTuple{N,Int}) where {N} = 1
f(x::NTuple{N,Float64}) where {N} = 2

之所以含糊不清,是因为以下可能性N == 0:没有元素可以确定是否应调用Intor或Float64variant。为了解决歧义,一种方法是为空元组定义一个方法:

代码语言:javascript复制
f(x::Tuple{}) = 3

或者,对于除方法之外的所有方法,您可以坚持认为元组中至少有一个元素:

代码语言:javascript复制
f(x::NTuple{N,Int}) where {N} = 1           # this is the fallback
f(x::Tuple{Float64, Vararg{Float64}}) = 2   # this requires at least one Float64

正交化您的设计

当您可能想分派两个或多个参数时,请考虑是否可以使用“包装器”函数来简化设计。例如,不要编写多个变体:

代码语言:javascript复制
f(x::A, y::A) = ...
f(x::A, y::B) = ...
f(x::B, y::A) = ...
f(x::B, y::B) = ...

您可以考虑定义

代码语言:javascript复制
f(x::A, y::A) = ...
f(x, y) = f(g(x), g(y))

其中g将参数转换为type A。这是正交设计更一般原理的一个非常具体的示例,其中将单独的概念分配给单独的方法。在这里,g很可能需要后备定义

代码语言:javascript复制
g(x::A) = x

一个相关的策略,利用promotexy通用类型:

代码语言:javascript复制
f(x::T, y::T) where {T} = ...
f(x, y) = f(promote(x, y)...)

这种设计的一个风险是,如果没有合适的促进方法变换的可能性xy同一类型,所述第二方法将无限递归自身和触发堆栈溢出。Base.promote_noncircular可以使用非导出功能作为替代。当升级失败时,它仍然会引发错误,但是失败更快,并带有更具体的错误消息。

一次发送一个论点

如果您需要分派多个参数,并且有很多回退和太多的组合,以至于无法定义所有可能的变体,那么可以考虑引入一个“名称级联”,例如,您在第一个参数上分派然后调用内部方法:

代码语言:javascript复制
f(x::A, y) = _fA(x, y)
f(x::B, y) = _fB(x, y)

那么内部的方法_fA_fB可调度上y没有关于歧义相互方面表示关注x

请注意,此策略至少有一个主要缺点:在许多情况下,用户无法f通过定义导出函数的进一步专门化来进一步自定义行为f。取而代之的是,他们必须为内部方法_fA和定义特殊化_fB,这模糊了导出方法和内部方法之间的界限。

抽象容器和元素类型

在可能的情况下,尽量避免定义在抽象容器的特定元素类型上分派的方法。例如,

代码语言:javascript复制
-(A::AbstractArray{T}, b::Date) where {T<:Date}

为定义方法的任何人产生歧义

代码语言:javascript复制
-(A::MyArrayType{T}, b::T) where {T}

最好的办法是避免定义两种这些方法:相反,依赖于一个通用的方法-(A::AbstractArray, b),并确保这种方法与普通电话(等实现similar-为每个容器类型和元素类型做正确的事)分开。这只是使您的方法正交的建议的更复杂的变体。

如果无法采用这种方法,可能值得与其他开发人员讨论解决歧义的方法。仅仅因为首先定义了一种方法,并不一定意味着它不能被修改或消除。作为最后的选择,一个开发人员可以定义“创可贴”方法

代码语言:javascript复制
-(A::MyArrayType{T}, b::Date) where {T<:Date} = ...

通过蛮力解决了歧义。

具有默认参数的复杂方法“级联”

如果要定义提供默认值的“层叠”方法,请小心删除与潜在默认值相对应的所有参数。例如,假设您正在编写一种数字滤波算法,并且有一种方法可以通过应用填充来处理信号的边缘:

代码语言:javascript复制
function myfilter(A, kernel, ::Replicate)
    Apadded = replicate_edges(A, size(kernel))
    myfilter(Apadded, kernel)  # now perform the "real" computation
end

这将违反提供默认填充的方法:

代码语言:javascript复制
myfilter(A, kernel) = myfilter(A, kernel, Replicate()) # replicate the edge by default

这两种方法一起产生无限递归,并且递归A不断增大。

更好的设计是这样定义您的呼叫层次结构:

代码语言:javascript复制
struct NoPad end  # indicate that no padding is desired, or that it's already applied

myfilter(A, kernel) = myfilter(A, kernel, Replicate())  # default boundary conditions

function myfilter(A, kernel, ::Replicate)
    Apadded = replicate_edges(A, size(kernel))
    myfilter(Apadded, kernel, NoPad())  # indicate the new boundary conditions
end

# other padding methods go here

function myfilter(A, kernel, ::NoPad)
    # Here's the "real" implementation of the core computation
end

NoPad与其他填充类型在相同的论点位置被提供,因此它使调度层次结构井井有条,并且减少了歧义的可能性。而且,它扩展了“公共” myfilter界面:想要显式控制填充的用户可以NoPad直接调用变体。

[克拉克61]

亚瑟·克拉克(Arthur C. Clarke),《未来概况》(1961):克拉克的第三定律。

0 人点赞