资讯动态

Julia 方法与多重分派完全指南:从方法定义到无歧义设计

发布时间:2026/9/19 18:23:17 来源:尧图企业网站定制
Julia 方法与多重分派完全指南从方法定义到无歧义设计【免费下载链接】juliaThe Julia Programming Language项目地址: https://gitcode.com/gh_mirrors/ju/julia本指南围绕 Julia 语言的核心机制——方法Methods与多重分派Multiple Dispatch展开系统讲解如何通过::类型标注、参数化方法where、varargs 约束、函数式对象等手段为同一函数定义多种行为并结合 Julia 官方手册与仓库源码base/目录下的实际实现深入剖析方法特化、方法歧义及其消除策略。读完本文你将掌握 Julia 中最具代表性的编程范式能够写出类型精确、性能优越且不会陷入分派歧义的设计代码。什么是方法Method与多重分派在 Julia 中函数是一个把参数元组映射到返回值或抛出异常的对象。同一个概念性操作比如加法针对不同类型参数往往需要完全不同的实现两个整数相加、两个浮点数相加、整数与浮点数相加这三者实现迥异但都归属于同一个概念加法。因此 Julia 让这些行为同属于一个函数对象。函数不需要一次性定义完毕而是可以分片piecewise定义为特定参数类型组合与参数个数提供特定行为。函数的一种可能行为的定义被称为一个方法method。方法定义可以通过::类型断言运算符标注参数类型。当函数被应用到某个具体参数元组时最具体most specific的适用方法会被执行因此函数的整体行为是其各方法行为拼接而成的拼图设计良好的拼图会让函数的外部行为显得无缝而一致。选择执行哪个方法的过程称为分派dispatch。Julia 允许分派过程同时依据参数的个数与全部参数的类型来选择方法——这与传统面向对象语言仅依据第一个参数如 C/Java 中隐式传入的this接收者进行分派截然不同。依据函数的全部参数而非仅第一个参数来选择方法即多重分派Multiple Dispatch。这对数学代码尤其自然x y中的加法操作并不比属于y更多地属于x其实现取决于所有参数的类型。多重分派配合灵活的参数化类型系统使 Julia 能够以抽象方式表达与实现细节解耦的高层算法。注意本章示例均假设你为同一模块内的函数定义方法。若要为其他模块中的函数添加方法必须import该函数或使用带模块名的限定名参见手册 namespace management 一节对应手册 Namespace management。定义方法用::约束参数类型在没有::标注时方法适用于任意类型的参数行为与传统的动态类型语言无异。而 Julia 标准库中所有函数与运算符如都通过大量方法覆盖各种参数类型组合。使用::类型断言运算符可以限定方法适用的参数类型。例如julia f(x::Float64, y::Float64) 2x y f (generic function with 1 method) julia f(2.0, 3.0) 7.0这个定义仅适用于x与y都是Float64值的调用。将其应用于其他类型参数会得到MethodErrorjulia f(2.0, 3) ERROR: MethodError: no method matching f(::Float64, ::Int64) The function f exists, but no method is defined for this combination of argument types. Closest candidates are: f(::Float64, !Matched::Float64) Main none:1注意MethodError的提示非常贴心它会列出最接近的候选方法Closest candidates并用!Matched标记出具体不匹配的形参位置。从错误信息可以看到参数必须精确地是Float64整数或 32 位浮点数不会被自动转换为 64 位浮点字符串也不会被解析为数字。因为Float64是具体类型concrete type而 Julia 中具体类型不可被继承subclass所以这样的定义只能应用于类型恰好为Float64的参数。用抽象类型写出更通用的方法更常见的做法是声明抽象类型参数从而一次覆盖一大类类型julia f(x::Number, y::Number) 2x - y f (generic function with 2 methods) julia f(2.0, 3) 1.0该定义适用于任意一对Number实例——它们不必是同一类型只要各自是数值即可异种数值类型如何协同工作被委托给表达式2x - y中的算术运算去处理。定义多方法函数只需多次定义该函数参数个数/类型不同即可。第一个方法定义创建函数对象后续定义向已有函数对象追加新方法。执行时选择与参数个数和类型匹配的最具体方法。两个定义合起来覆盖了所有Number对但对(Float64, Float64)有专属行为julia f(2.0, 3.0) # 使用 2x y 7.0 julia f(2, 3.0) # 使用 2x - y 1.0 julia f(2.0, 3) # 使用 2x - y 1.0 julia f(2, 3) # 使用 2x - y 12x y仅在第一种情形下被使用。Julia 从不执行任何自动的类型转换或强制转换——所有转换都是非魔法的、完全显式的Conversion and Promotion展示了如何用足够先进的技术实现看似魔法般的自动转换。对于非数值参数或少于/多于两个参数函数f仍未定义调用会抛出MethodErrorjulia f(foo, 3) ERROR: MethodError: no method matching f(::String, ::Int64) Closest candidates are: f(!Matched::Number, ::Number) Main none:1 f(!Matched::Float64, !Matched::Float64) Main none:1查看已有方法methods与默认的Any类型在交互式会话中输入函数对象本身可以看到它拥有几个方法julia f f (generic function with 2 methods)使用methods函数可以查看各方法的签名julia methods(f) # 2 methods for generic function f from Main: [1] f(x::Float64, y::Float64) none:1 [2] f(x::Number, y::Number) none:1methods不仅展示签名还标注了方法定义所在的文件与行号本例中因在 REPL 定义显示为none:1。在仓库中methods的实现位于 base/reflection.jl它还可配合类型元组使用例如methods(f, (Union{Vector{Int},UnitRange{Int}},))只列出能匹配给定参数类型的候选方法。缺省情况下无::标注方法参数的类型为Any——因为 Julia 中所有值都是抽象类型Any的实例。因此可以定义兜底catch-all方法julia f(x,y) println(Whoa there, Nelly.) f (generic function with 3 methods) julia methods(f) # 3 methods for generic function f from Main: [1] f(x::Float64, y::Float64) [2] f(x::Number, y::Number) [3] f(x, y) julia f(foo, 1) Whoa there, Nelly.兜底方法比任何其他针对一对参数的方法都更不具体因此只有无其他方法适用时才会被调用。f(x, y)是无类型声明的简写等价于f(x::Any, y::Any)。多重分派看似简单却是 Julia 最强大、最核心的特性。核心操作通常拥有几十个方法例如在 Julia 1.x 中methods()会列出约 180 个方法覆盖从Bool、Float16/32/64、Complex{Bool}、Char Integer到BigInt/BigFloat在Base.GMP与Base.MPFR中实现再到接受任意多参数的(a, b, c, xs...)定义于 base/operators.jl的广泛组合。方法特化Method Specializations你主动为同一函数添加多个方法被称为对函数的特化——每个新方法都是函数的一个新特化这些特化可通过methods查看。但还存在另一种无需程序员干预的特化Julia 编译器会依据实际使用的具体参数类型自动为方法生成特化代码。这类特化不会出现在methods列表中因为并未创建新的Method对象需要用code_typed等工具来检视。例如定义方法mysum(x::Real, y::Real) x y该方法接受任意一对Real。若随后执行mysum(1, 2)与mysum(1.0, 2.0)Julia 会编译两次mysum一次针对x::Int, y::Int一次针对x::Float64, y::Float64。编译两次的意义在于性能mysum内部调用的方法随x、y的具体类型而变化通过预编译不同特化Julia 可以在运行前完成全部方法查找运行时无需再查询方法表从而大幅提速。这就是 Julia 自动特化的价值写出通用算法编译器负责为每种情况生成高效的特化代码。在某些特化数量可能几乎无限的场景下Julia 可能避免默认特化例如当参数类型可无限变化时详见手册 Be aware of when Julia avoids specializing 一节。方法歧义Method Ambiguities可能定义出一组方法使得某些参数组合下不存在唯一最具体的方法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.0) ERROR: MethodError: g(::Float64, ::Float64) is ambiguous. Candidates: g(x, y::Float64) Main none:1 g(x::Float64, y) Main none:1 Possible fix, define g(::Float64, ::Float64)调用g(2.0, 3.0)既可由g(::Float64, ::Any)处理也可由g(::Any, ::Float64)处理方法定义顺序无关紧要二者没有谁更具体。此时 Julia 抛出MethodError而非任意挑选一个方法。消除歧义的办法是为交集情形定义专门的方法julia g(x::Float64, y::Float64) 2x 2y julia g(2.0, 3.0) 10.0建议优先定义消除歧义的方法否则在该更具体方法定义之前歧义会哪怕只是暂时地存在。更复杂的歧义消解涉及设计层面的考量见下文方法设计与歧义规避。参数化方法Parametric Methods方法定义可以带限定签名的类型参数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)第一个方法适用于两个参数为同一具体类型的情况无论该类型是什么第二个作为兜底。合起来这就是一个判断两参数是否同类型的布尔函数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。方法类型参数不仅可作参数类型还能用在签名或函数体中的任何位置。例如把T用作Vector{T}的类型参数julia function myappend(v::Vector{T}, x::T) where {T} return [v..., x] end myappend (generic function with 1 method)where {T}在方法签名之后引入约束列表它同样适用于单行定义且若存在返回类型声明where必须位于返回类型声明之前julia (myappend(v::Vector{T}, x::T)::Vector) where {T} [v..., x] julia myappend([1,2,3],4) 4-element Vector{Int64}: 1 2 3 4 julia myappend([1,2,3],2.5) ERROR: MethodError: no method matching myappend(::Vector{Int64}, ::Float64)追加元素类型与向量元素类型不匹配时抛出MethodError。类型参数也可作为返回值julia mytypeof(x::T) where {T} T julia mytypeof(1) # Int64 julia mytypeof(1.0) # Float64如同类型声明中可为类型参数加子类型约束见手册 Parametric Types方法类型参数同样可约束julia same_type_numeric(x::T, y::T) where {T:Number} true julia same_type_numeric(x::Number, y::Number) false 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) # MethodErrorsame_type_numeric行为类似same_type但仅对数值对定义。参数化方法复用类型定义中where表达式的全部语法只有一个参数时where {T}的花括号可省略但常保留以示清晰多参数用逗号分隔如where {T, S:Real}也可嵌套书写如where S:Real where T。重定义方法与世界年龄World Age重定义方法或添加新方法时改动不会立即生效。这正是 Julia 能静态推断并编译出高速代码、而无需常规 JIT 技巧和开销的关键。任何新方法定义对当前运行时环境包括 Task、Threads 以及此前定义的generated函数都不可见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.) julia newfun() 1示例中newfun的新定义已创建但不能被立即调用。新全局变量对tryeval立即可见可以return newfun返回函数对象但你、你的调用者乃至调用者的调用链都无法调用这个新方法例外是从 REPL 发起的未来调用可以看到并调用新定义但对tryeval的未来调用仍会看到newfun在REPL 上一条语句即那次tryeval调用之前的定义。建议读者亲自运行验证。该行为的实现机制是世界年龄计数器world age counter详见手册 World Age 章节。参数化方法的设计模式复杂的分派逻辑并非性能或可用性的必需但有时是表达某些算法的最佳方式。以下是常见的几种设计模式。从超类型提取类型参数三角分派正确地从任意具有明确元素类型的AbstractArray子类型中提取元素类型T的模板abstract type AbstractArray{T, N} end eltype(::Type{:AbstractArray{T}}) where {T} T这被称为三角分派triangular dispatch。注意UnionAll类型如eltype(AbstractArray{T} where T : Integer)不匹配上述方法Base中eltype的实现为此类情形添加了针对Any的兜底方法——在仓库 base/abstractarray.jl 中可以看到eltype(::Type) Any、eltype(::Type{:AbstractArray{E}}) where {E} isdefined(E) ? E : Any等分层定义。常见的错误写法之一是试图通过内省introspection获取元素类型eltype_wrong(::Type{A}) where {A:AbstractArray} A.parameters[1]但这很容易构造出失败案例struct BitVector : AbstractArray{Bool, 1}; endBitVector没有任何参数但其元素类型完全确定为Bool另一个错误是沿类型层级用supertype向上爬eltype_wrong(::Type{AbstractArray{T}}) where {T} T eltype_wrong(::Type{AbstractArray{T, N}}) where {T, N} T eltype_wrong(::Type{A}) where {A:AbstractArray} eltype_wrong(supertype(A))这对显式声明的类型有效但对没有supertype的类型如Union类型会失败julia eltype_wrong(Union{Vector{Int}, Matrix{Int}}) ERROR: MethodError: no method matching supertype(::Type{VecOrMat{Int64}}) Closest candidates are: supertype(::UnionAll) Base operators.jl:44 supertype(::DataType) Base operators.jl:43supertype的这两个方法定义确实位于 base/operators.jlsupertype(T::DataType)与supertype(T::UnionAll)。用不同的类型参数构建相似类型编写通用代码时常需在改变类型布局从而改变类型参数的前提下构造相似对象。比如以特定元素类型对某个任意元素类型的抽象数组做计算。没有一种通用的变换能把一个子类型变成参数不同的另一个子类型因此需要为每个AbstractArray{T}子类型实现描述该类型变换的方法。AbstractArray的子类型通常实现两个方法一个把输入数组转换为特定AbstractArray{T, N}抽象类型的子类型一个以特定元素类型创建新的未初始化数组。二者在 Julia Base 中有示例实现基本用法如下可保证input与output类型一致input convert(AbstractArray{Eltype}, input) output similar(input, Eltype)在仓库 base/abstractarray.jl 中可以看到similar的完整方法族similar(a::AbstractArray{T})、similar(a::AbstractArray, ::Type{T})、similar(a::AbstractArray{T}, dims::Tuple)等重载最终汇聚到similar(a::AbstractArray, ::Type{T}, dims::Dims{N}) where {T,N} Array{T,N}(undef, dims)。若算法需要输入数组的副本convert不够返回值可能别名原输入。组合similar创建输出数组与copyto!填入输入数据是表达输入参数的可变副本的通用方式copy_with_eltype(input, Eltype) copyto!(similar(input, Eltype), input)迭代分派Iterated dispatch对多层级参数化参数列表进行分派时最好把每一层分派拆到不同函数中。这听上去像单分派但实际上更灵活。例如直接对数组元素类型分派常陷入歧义惯用做法是先按容器类型分派再基于eltype递归到更具体的方法。大多数算法的层次结构天然适合这种方式。以两矩阵逐元素求和为例其分派分支如下# 第一层分派为逐元素求和选择 map 算法 (a::Matrix, b::Matrix) map(, a, b) # 然后处理每个元素为计算选择公共元素类型 (a, b) (promote(a, b)...) # 元素类型一致后即可相加例如通过处理器暴露的原始操作 (a::Float64, b::Float64) Core.add(a, b)基于特质的分派Trait-based dispatch / Holy trait迭代分派的自然延伸是增加一层方法选择使其能针对独立于类型层级的类型集合分派。把相关类型写成Union固然可以但Union类型一旦创建不可再修改集合无法扩展。可扩展的集合可通过常被称为Holy trait的设计模式编程实现。该模式实现方式是定义一个通用函数为参数所属的每个特质集合计算不同的单例值或类型只要该函数是纯函数性能上与普通分派无异。上一节示例中map与promote的实现细节都依赖这类特质遍历矩阵时map要回答按什么顺序遍历数据这一问题。当AbstractArray子类型实现Base.IndexStyle特质后map等函数即可据此选择最优算法各子类型无需自实现map。这一点在仓库 base/abstractarray.jl 中有直接印证eachindex(A::AbstractArray)内部通过IndexStyle(A)分派如eachindex(IndexStyle(A), A)、eachindex(IndexStyle(A,B), A, B)getindex/setindex!也基于IndexStyle选择笛卡尔索引或线性索引路径。map的通用定义map(f, A::AbstractArray) collect_similar(A, Generator(f,A))同样位于该文件。特质分派的玩具级map实现map(f, a::AbstractArray, b::AbstractArray) map(Base.IndexStyle(a, b), f, a, b) # 通用实现 map(::Base.IndexCartesian, f, a::AbstractArray, b::AbstractArray) ... # 线性索引实现更快 map(::Base.IndexLinear, f, a::AbstractArray, b::AbstractArray) ...特质方法同样体现在标量所采用的promote机制中它使用promote_type返回给定两个操作数类型的最优公共计算类型base/promotion.jl 中定义了promote_type(T, S, U)的折叠式多参数版本及promote_type(::Type{T}, ::Type{S})的核心逻辑。这使为每对类型实现每个函数的大问题化简为为每个类型实现到公共类型的转换 一张首选成对提升规则表的小问题。输出类型计算Output-type computation实现加法等原始操作时用promote_type计算期望的输出类型。而对矩阵上更复杂的函数可能需要为一系列操作计算期望的返回类型通常分三步编写一个小函数op表达算法核心执行的操作集合以promote_op(op, argument_types...)计算结果矩阵的元素类型R其中argument_types由对各输入数组应用eltype得到以similar(R, dims)构建输出矩阵dims为输出数组的期望维度。一个通用的方阵乘法伪代码示例function matmul(a::AbstractMatrix, b::AbstractMatrix) op (ai, bi) - ai * bi ai * bi ## 以下写法均不正确 # R typeof(op(one(eltype(a)), one(eltype(b)))) # 假设 one(eltype(a)) 可构造 # R typeof(op(a[1], b[1])) # 假设 a[1] 存在且具代表性 # R promote_type(ai, bi) # 假设 调用 promote_typeBool 等类型不成立 # R Base.return_types(op, (eltype(a), eltype(b))) # 依赖类型推断的返回值脆弱且不可优化 ## 正确写法 R promote_op(op, eltype(a), eltype(b)) ## 可能给出比期望更大的类型但总能给出正确类型 output similar(b, R, (size(a, 1), size(b, 2))) if size(a, 2) 0 for j in 1:size(b, 2) for i in 1:size(a, 1) ## 不能用 ab zero(R)因为 R 可能是 Any 而 zero(Any) 未定义 ## 声明 ab::R 使 ab 在循环中类型恒定 ab::R a[i, 1] * b[1, j] for k in 2:size(a, 2) ab a[i, k] * b[k, j] end output[i, j] ab end end end return output end需要说明的是仓库 base/promotion.jl 中promote_op的文档明确警告它基于类型推断的猜测结果可能随时变化返回的只是上界官方建议尽量以实际元素的类型为依据决定容器元素类型仅在没有元素空结果容器时才调用promote_op。因此上例属于仅当无法从元素值获得类型时的合法用法。分离转换逻辑与核心逻辑将转换为目标类型与计算的逻辑隔离可显著降低编译时间与测试复杂度让编译器独立于大型核心函数体来特化并内联转换逻辑。这是把更大类别的类型转换到算法真正支持的那一个参数类型时常见的模式complexfunction(arg::Int) ... complexfunction(arg::Any) complexfunction(convert(Int, arg)) matmul(a::T, b::T) ... matmul(a, b) matmul(promote(a, b)...)参数约束的可变参数方法Parametrically-constrained Varargs函数参数还可用于约束varargs函数可接收的参数个数。记号Vararg{T,N}表示此类约束julia bar(a,b,x::Vararg{Any,2}) (a,b,x) julia bar(1,2,3) # MethodError只需 2 个可变参数这里只给了 1 个 julia bar(1,2,3,4) (1, 2, (3, 4)) julia bar(1,2,3,4,5) # MethodError可变参数给了 3 个超出 2更有用的场景是用参数同时约束可变参数的数量与类型例如function getindex(A::AbstractArray{T,N}, indices::Vararg{Number,N}) where {T,N}只有当indices的个数与数组维度一致时该方法才被调用——这正是AbstractArray索引接口中按维度校验索引个数的经典约束仓库 base/abstractarray.jl 中多维getindex的实现即采用类似思路。仅需约束类型时Vararg{T}等价于T...f(x::Int...) x是f(x::Vararg{Int}) x的简写。可选参数与关键字参数的本质可选参数本质上是多个方法定义的语法糖。例如f(a1,b2) a2b会翻译为以下三个方法f(a,b) a2b f(a) f(a,2) f() f(1,2)因此f()等价于f(1,2)结果为5。但这并非恒真若再定义更特化的整型方法f(a::Int,b::Int) a-2b则f()与f(1,2)的结果都变为-3。换言之可选参数绑定在函数上而非函数的某个具体方法上具体调用哪个方法取决于可选参数的类型。当可选参数由全局变量定义时其类型甚至可能在运行时改变。关键字参数的行为与普通位置参数截然不同它们不参与方法分派。方法仅依据位置参数分派关键字参数在匹配方法确定之后才被处理。已知缺陷关键字参数的存在影响分派JuliaLang/julia#9498 由于一个长期存在的 bug提供关键字参数的调用只会考虑接受关键字参数的方法这可能选中一个比不接受关键字但更具体的方法更不具体的版本julia f(x; y10) generic; julia f(x::Int) int-specific; julia f(1) # int-specific julia f(1; y2) # bug绕过了更具体的方法返回 generic相关地重定义不带关键字参数的同位置签名方法并不会替换此前定义的关键字处理逻辑尽管只显示一个方法julia g(x; y1) with keywords; julia g(x) without keywords; julia g g (generic function with 1 method) julia g(1) # without keywords julia g(1; y2) # bug调用的是被覆盖的定义返回 with keywords此警告仅为描述性说明而非行为约定代码不应依赖这些行为。若希望特化方法也能被带关键字参数的调用选中应为其提供自己的关键字接口——要么重复关键字参数要么用kwargs...收集任意关键字。函数式对象Function-like objects方法关联于类型因此可以通过为任意对象的类型添加方法使其可调用。例如定义存储多项式系数、同时表现得像求值函数一样的类型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 julia (p::Polynomial)() p(5) julia poly Polynomial([1,10,100]) Polynomial{Int64}([1, 10, 100]) julia poly(3) 931 julia poly() 2551注意这里函数由类型而非名称指定同样支持简洁语法形式函数体中p指代被调用的对象。这一机制也是 Julia 中类型构造器与闭包引用其周围环境的内层函数实现的关键。空泛型函数Empty generic functions有时需要先引入一个泛型函数而暂不添加方法以分离接口定义与实现或用于文档与代码可读性。语法为不带参数元组的空function块function emptyfunc end方法设计与歧义规避方法多态是 Julia 最强大的特性之一但滥用会带来设计挑战——在较复杂的方法层级中歧义并不罕见。前面提到过用f(x::Int, y::Int) 3消解f(x, y::Int) 1 f(x::Int, y) 2的歧义。这通常是正确策略但盲目套用可能适得其反泛型函数的方法越多歧义可能性越大。当方法层级复杂到超出上述简单示例时值得认真考虑替代策略。以下讨论特定挑战及一些替代解法。Tuple与NTuple参数Tuple及NTuple参数有特殊挑战。例如f(x::NTuple{N,Int}) where {N} 1 f(x::NTuple{N,Float64}) where {N} 2在N 0时是歧义的空元组没有元素可判定该调用Int还是Float64变体。消解方法之一是定义空元组的方法f(x::Tuple{}) 3或者除一个方法外都要求元组至少含一个元素f(x::NTuple{N,Int}) where {N} 1 # 这是兜底 f(x::Tuple{Float64, Vararg{Float64}}) 2 # 要求至少有一个 Float64正交化你的设计Orthogonalize当你忍不住要对两个或更多参数分派时考虑包装函数是否能让设计更简单。例如与其写四个变体f(x::A, y::A) ... f(x::A, y::B) ... f(x::B, y::A) ... f(x::B, y::B) ...不如定义f(x::A, y::A) ... f(x, y) f(g(x), g(y))其中g把参数转换为类型A。这是正交设计原则的具体例子分离的概念交给分离的方法。g通常需要兜底定义g(x::A) x相关策略是借助promote把x、y带到公共类型f(x::T, y::T) where {T} ... f(x, y) f(promote(x, y)...)此设计的一个风险是若没有合适的提升方法能把x和y转换为同类型第二个方法会对自身无限递归触发栈溢出。逐参数分派Dispatch on one argument at a time若必须对多个参数分派而回退组合太多、无法穷举所有变体可引入名称级联name cascade先对第一个参数分派再调用内部方法f(x::A, y) _fA(x, y) f(x::B, y) _fB(x, y)内部方法_fA、_fB可再对y分派而无需担心与彼此在x维度上的歧义。此策略的主要缺点是在很多情况下用户无法通过为你导出的函数f定义更多特化来定制行为而只能特化你的内部方法_fA/_fB这模糊了导出方法与内部方法的界限。抽象容器与元素类型尽量避免定义针对抽象容器特定元素类型分派的方法。例如-(A::AbstractArray{T}, b::Date) where {T:Date}会与任何用户定义的-(A::MyArrayType{T}, b::T) where {T}产生歧义。最佳做法是两者都不定义依赖泛型方法-(A::AbstractArray, b)并确保它用通用调用如similar与-实现使各容器类型与元素类型各自正确工作——这只是上述正交化建议的一个更复杂变体。当此路不通时值得与其他开发者讨论消解方案先定义的方法并不必然不可修改或删除。最后手段是某位开发者定义创可贴方法强行消歧-(A::MyArrayType{T}, b::Date) where {T:Date} ...带默认参数的复杂方法级联定义提供默认值的级联方法时小心不要丢掉对应潜在默认值的参数。例如数字滤波算法中处理信号边缘打 padding的方法function myfilter(A, kernel, ::Replicate) Apadded replicate_edges(A, size(kernel)) myfilter(Apadded, kernel) # 现在做真正的计算 end这会与提供默认 padding 的方法冲突myfilter(A, kernel) myfilter(A, kernel, Replicate()) # 默认复制边缘两者合在一起会导致A不断变大的无限递归。更好的设计是把调用层级组织如下struct NoPad end # 表示不需要 padding或已应用过 padding myfilter(A, kernel) myfilter(A, kernel, Replicate()) # 默认边界条件 function myfilter(A, kernel, ::Replicate) Apadded replicate_edges(A, size(kernel)) myfilter(Apadded, kernel, NoPad()) # 表示新的边界条件 end # 其他 padding 方法写在这里 function myfilter(A, kernel, ::NoPad) # 这里才是核心计算的真正实现 endNoPad与其他 padding 类型位于相同的参数位置使分派层级井井有条、歧义可能性更低同时它扩展了公开的myfilter接口——想显式控制 padding 的用户可以直接调用NoPad变体。在局部作用域定义方法可以在局部作用域内定义方法例如julia function f(x) g(y::Int) y x g(y) y - x g end f (generic function with 1 method) julia h f(3); julia h(4) # 7 julia h(4.0) # 1.0但不应在条件分支或控制流中定义局部方法function f2(inc) if inc g(x) x 1 else g(x) x - 1 end end function f3() function g end return g g() 0 end因为最终定义的函数并不明确未来这种写法可能直接成为错误。此类场景请改用匿名函数function f2(inc) g if inc x - x 1 else x - x - 1 end end小结Julia 方法系统的设计要点以全参数分派f(x::Float64, y::Float64)等::标注精确约束参数类型最具体方法胜出参数永不被自动转换。善用抽象类型与兜底方法Number等抽象类型配合Any兜底构建覆盖全部参数空间的行为拼图用methods(f)实现见 base/reflection.jl随时检视方法集。参数化方法与wherewhere {T}、where {T:Number}、多参数与嵌套where让方法在类型层面表达约束Vararg{T,N}约束可变参数的数量与类型。依赖编译器自动特化针对具体参数类型的自动特化是性能之源无需手工干预。主动消解歧义为交集情形定义更具体的方法或在设计中采用正交化、逐参数分派、promote归一、NoPad式哨兵类型等模式从源头避免歧义。留意世界年龄运行时新定义的方法对旧调用链不可见这是 Julia 静态编译保证速度的代价。关键字参数不参与分派且存在已知的 #9498 行为缺陷特化方法应自带关键字接口。多重分派与灵活的参数化类型系统是 Julia 表达力与性能兼具的根基——写出通用算法 精确分派的代码让编译器替你在每种情况下生成高效实现正是这门语言最核心的编程心智。【免费下载链接】juliaThe Julia Programming Language项目地址: https://gitcode.com/gh_mirrors/ju/julia创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价