第 14 章 / 共 58 章 卷三 函数

重载与泛型函数

一个名字服务多种类型,一段逻辑适配所有类型——函数的两种"复用术"。

本章导读

给数字开平方和给复数开平方,该叫两个名字还是同一个名字?找两个 int 的最大值和找两个 string 的最大值,该写两份代码还是一份?前者是重载要回答的——同一个名字挂多个签名;后者是泛型要回答的——同一份逻辑抽象掉具体类型。本章把这两个机制连同它们的诊断信息一次讲透。

本章学习目标

掌握重载声明与解析规则;理解重载与默认参数/可变参数的交互禁区;会写泛型函数并用 turbofish 显式实例化;知道类型推导在哪些位置有效;掌握 comparable/cloneable/json/xson 四个内置约束与 and 组合;读懂约束失败与推断冲突的诊断。

14.1 函数重载

同一个名字可以定义多个函数,只要签名不同(参数数量或参数类型不同):

overload.xl
func show(int x)
    println("int: " + string(x))
end
func show(string s)
    println("string: " + s)
end
func show(int x, int y)
    println("pair: " + string(x) + "," + string(y))
end

show(42)       // int: 42
show("hi")     // string: hi
show(1, 2)      // pair: 1,2

重载是编译期的静态选择:编译器按实参的数量与类型挑选最匹配的签名,选完就是普通函数调用(第 11 章拾遗:静态分派)。返回类型不参与区分——只有参数列表构成重载身份(第 12 章拾遗 2:参数模式 byval/byref/out 也是签名的一部分)。

什么时候用重载?同一概念、多种输入形态:show 什么都可以、parse 从多种来源解析、构造函数接受不同初始化方式。如果两个函数做的事在概念上不同,还是给不同的名字——重载是"一词多义",不该是"一词乱义"。

14.2 重载解析规则

多个候选签名时,编译器的选择逻辑实测如下:

resolve.xl
func show(int x)
    println("int")
end
func show(float x)
    println("float")
end

show(1)            // int:整数字面量精确命中
show(1.5)          // float
int8 b = 3
show(b)            // int:int8 安全拓宽到 int(第 7 章)

规则提炼:先精确匹配,再按安全拓宽找最近的家。int8 没有专属签名时拓宽到 int(而不是 float)——拓宽方向遵守第 7 章的无损规则。完全对不上号时,报第 12 章坑 4 那条熟悉的 [sem] 没有匹配的函数重载。

把重载函数装进函数值时的歧义处理(回链第 13 章):必须写出显式签名"点名":

pick.xl
func twice(int x) as int
    return x * 2
end
func twice(string s) as string
    return s + s
end

func(int) as int p = twice        // 签名点名 int 版本
println(string(p(21)))            // 42
func(string) as string q = twice  // string 版本
println(q("ab"))                  // abab

直接 var v = twice 装重载函数会被拦下:[checked] bare overloaded function assigned to var requires an explicit func signature——歧义必须在赋值这一刻消掉,不留到运行期。

14.3 重载与默认参数/可变参

默认参数会让两个看似不同的签名在调用点"撞车",xlang 在声明时就拦截:

# bad.xl 内容: # func f(int x) # println("one") # end # func f(int x, int y = 10) # println("two") # end > xl run bad.xl bad.xl:4:1: [sem] 重复或歧义的函数重载签名

f(1) 到底进哪个?语言不允许这个问题存在——带默认参数的签名与更短的签名冲突,声明即报错。设计重载家族时自查一遍:把每个签名"实际可接受的调用形态"列出来,有交集就是歧义。可变参 ... 同理,它能吞掉任意数量的实参,与固定参数重载组合时尤其要画清边界。

14.4 泛型函数与 turbofish

重载是"每个类型写一份",泛型是"一份逻辑适配所有类型"。类型参数写在名字后面的尖括号里:

generic.xl
func max<T>(T a, T b) as T
    if a > b then
        return a
    end
    return b
end

int a = max::<int>(1, 2)
println(string(a))              // 2
string s = max::<string>("apple", "banana")
println(s)                        // banana:字符串按字典序比大小

调用点的 ::<int> 叫 turbofish(涡轮鱼,Rust 社区的昵称)——显式告诉编译器"T 这次是 int"。为什么要 ::?因为 max<int>(1, 2) 在表达式位置会被解析成"max 小于 int 再大于..."的比较链,:: 消掉这个歧义。

泛型函数的编译模型:每个具体实例化(int 版、string 版)都会独立做类型检查、独立生成代码——a > b 在 int 版是整数比较,在 string 版是字符串比较,各自静态安全。这和重载写两份的本质区别是:逻辑只维护一份,类型安全一份不少。

14.5 类型推导

turbofish 可以省略吗?实测结论:语句级调用可以推导,嵌套在别的调用里不行:

infer.xl
// ✅ 推导成功:实参给出 T = int
int m = max(3, 7)
println(string(m))

