一个函数的"接口素养",全写在参数表里。
第 11 章的函数参数都是"老实人":几个参数、什么类型,一一对应。真实世界的函数接口要灵活得多——"这个参数大多数时候是 1,偶尔要改";"二十个配置项,调用时只想写其中三个";"参数个数事先不知道";"我要函数把我的变量直接改掉"。本章把 xlang 参数表的全部武器摆出来:默认参数、命名参数、两种可变参数、三种显式传递模式,以及每种武器背后的语义。
掌握默认参数的声明与省略规则;会用命名参数乱序调用并理解与位置参数混用的边界;掌握 ... 与 *** 两种可变参数及 arg 访问;理解标量按值、容器共享引用、静态→var 转换隔离三种默认传递语义;会用 byval/byref/out 并遵守调用点标记纪律;了解 c_vararg 对接 C 可变参。
形参写上默认值,调用时就可以省略它:
func greet(string name, string greeting = "你好") println(greeting + ", " + name) end greet("小明") // 你好, 小明 greet("小红", "早上好") // 早上好, 小红
一条硬规则:可选参数(带默认值)必须排在必选参数之后,否则编译期报错:
这条规则的存在理由很直白:如果可选参数后面还能跟必选参数,省略中间参数时编译器无法对齐位置。默认值可以是任意编译期能确定的表达式;调用时带默认值参数按位置省略,想"跳过一个可选参数只改后面那个",用 12.2 节的命名参数。
调用时写出形参名 名字 = 值,参数顺序就自由了:
func move(int x, int y) println(string(x) + "," + string(y)) end move(y = 2, x = 1) // 1,2:乱序命名 move(1, y = 2) // 1,2:位置 + 命名混用
混用三条边界(全部实测):
move(x = 1, 2) 报 [sem] 位置参数不能出现在命名参数之后;move(1, x = 2) 报 [sem] 重复传入同一个命名参数;move(1, z = 2) 报 [sem] 未知命名参数(除非函数声明了 ***,见 12.4 节)。命名参数最大的价值不是乱序,而是自文档化:connect(host, 8080, true, false) 天知道后两个 bool 是什么;connect(host, 8080, tls = true, retry = false) 一目了然。布尔和字面量密集的长参数表,强烈建议命名调用。
参数个数事先不定,用 ... 收尾;函数体内通过内建 arg 数组访问全部多余实参:
func sum(...) as int int total = 0 for v in arg total += v as int // arg 的元素是 var,取出要 as end return total end println(string(sum(1, 2, 3))) // 6 println(string(sum())) // 0:不传也行 println(string(sum(10, 20, 30, 40))) // 100
三个要点:
... 前可以有固定形参:func f(int first, ...) 中 first 吃掉第一个实参,arg.length 只数可变部分(f(1, 2, 3) 时 first=1、arg.length=2);... 必须在参数表最后,与 *** 二选一:func both(..., ***) 报 [sem] 可变参数必须位于参数表最后。典型用途:printf 风格的聚合函数、参数个数不定的便捷入口(max(...)、log(...))。
... 收"多余的位置参数",*** 收"多余的命名参数",装进 arg 字典:
func open(string path, ***) as var println(path) return arg["mode"] end println(string(open("f.txt", mode = "r", flags = 0)))
匹配规则(功能清单口径,已实测):命名实参优先匹配已声明的形参,剩下的全部进入 arg 字典。这就是 12.2 节"未知命名参数"报错的豁免通道——函数声明了 ***,任何额外的 name = value 都合法且可查。arg 的键是 string、值是 var,遍历用第 9 章的 for k, v in arg。
典型用途:配置袋(options bag)——函数有十几个可选配置项时,与其声明十几个默认参数,不如一个 *** 收进来按需读取,接口永远不必加参数。
函数内修改参数,调用方看不看得到?答案分三类,全部实测:
| 参数类型 | 默认语义 | 函数内修改对调用方 |
|---|---|---|
| bool / 整数 / 浮点 / time / type / union | 按值复制 | 不可见(改的是副本) |
array<T> 等静态泛型容器 | 共享受管理句柄 | 可见(同一 backing) |
var 动态容器([...]、{...}) | 共享引用 | 可见(同一 backing) |
array<T> 传 var 形参 | 转换复制 | 不可见(静态→动态转换产生隔离副本) |
| class / object(第 27 章预告) | 共享受管理引用 | 成员修改可见;重绑定局部 |
// 1. 标量:按值 func bump(int n) n += 100 end int x = 1 bump(x) println(string(x)) // 1:调用方纹丝不动 // 2. 静态数组:共享 func pushStatic(array<int> a) a.push(99) end array<int> sa = array<int>[1, 2, 3] pushStatic(sa) println(string(sa.length)) // 4:调用方看得见! // 3. 动态 var 数组:同样共享,修改可见 func pushDyn(var a) a.push(99) end var da = [1, 2, 3] pushDyn(da) println(string(da.length)) // 4:var 传 var 共享,调用方看得见 // 4. 静态 array 传 var 形参:转换隔离(唯一"不可见") auto sa2 = [1, 2, 3] pushDyn(sa2) println(string(sa2.length)) // 3:静态→动态转换产生隔离副本
本节的结论经 2026-07-18 复测更正:var 动态容器与静态泛型容器一样,都是共享引用——赋值、传参不复制,函数内修改对调用方可见;var b = a 之后改 b,a 同样变。唯一的"不可见"出现在静态 → var 形参的跨形态传递:编译器插入一次转换,函数内拿到的是转换副本,与调用方隔离。早先版本此处误记为"写时复制(COW)"——规范文档确有 COW 口径,但 RC0 在简单场景从未触发分离,一切以实测为准。反过来,var 容器传给静态 array<T> 形参不被允许,需 as array<int> 显式转换。
默认语义之外,三个显式模式标记精确控制传递行为:
func bump(byref int n) n += 100 // 直接改写调用方变量 end func fill(out int n) n = 42 // out:必须赋值后才能返回 end int x = 1 bump(byref x) // 调用点必须写同名标记 println(string(x)) // 101 int y = 0 fill(out y) println(string(y)) // 42
三个模式各自的语义(逐条实测):
rebind(byref arr) 后调用方的 arr 已是新数组;[sem] out 参数在整体赋值前不能读取),且每条返回路径都必须完成赋值([sem] out 参数必须在函数正常结束前整体赋值)。最重要的纪律:byref 和 out 在调用点必须写出同名标记——bump(x) 而不是 bump(byref x) 会报 [sem] 没有匹配的函数重载(见坑 4)。标记让"这个调用会改我的变量"在代码阅读时无处遁形,这是 xlang 显式哲学的又一次体现。byref/out 还有一串禁区:不能带默认值、不能进可变参数、不能被匿名函数捕获、不能穿越 async/线程边界。
xlang 的 ... 是语言级参数包(走 xvalue 数组),不能直接对接 C 的原生可变参。调用 printf 这类 C 函数要用专用标记 c_vararg:
extern "printf" func printf(string fmt, c_vararg) as c_int printf("value=%d text=%s pi=%.2f\n", c_int(42), "hello", c_double(3.14159))
三条规则:c_vararg 只能出现在顶层 extern "符号" func 声明里(普通函数用它报 [sem] c_vararg 只能用于 extern C 函数声明);必须是最后一个参数;调用方必须用 c_int()、c_double() 等 ABI 类型显式包裹——C vararg 没有类型信息,宽度全靠调用方保证,%d 配 c_int、%f 配 c_double,配错就是未定义行为。本节只需认识写法,extern/export 的完整 C 混编体系是第 38 章的专题。
规范文档对 var 容器描述过写时复制(COW)模型:共享 backing + 引用计数,首次写入分离。但 RC0 实测矩阵显示该行为在简单场景一律未触发:var 传 var、var 赋值 var、函数内局部 var 接收全局容器,修改全部对原持有者可见。真实发生的隔离只有一处——静态 → 动态的转换隔离:array<int> 传给 var 形参时编译器生成一次容器转换,转换结果是新 backing,函数内的 push 与调用方无关。早先版本正是误把这个转换隔离当成了 COW 证据。实务建议:需要独立副本时在函数入口手工复制(var local = [] ; for v in src { local.push(v) }),不要指望自动分离;若后续版本 COW 落地,本章会按新实测再更正。
byval/byref/out 不只是"调用约定小贴士"——功能清单明确:参数模式属于函数签名、重载身份、XAPI 和 ABI 元数据的一部分。两个现实推论:其一,f(int) 与 f(byref int) 是两个不同签名(重载视角),这就是 byref 调用忘写标记时报"没有匹配的函数重载"而非"缺少标记"的原因;其二,C 混编时(第 38 章)byref/out 会映射为指针参数出现在生成的头文件里,C 侧看到的接口与 xlang 侧严格对应。模式标记是接口契约,不是实现细节。
把带 ... 的函数导出给 C 时,两种可变参不会变成 C 的 vararg——C 侧收到的是一个 xvalue arg:... 对应 array<var>,*** 对应 dict<var>。也就是说 xlang 可变参是"打包成容器传递",与 c_vararg 的"原生 C ..."是两套完全不同的机制:前者安全、带类型信息(var 包裹),后者零包装、类型靠格式串约定。选型一目了然:xlang 内部用 .../***,跨 C 边界的原生互操作用 c_vararg。
以下报错全部来自真实编译器输出(文件路径已缩短)。
原因:可选参数必须整体殿后。把 b 提前,或给 b 也加默认值。
原因:命名参数一旦出场,后面只能继续命名——move(x = 1, y = 2)。这条规则保证参数对齐永远无歧义。
原因:可变参 arg 的元素是 var,进 int 累加器前要 v as int。可变参函数的内部纪律就是第 8 章的 is/as 模式:先判断、再取出。
原因:参数模式是签名的一部分,bump(x) 匹配的是"无 byref 签名",找不到就报重载不匹配——报错不提 byref 半个字,极具迷惑性。正确做法:bump(byref x)。看到"没有匹配的函数重载"先检查三件事:参数数量、参数类型、模式标记。
| 机制 | 写法 | 要点 |
|---|---|---|
| 默认参数 | int level = 1 | 可选必须殿后;跳过用命名调用 |
| 命名参数 | f(y = 2, x = 1) | 位置在前命名在后;不重复、不虚构 |
| 数组可变参 | func f(...) + arg | arg 元素是 var;必须殿后;与 *** 互斥 |
| 字典可变参 | func f(***) + arg | 收未匹配命名参数;配置袋模式 |
| 默认传递 | 标量按值 / 容器共享引用 / 静态→var 转换隔离 | 三类语义背熟 |
| byval | byval T x | 标量复制;容器仍共享,非对象复制 |
| byref / out | 声明与调用点双标记 | byref 可重绑调用方;out 必赋值、不可先读 |
| c_vararg | extern 专用,末位 | c_int/c_double 包裹保证宽度 |
... 与 c_vararg 都是"可变参",为什么不能互相替代?编写 logger.xl:函数 log(string text, int level = 1, string tag = "app") 按 [level][tag] text 格式打印。分别用全省略、省一个、命名乱序、命名跳过的方式调用四次,验证四种形态输出。
参考思路:命名调用 log("msg", tag = "net") 跳过 level——这就是"默认参数 + 命名参数"的黄金搭档。
编写 maxof.xl:max(...) as int 返回所有实参中的最大值,零参数时 throw ValueError "max needs at least one argument"(回链第 10 章)。用 1 个、3 个、7 个参数及 0 个参数分别测试。
参考思路:先查 arg.length 再遍历;元素是 var,比较前 as int。体会"灵活接口 + 严格校验"的组合。
编写 swapacc.xl:① swap(byref int a, byref int b) 交换两变量;② acc(int x, out int total) 把 x 乘 10 后写入 total。顶层声明变量调用验证,并刻意试一次 swap(a, b) 不带标记,观察报错。
参考思路:体会调用点标记的自我文档价值——读代码的人一眼看出哪两个变量会被改。