多重派发是 Julia 的核心执行模型:函数不是一个代码块,而是一张"方法表";调用时按所有参数的具体类型联合挑选最特化的方法。它取代了其他语言的类继承与函数重载,是 Julia 既灵活又快的机制支点。
collide(a, b) = "两个普通物体相撞" collide(a::Integer, b::Integer) = "两个整数:按数值处理" collide(a::String, b::String) = "两个字符串:按词典处理" methods(collide) # 查看这个函数名下挂了几个方法
分别调用:
collide(1, 2) # 走整数方法 collide("a", "b") # 走字符串方法 collide(1, "b") # 没有精确匹配,落到兜底方法
关键点:选择依据是所有参数的类型,不只第一个。这就是"多重"的含义——面向对象语言的方法只看第一个参数(自己),Julia 看全部。

延续 2.1 的 Point,给加法挂一个方法,让 + 认识你的类型:
import Base: + struct Point{T} x::T y::T end +(a::Point, b::Point) = Point(a.x + b.x, a.y + b.y) Point(1, 2) + Point(3, 4) # Point(4, 6)
这就是 Julia 的"扩展不开分"哲学:+ 定义在 Base 里,但你可以为自己的类型追加方法,不用继承、不用包装。整个 Julia 生态——包括 println、size、getindex——都靠这套机制被第三方包扩展。
⚠️ 常见坑:两个方法对同一组参数都"最特化"时会产生歧义错误,Julia 会直接抛
MethodError系的歧义提示。解法是再写一个更特化的方法显式覆盖交叉情形。
| 面向对象 | Julia |
|---|---|
| 数据和方法绑在类里 | 数据(struct)与方法(函数)分离 |
| 单派发:看 this 的类型 | 多重派发:看全部参数 |
| 继承复用行为 | 组合 + 给新类型挂方法 |
| 接口靠约定 | 接口靠共同抽象类型 + 方法表 |
我的体会:二元操作(a + b、a 碰 b)在单派发语言里总要纠结放哪个类,多重派发直接消解了这个问题——这也是数值计算代码特别吃这套机制的原因,矩阵乘向量、向量乘矩阵天然对称。
空讲歧义没感觉,动手造一个。两个方法各自特化了一个参数,交叉情形就没人认领:
f(x::Number, y) = "y 任意" f(x, y::Integer) = "x 任意" # f(1, 2) 直接报 MethodError:两个方法都不更特化,产生歧义 f(x::Integer, y::Integer) = "两个都是整数" # 补上交叉方法,歧义消失 f(1, 2) # "两个都是整数" f(1, 2.5) # 走第一个 f("a", 2) # 走第二个
工程上的意义在于:接口设计者要把交叉情形想清楚并显式声明,而不是等用户撞上报错。写库的时候多花十分钟枚举参数组合,用户就少熬一个夜。
方法签名里写 T 能同时拿到"泛化"和"类型信息":
same_type(x::T, y::T) where {T} = true same_type(x, y) = false same_type(1, 2) # true,都是 Int64 same_type(1, 2.0) # false same_type(1, Int16(2)) # false,具体类型不同即不同
这比先判断 typeof(x) == typeof(y) 好:类型信息进了签名,派发阶段就完成判断,函数体里拿到的是确定的 T,编译器照样全速优化。生态里常见的签名是 norm(p::Point{T}) where T 这类写法,读懂它你就读懂了半个 Julia 标准库的接口风格。经验法则是:方法签名收得越窄,机器码越快、语义越明确;但收太窄会逼用户频繁转换类型。对外发布的接口留一个宽松兜底方法,内部实现层层特化,是兼顾两者的常见折中。
有一种担心很普遍:"方法表这么大,派发会不会慢?"静态派发(编译器能确定具体类型)零开销;动态派发(参数类型是抽象的,比如装在 Vector{Any} 里再取出来调用)要走一次方法表查找,代价大约几纳秒。判断自己的热点代码处于哪种情况,最直接的办法是看 @code_typed 的输出里有没有 invoke 字样。减少动态派发的套路和保类型稳定是一套:容器声明具体元素类型、函数别返回抽象类型、循环里提出的变量先落地再调用。换句话说,本章前两节讲的是同一件事的两面。
把本节知识放进一个能跑通的小项目:平面上三种图形两两求交,按"是否相交"返回布尔。背景是这类问题天然适合多重派发——圆与圆、圆与矩形、矩形与矩形各有专属算法,硬塞进一个类体系会得到一坨 if-else。先定义类型与统一入口:
abstract type Shape end struct Circle <: Shape c::Tuple{Float64,Float64}; r::Float64 end struct Rect <: Shape lo::Tuple{Float64,Float64}; hi::Tuple{Float64,Float64} end intersects(a::Shape, b::Shape) = error("组合未实现: $(typeof(a)) 与 $(typeof(b))")
然后逐对实现,每对一个方法,互不干扰:
# 圆与圆:比较圆心距与半径和 intersects(a::Circle, b::Circle) = sqrt(sum((a.c .- b.c) .^ 2)) <= a.r + b.r # 圆与矩形:把圆心夹到矩形内再算距离 intersects(a::Circle, b::Rect) = begin cx = clamp(a.c[1], b.lo[1], b.hi[1]) cy = clamp(a.c[2], b.lo[2], b.hi[2]) sqrt((a.c[1]-cx)^2 + (a.c[2]-cy)^2) <= a.r end intersects(a::Rect, b::Circle) = intersects(b, a) # 复用对称性 c1 = Circle((0.0,0.0), 1.0) r1 = Rect((0.5,0.5), (2.0,2.0)) intersects(c1, r1) # true:圆心到矩形的最近点距离约 0.707 < 1
解读这段设计的收益:新增一种图形(比如三角形)只需补三个方法,已有代码零改动;每个方法体只面对自己最熟悉的那对类型,测试也好写。变式练习:把返回值从布尔改成交区域面积,intersects 就变成一个真正的小几何库雏形——这就是多重派发支撑"生态可组合"的微观样本。
Base 函数挂方法忘了 import Base: xxx,报"不支持为另一个模块的函数定义方法";let 或函数体里又想扩展方法表,Julia 不允许,方法定义必须在顶层作用域;::Any 显式标注兜底方法看似无害,但会挡住真正的兜底路径,让 MethodError 变成更难查的逻辑错,兜底方法通常干脆不写类型;where T 里的 T 没用上(既不约束参数也不用于返回),属于误导性代码,审阅时应当删除。methods(f) 随时查看;import Base: xxx,这是扩展生态的标准姿势;