// ✅ auto 接收同样触发推导
auto m2 = max(3, 7)
println(type(m2))               // int

// ❌ 直接嵌进 string():T 悬而未决
// println(string(max(3, 7)))
# 嵌套调用的实测报错: infer.xl:7:19: [sem] 该表达式不能转换为目标类型: target string, source T

嵌套位置的报错提到 source T——类型参数还悬着没被实例化。推导规则"第一阶段"的边界就在这里:要么编译器能从当层语句推定 T,要么你写完整 turbofish。嵌套调用、长表达式链里,养成随手写 ::<T> 的习惯最稳妥(见坑 2)。

推导还可能撞上另一种失败——两个实参给 T 指了不同方向:

# bad.xl 内容:auto r = pick(1, "s") # func pick<T>(T a, T b) as T > xl run bad.xl bad.xl:4:18: [sem] 泛型函数类型推断冲突: target int, value string

T 不可能既是 int 又是 string——报错把冲突双方直接点名。修法:统一实参类型,或显式实例化到公共类型。

14.6 能力约束

裸 <T> 意味着"任何类型都行"——但 max 里写了 a > b,并非任何类型都能比大小。约束把合法实例集合收紧到"有相应能力"的类型,语法是在类型参数后加 is:

bounded.xl
func max<T is comparable>(T a, T b) as T
    if a > b then
        return a
    end
    return b
end

func save<T is comparable and json>(T obj)
    println("ok")
end
save::<int>(1)                 // ok:int 两个能力都有

内置能力约束就四个,背下来:

约束能力谁满足
comparable比较运算(>、<、== 等)数值、string、bool 等基础类型
cloneableclone() 深拷贝基础类型与声明了复制能力的 class
jsonJSON 序列化基础类型与 derive json 的 class
xsonXSON 序列化基础类型与 derive xson 的 class

多约束用 and 连接,含义是"同时满足"。除四个能力名外,约束名还可以是已声明的 class 名——T is Entity 表示 T 必须是 Entity 或其子类(第 27 章)。写不存在的名字,编译期即拒:

# bad.xl 内容:func f<T is hash>(T x) > xl run bad.xl bad.xl:1:13: [sem] unknown generic constraint

约束在实例化时检查。类型不满足约束时的诊断:

# bad.xl 内容:class Point ... end(无比较能力) # auto m = max(p1, p2) ← max 要求 comparable > xl run bad.xl bad.xl:13:13: [sem] generic type argument does not satisfy constraint

报错指出了调用位置,但目前不点名具体缺哪个约束——多约束函数排查时,逐个对照约束清单即可。基础类型(int、string 等)对四个能力约束全部天然满足,上例 save::<int> 顺利通过就是证明。

14.7 泛型与重载的组合

泛型签名与具体签名可以共存于同一个名字下,各管一段:

combo.xl
func max<T>(T a, T b) as T
    if a > b then
        return a
    end
    return b
end
func max(int a, int b, int c) as int
    return max::<int>(max::<int>(a, b), c)
end

println(string(max(1, 5, 3)))      // 5:三参数具体版
println(string(max::<int>(2, 4))) // 4:两参数泛型版

注意具体版内部复用了泛型版——具体重载做特化,泛型做兜底,特化内部回调泛型,这是标准库式的分层手法。调用解析时 turbofish 也起到"指路"作用:写了 ::<int> 就直奔泛型签名,普通调用按 14.2 节规则匹配。

深入细节

拾遗 1:重载、泛型、var——三条"一个名字多种类型"的路线对比

xlang 提供三种多态:重载(编译期按签名挑实现,零运行时成本,每种类型一份代码);泛型(编译期实例化,逻辑一份、每类型一份代码,类型安全);var 参数(运行期动态分派,一份代码,失去静态检查)。选型阶梯很清晰:类型集合固定且概念同一→重载;逻辑真正与类型无关→泛型;只有万不得已(异构容器、脚本化 API)→var。前两条路线的错误都在编译期暴露,第三条的错误等你上线。

拾遗 2:为什么约束叫"能力"而不是"接口"

comparable/json/cloneable/xson 不是传统 OO 的接口继承——基础类型 int 并没有"实现"任何接口,却天然满足全部四个约束。xlang 的约束是结构化的能力检查:实例化时问"这个类型能不能比较/能不能序列化",而不是"这个类型声明过继承谁"。这让泛型既能套在 int 上、也能套在未来的 class 上(通过 derive 获得能力,第 27 章),一套机制打通内置类型与用户类型。constraint 只收紧合法实例集合,不改变运行时 is/as 语义——设计文档的这句话值得重读一遍。

拾遗 3:泛型实例化的"单态化"本质

