一个名字服务多种类型,一段逻辑适配所有类型——函数的两种"复用术"。
给数字开平方和给复数开平方,该叫两个名字还是同一个名字?找两个 int 的最大值和找两个 string 的最大值,该写两份代码还是一份?前者是重载要回答的——同一个名字挂多个签名;后者是泛型要回答的——同一份逻辑抽象掉具体类型。本章把这两个机制连同它们的诊断信息一次讲透。
掌握重载声明与解析规则;理解重载与默认参数/可变参数的交互禁区;会写泛型函数并用 turbofish 显式实例化;知道类型推导在哪些位置有效;掌握 comparable/cloneable/json/xson 四个内置约束与 and 组合;读懂约束失败与推断冲突的诊断。
同一个名字可以定义多个函数,只要签名不同(参数数量或参数类型不同):
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 从多种来源解析、构造函数接受不同初始化方式。如果两个函数做的事在概念上不同,还是给不同的名字——重载是"一词多义",不该是"一词乱义"。
多个候选签名时,编译器的选择逻辑实测如下:
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 章):必须写出显式签名"点名":
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——歧义必须在赋值这一刻消掉,不留到运行期。
默认参数会让两个看似不同的签名在调用点"撞车",xlang 在声明时就拦截:
f(1) 到底进哪个?语言不允许这个问题存在——带默认参数的签名与更短的签名冲突,声明即报错。设计重载家族时自查一遍:把每个签名"实际可接受的调用形态"列出来,有交集就是歧义。可变参 ... 同理,它能吞掉任意数量的实参,与固定参数重载组合时尤其要画清边界。
重载是"每个类型写一份",泛型是"一份逻辑适配所有类型"。类型参数写在名字后面的尖括号里:
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 版是字符串比较,各自静态安全。这和重载写两份的本质区别是:逻辑只维护一份,类型安全一份不少。
turbofish 可以省略吗?实测结论:语句级调用可以推导,嵌套在别的调用里不行:
// ✅ 推导成功:实参给出 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)))
嵌套位置的报错提到 source T——类型参数还悬着没被实例化。推导规则"第一阶段"的边界就在这里:要么编译器能从当层语句推定 T,要么你写完整 turbofish。嵌套调用、长表达式链里,养成随手写 ::<T> 的习惯最稳妥(见坑 2)。
推导还可能撞上另一种失败——两个实参给 T 指了不同方向:
T 不可能既是 int 又是 string——报错把冲突双方直接点名。修法:统一实参类型,或显式实例化到公共类型。
裸 <T> 意味着"任何类型都行"——但 max 里写了 a > b,并非任何类型都能比大小。约束把合法实例集合收紧到"有相应能力"的类型,语法是在类型参数后加 is:
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 等基础类型 |
cloneable | clone() 深拷贝 | 基础类型与声明了复制能力的 class |
json | JSON 序列化 | 基础类型与 derive json 的 class |
xson | XSON 序列化 | 基础类型与 derive xson 的 class |
多约束用 and 连接,含义是"同时满足"。除四个能力名外,约束名还可以是已声明的 class 名——T is Entity 表示 T 必须是 Entity 或其子类(第 27 章)。写不存在的名字,编译期即拒:
约束在实例化时检查。类型不满足约束时的诊断:
报错指出了调用位置,但目前不点名具体缺哪个约束——多约束函数排查时,逐个对照约束清单即可。基础类型(int、string 等)对四个能力约束全部天然满足,上例 save::<int> 顺利通过就是证明。
泛型签名与具体签名可以共存于同一个名字下,各管一段:
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 节规则匹配。
xlang 提供三种多态:重载(编译期按签名挑实现,零运行时成本,每种类型一份代码);泛型(编译期实例化,逻辑一份、每类型一份代码,类型安全);var 参数(运行期动态分派,一份代码,失去静态检查)。选型阶梯很清晰:类型集合固定且概念同一→重载;逻辑真正与类型无关→泛型;只有万不得已(异构容器、脚本化 API)→var。前两条路线的错误都在编译期暴露,第三条的错误等你上线。
comparable/json/cloneable/xson 不是传统 OO 的接口继承——基础类型 int 并没有"实现"任何接口,却天然满足全部四个约束。xlang 的约束是结构化的能力检查:实例化时问"这个类型能不能比较/能不能序列化",而不是"这个类型声明过继承谁"。这让泛型既能套在 int 上、也能套在未来的 class 上(通过 derive 获得能力,第 27 章),一套机制打通内置类型与用户类型。constraint 只收紧合法实例集合,不改变运行时 is/as 语义——设计文档的这句话值得重读一遍。
max::<int> 与 max::<string> 在生成的 C 代码里是两个独立的函数,各自按具体类型检查与优化——这就是单态化(monomorphization),与 C++ 模板、Rust 泛型同宗。推论一:泛型没有运行时类型擦除开销,max::<int> 和手写 int 版一样快;推论二:实例化种类越多代码体积越大(每种 T 一份);推论三:泛型函数的错误信息会带"实例化调用位置",因为检查发生在实例化时而非定义时——14.6 节约束诊断把声明位置与调用位置都给出,正是这个模型的直接产物。
以下报错全部来自真实编译器输出(文件路径已缩短)。
原因:f(1) 两个签名都能接,声明时即被拦截。正确做法:让各签名"可接受的调用形态"互不重叠——合并成一个带默认参数的函数,或拉开参数数量/类型差距。
原因:推导当前只在语句级生效,嵌套进别的调用时 T 悬而未决——报错里的 source T 就是信号。正确做法:写 string(max::<int>(3, 7)),或先 auto m = max(3, 7) 再使用。
原因:内置能力约束只有 comparable、cloneable、json、xson 四个;其余名字必须是已声明的 class。hash 不存在——需要哈希语义时用具体类型或 class 约束表达。
原因:重载名字本身有歧义,装入 var 前必须消歧。正确做法:func(int) as int v = twice——用显式签名点名(14.2 节)。
| 知识点 | 写法/规则 | 要点 |
|---|---|---|
| 重载 | 同名不同签名 | 参数列表定身份;返回类型不参与 |
| 解析 | 精确匹配 → 安全拓宽 | 无匹配报"没有匹配的函数重载" |
| 默认参数交互 | 调用形态不得重叠 | 声明时报"重复或歧义的函数重载签名" |
| 泛型声明 | func max<T>(T a, T b) as T | 每实例独立检查、单态化 |
| turbofish | max::<int>(a, b) | 消表达式歧义;嵌套调用必写 |
| 推导 | 语句级有效 | 嵌套失效;冲突报双方类型 |
| 约束 | <T is comparable and json> | 四能力 + class 名;实例化时检查 |
:: 解决什么歧义?什么场景下必须写它?max::<int> 和手写的 int 版 max 一样快?< 在表达式里是小于号,:: 标明后面是泛型实参。嵌套调用(推导失效处)和需要与非泛型重载区分时必须写。编写 shows.xl:为 int、float、string、bool 各写一个 show 重载,分别输出带类型标签的一行(如 bool: true)。再用 int8、int64 变量调用,观察拓宽落到哪个签名。
参考思路:复用 14.2 节解析规则——没有精确签名时按安全拓宽找最近的家,用输出验证你的预判。
编写 gmin.xl:实现 min<T is comparable>(T a, T b) as T,分别用 int、float、string 实例化验证;再定义一个无比较能力的 class,尝试实例化,观察约束报错。
参考思路:照 max 改方向即可;第三步体会"约束把类型错误挡在实例化这一刻"的价值。
编写 gswap.xl:swap<T>(byref T a, byref T b) 交换两个同类型变量(回链第 12 章 byref)。用 int、string 各测一组,注意调用点标记与 turbofish 同现:swap::<int>(byref x, byref y)。
参考思路:参数模式是签名的一部分,泛型不改变 byref 纪律。这是泛型 + 模式标记的第一次合体。