max::<int> 与 max::<string> 在生成的 C 代码里是两个独立的函数,各自按具体类型检查与优化——这就是单态化(monomorphization),与 C++ 模板、Rust 泛型同宗。推论一:泛型没有运行时类型擦除开销,max::<int> 和手写 int 版一样快;推论二:实例化种类越多代码体积越大(每种 T 一份);推论三:泛型函数的错误信息会带"实例化调用位置",因为检查发生在实例化时而非定义时——14.6 节约束诊断把声明位置与调用位置都给出,正是这个模型的直接产物。

⚠️ 常见错误

以下报错全部来自真实编译器输出(文件路径已缩短)。

坑 1:默认参数制造出歧义重载
# bad.xl 内容: # func f(int x) # end # func f(int x, int y = 10) # end > xl run bad.xl bad.xl:4:1: [sem] 重复或歧义的函数重载签名

原因:f(1) 两个签名都能接,声明时即被拦截。正确做法:让各签名"可接受的调用形态"互不重叠——合并成一个带默认参数的函数,或拉开参数数量/类型差距。

坑 2:嵌套调用里省掉 turbofish
# bad.xl 内容:println(string(max(3, 7))) > xl run bad.xl bad.xl:7:19: [sem] 该表达式不能转换为目标类型: target string, source T

原因:推导当前只在语句级生效,嵌套进别的调用时 T 悬而未决——报错里的 source T 就是信号。正确做法:写 string(max::<int>(3, 7)),或先 auto m = max(3, 7) 再使用。

坑 3:自造约束名
# bad.xl 内容:func f<T is hash>(T x) > xl run bad.xl bad.xl:1:13: [sem] unknown generic constraint

原因:内置能力约束只有 comparable、cloneable、json、xson 四个;其余名字必须是已声明的 class。hash 不存在——需要哈希语义时用具体类型或 class 约束表达。

坑 4:var 直接装重载函数
# bad.xl 内容: # func twice(int x) as int ... end # func twice(string s) as string ... end # var v = twice ← 哪个 twice? > xl run bad.xl bad.xl:9:9: [checked] bare overloaded function assigned to var requires an explicit func signature

原因:重载名字本身有歧义,装入 var 前必须消歧。正确做法:func(int) as int v = twice——用显式签名点名(14.2 节)。

本章小结

知识点写法/规则要点
重载同名不同签名参数列表定身份;返回类型不参与
解析精确匹配 → 安全拓宽无匹配报"没有匹配的函数重载"
默认参数交互调用形态不得重叠声明时报"重复或歧义的函数重载签名"
泛型声明func max<T>(T a, T b) as T每实例独立检查、单态化
turbofishmax::<int>(a, b)消表达式歧义;嵌套调用必写
推导语句级有效嵌套失效;冲突报双方类型
约束<T is comparable and json>四能力 + class 名;实例化时检查

复习题

✍️ 先遮住答案想一想
  1. 重载的身份由什么构成?为什么返回类型不参与?
  2. turbofish 的 :: 解决什么歧义?什么场景下必须写它?
  3. 约束检查发生在什么时候?这与报错信息的位置有什么关系?
  4. 重载、泛型、var 三条多态路线各自的成本与适用场景是什么?
  5. 为什么 max::<int> 和手写的 int 版 max 一样快?
参考答案
  1. 参数列表(数量、类型、模式标记)。返回类型不参与是因为调用点通常无法从上下文唯一确定期待的返回类型,允许只靠返回类型区分会使解析歧义丛生。
  2. < 在表达式里是小于号,:: 标明后面是泛型实参。嵌套调用(推导失效处)和需要与非泛型重载区分时必须写。
  3. 实例化时。约束在定义处只保存不检查,所以报错定位到调用位置而非定义位置。
  4. 重载:零成本、类型集固定;泛型:逻辑一份、类型安全、每实例一份代码;var:一份代码、运行期分派、失去静态检查。按"类型集固定→逻辑无关类型→万不得已"顺序选。
  5. 单态化:每个实例化生成独立的、按具体类型编译的函数,没有装箱或运行期查表,与手写特化等价。

编程练习

练习 1:show 重载家族

编写 shows.xl:为 int、float、string、bool 各写一个 show 重载,分别输出带类型标签的一行(如 bool: true)。再用 int8、int64 变量调用,观察拓宽落到哪个签名。

参考思路:复用 14.2 节解析规则——没有精确签名时按安全拓宽找最近的家,用输出验证你的预判。

练习 2:泛型 min 与约束

编写 gmin.xl:实现 min<T is comparable>(T a, T b) as T,分别用 int、float、string 实例化验证;再定义一个无比较能力的 class,尝试实例化,观察约束报错。

参考思路:照 max 改方向即可;第三步体会"约束把类型错误挡在实例化这一刻"的价值。

练习 3:swap 泛型

编写 gswap.xl:swap<T>(byref T a, byref T b) 交换两个同类型变量(回链第 12 章 byref)。用 int、string 各测一组,注意调用点标记与 turbofish 同现:swap::<int>(byref x, byref y)。

参考思路:参数模式是签名的一部分,泛型不改变 byref 纪律。这是泛型 + 模式标记的第一次合体。