第 15 章 / 共 58 章 卷三 函数

var/auto 与类型推断

一个让类型随值流动,一个让类型由编译器代笔——两个最容易被误用的"省事"关键字。

本章导读

从第 4 章起,var 和 auto 就反复出现:var 装过容器、接过 JSON 式的字典,auto 接过几乎一切。它们看着都像"不用写类型",本质却南辕北辙:var 是动态类型——变量的类型随所装的值流动;auto 是类型推断——编译器替你写下那个静态类型,之后与普通静态变量毫无区别。本章把这对"假双胞胎"彻底分开,顺便回答一个工程问题:var 灵活,代价是什么?

本章学习目标

理解 var 的 xvalue 动态本质;掌握 auto 的推断规则与边界;背下 var vs auto 对照表;明确 var 运算结果进静态类型的限制;理解 var 容器的共享引用行为;会用 type() 做运行时判断;建立"何时用 var"的工程准则。

15.1 var 的动态本质

var 变量没有固定类型——它的类型就是"此刻装着的值的类型",随时可变:

fluid.xl
var v = 42
println(type(v))     // int
v = "hello"
println(type(v))     // string
v = [1, 2]
println(type(v))     // array

底层上,var 对应 XRT 的统一值类型 xvalue:一个带类型标签的"盒子",装 int、装 string、装容器都是同一个盒子换内容。用 xl cgen 看证据——同一个 a = 1,两种写法生成的 C 截然不同:

写法生成的 C 声明赋值生成的代码
int a = 1static long long直接写机器整数
var a = 1static xvaluexvoCreateInt(...) 装箱 + 共享化处理

这就是 var 的全部真相:静态类型是编译期的承诺,var 是运行期的容器。好处是无所不装(JSON 式数据、异构数组、可变参 arg),代价是每次触碰都要开箱验货(15.4 节与拾遗 1)。

回链

第 7 章的 is/as 是 var 的"安检与取货";第 12 章的可变参 arg 是 var 数组;第 13 章 var 可装函数值。var 是 xlang 动态世界的通用货币。

15.2 auto 推断规则

auto 让编译器从初始化器推断类型,推断结果一经确定就是静态类型,终身锁定:

infer.xl
auto x = 42
println(type(x))          // int:推断为 int
x = 100                      // OK:同为 int

auto s = "txt"             // string
auto f = 1.5               // float
auto b = true              // bool
auto a = [1, 2, 3]          // array<int>:静态泛型数组!
println(type(a))           // array<int>

四条规则:

  • 必须有初始化器——auto x 无初值是语法错误(编译器无从推断);
  • 类型锁定——x = "hello" 报 [sem] 赋值表达式类型和目标变量类型不匹配: target int, value string,与手写 int x 完全同等;
  • 字面量推断走默认类型——整数得 int(64 位,第 3 章),小数得 float,同类容器字面量得静态泛型容器;
  • 从 var 推断得 var——var v = 1; auto x = v 中 x 是 var(动态性"传染"),想静态必须 auto x = v as int。

auto 的定位是少写字、不少类型:长类型名(array<dict<int>>、第 13 章的函数签名)和一眼明了的初始化(auto m = max(3, 7))是它的主场。

15.3 var vs auto 的对照

把两节的结论压成一张表——这是本章最需要刻进脑子的一页:

维度varauto
本质动态类型(xvalue 盒)静态类型(编译器代写)
类型何时定运行期,随值流动编译期,一次锁定
换类型赋值合法(v = 1 后 v = "s")[sem] 报错
无初始化器合法(值为 null)语法错误
参与运算运行期分派,结果为 var按推断出的静态类型走
type() 返回值当前内容物的类型推断出的固定类型
性能装箱 + 运行期分派与手写类型完全一致
典型用途异构数据、可变参、动态 API省去冗长类型名

一句话:auto 是"帮我写类型",var 是"先别管类型"。前者不损失任何静态安全,后者用安全换灵活——换得值不值,15.7 节谈。

15.4 var 参与运算的边界

var 可以参与运算——第 8 章复习题已见过 println(v + 1) 合法。规则实测如下:

varops.xl
var a = 10
var b = 3
println(a + b)              // 13:var 运算合法
println(type(a + b))        // int:结果的"内容"是 int

var s = "ab"
var t = "cd"
println(s + t)              // abcd:按内容的类型分派

auto r = a + 1              // r 是 var:运算结果的静态类型是 var
println(type(r))            // int(运行期内容)

关键在静态类型与运行期内容的分离:a + b 的运行结果是 int 值,但它的静态类型是 var——这带来一条铁律(第 8、11 章反复出现):

# bad.xl 内容: # var v = 10 # int n = v + 1 ← var 结果进不了 int 变量 > xl run bad.xl bad.xl:2:11: [sem] 初始化表达式类型和声明类型不匹配: target int, value var

var 世界的产出要进入静态世界,只有一座桥:as 显式取出(int n = (v as int) + 1),而 as 的纪律第 8 章已立——先 is 后 as。这就是 xlang 的"关税制度":动态世界内部自由贸易,跨境必申报。

15.5 var 与容器

var 装容器时是动态容器([1, 2]、{"a": 1} 直接赋给 var 的形态),读写自由:

varcont.xl
var m = {"a": 1}
m["b"] = 2                  // 动态字典:随手加键
println(string(m["a"]))    // 1
println(string(m["b"]))    // 2

var arr = [1, "two", 3.0]  // 异构数组:静态容器做不到
println(string(arr[1]))   // two

容器语义经 2026-07-18 复测更正(第 12、16 章同步勘误):var 容器与静态容器一样是共享引用——赋值、传参不复制,函数内修改对调用方可见;var b = a 之后改 b,a 同样变。唯一例外是静态 array<T> 传给 var 形参:编译器插入转换,函数内拿到隔离副本。早先版本此处按文档口径记为写时复制(COW),RC0 实测从未触发分离。选型对照:

需求选择理由
同类型元素、要共享修改array<T>(或 auto 推断)静态类型 + 共享句柄
异构元素、结构随数据走var 动态容器xvalue 无所不装
配置/JSON 式嵌套数据var 动态容器天然对应数据形态

15.6 type() 的运行时之眼

第 7 章学过:type() 返回值的确切类型身份。在 var 身上,它是观察"盒子里现在装的是什么"的窗口:

watch.xl
var v = 42
println(type(v))      // int
v = "hi"
println(type(v))      // string

auto w = 42
println(type(w))      // int:auto 的结果永远不变

分工回链第 7 章:type() 做精确身份判断与比较,is 做分支条件。在 var 的调试中 type() 尤其顺手——var 变量"实际装着什么"不再靠猜:

guard.xl
var data = [1, 2, 3]
if data is array then
    println("是数组,长度待查")
end
println(type(data))     // array

注意一个语义细节:type(var) 回答的是内容物的类型,不是"var"——var 是容器的静态身份,int/string/array 才是内容的运行期身份。这也正是 is var 被禁止的原因(第 7 章:is 目标类型不能是 auto/var)——问"是不是动态值"没有意义,要问就问内容。

15.7 何时该用 var

var 的代价清单(证据见拾遗 1):每次运算经过 XRT 运行期分派;每次装箱/拆箱有内存与引用计数开销;编译期检查全面让位(类型错误推迟到运行期);IDE 式的静态导航无从谈起。灵活从来不是免费的。

工程准则——该用:

  • 结构由外部数据决定的场景:JSON/XSON 解析结果、配置袋、动态查询结果;
  • 异构容器:一个数组里确实要装多种类型;
  • 语言机制要求处:可变参 arg、catch 到的 error 之外的动态回调值;
  • 脚本化小工具:生命周期短、逻辑直白,静态标注收益低。

不该用:

  • "懒得想类型"——用 auto,别用 var;
  • 热路径计算:循环体内的密集运算,静态类型快一个数量级;
  • 库与 API 的公开签名:静态类型是文档也是契约,var 签名把检查责任推给所有调用方;
  • 长期演进的大项目主干:var 每多一处,重构时的盲区就大一块。

一句话收束:var 是边境口岸,不是居民区——数据从动态世界进来,尽快 as 成静态类型再加工。

深入细节

拾遗 1:var 运算的 cgen 证据——一次加法隔着多少层

同样是 a + b:int a, b 生成的 C 是 (a + b)——一条 CPU 指令;var a, b 生成的是 xl6_xrt_basic_binary_release(xl6_xrt_retain(a), xl6_xrt_retain(b), 0)——两次引用计数 retain、一次 XRT 运行时分派(查两边类型标签、选加法实现、可能装箱结果)、再配对的 release。保守估计几十到上百条指令的差距,外加内存管理簿记。这就是 15.7 节"热路径别用 var"的硬证据:不是风格建议,是生成代码里看得见摸得着的成本。

拾遗 2:auto 不是 var 的"安全版",它们根本不是一个物种

新手常把 auto 理解成"带保护的 var"。错。auto 在编译期结束后从程序里消失——生成的 C 代码里 auto x = 42 与 int x = 42 一字不差(都是 static long long)。var 则一路活到运行期,xvalue 盒子伴随进程始终。判断用哪个的自测题:"这个变量的类型,我此刻写得出来吗?"写得出来只是嫌长→auto;写不出来(类型随数据走)→var。答错题的代价:该 auto 的用了 var,失去全部静态保护;该 var 的用了 auto,编译器会直接拒收。

拾遗 3:动态性沿着"var 接触面"传染

观察本章的规则网络会发现一条主线:var 的运算结果是 var;从 var 推断的 auto 是 var;var 进静态变量必须 as;is var / as var 被禁止。合起来就是一道"隔离堤"——动态性只能停留在 var 世界内部,每次跨境都要显式签名(as)。这与第 7 章"有风险的转换必须亲笔签名"、第 12 章"byref 调用点必写标记"一脉相承:xlang 不反对你进入动态世界,但要求你在每一个边界留下笔迹。理解了这条哲学,var 的所有规则都不用背。

⚠️ 常见错误

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

坑 1:以为 auto 是"随便变"的类型
# bad.xl 内容: # auto x = 42 # x = "hello" ← 以为 auto 像 var 一样能换类型 > xl run bad.xl bad.xl:2:5: [sem] 赋值表达式类型和目标变量类型不匹配: target int, value string

原因:auto 推断为 int 后就是 int,终身锁定。想换类型用 var——但请先读 15.7 节确认你真的需要动态性。

坑 2:auto 不给初始化器
# bad.xl 内容:auto x > xl run bad.xl bad.xl:1:7: [parse] 语法错误

原因:没有初始化器编译器无从推断。正确做法:写清类型(int x,第 4 章的声明可后赋值),或给 auto 一个初值。

坑 3:var 运算结果直接喂给静态变量
# bad.xl 内容: # var v = 10 # int n = v + 1 > xl run bad.xl bad.xl:2:11: [sem] 初始化表达式类型和声明类型不匹配: target int, value var

原因:var 运算结果的静态类型是 var,跨境必须 as:int n = (v as int) + 1。记住第 8 章纪律——as 之前先用 is 确认内容类型,别让 as 裸奔。

坑 4:从 var 推断,以为拿到了静态类型
# bad.xl 内容: # var v = 1 # auto x = v # int n = x ← x 其实是 var! > xl run bad.xl bad.xl:3:9: [sem] 初始化表达式类型和声明类型不匹配: target int, value var

原因:auto 推断的是初始化器的静态类型——v 的静态类型是 var,x 就是 var,动态性完成了"传染"。正确做法:auto x = v as int,在推断的源头就完成跨境。

本章小结

知识点规则要点
var 本质xvalue 动态盒类型随值流动;type() 看内容
auto 本质编译期推断,静态锁定与手写类型零差别
auto 边界必须初始化;从 var 推断得 var嫌类型长才用,不是躲类型
var 运算合法,结果静态类型为 var进静态世界必须 as
var 容器动态、异构、共享引用函数内修改对调用方可见
性能装箱 + 运行期分派热路径禁 var(cgen 证据)
选型写得出的类型用 auto/显式,写不出才 varvar 是口岸不是居民区

复习题

✍️ 先遮住答案想一想
  1. auto x = 42 与 var x = 42 在生成代码层面有何差别?
  2. 为什么 auto x = v(v 是 var)推断出的 x 是 var?这是 bug 还是规则?
  3. var 参与运算合法,为什么 int n = v + 1 又报错?两个说法矛盾吗?
  4. type(v) 对 var 回答的是什么?为什么 is var 反而被禁止?
  5. 热路径为什么禁用 var?用 cgen 证据说明。
参考答案
  1. auto 版生成 static long long 与直接赋值,和手写 int 完全相同;var 版生成 static xvalue 与装箱调用(xvoCreateInt),盒子伴随整个运行期。
  2. 规则:auto 推断初始化器的静态类型,v 的静态类型就是 var。它保证"推断绝不发明新语义"——动态性来自源头,不来自 auto。
  3. 不矛盾。运算在 var 世界内部合法(运行期分派);但结果的静态类型是 var,进 int 变量属于跨境,必须 as 显式签名。
  4. 回答内容物的运行期类型(int/string/array 等)。is var 被禁是因为"是不是动态值"没有判断价值——var 是静态身份,要问的是内容。
  5. int 加法生成一条 C 加法指令;var 加法生成 retain × 2 + XRT 运行时分派 + release 的完整调用序列,差距数十倍指令量级,还附带引用计数簿记。

编程练习

练习 1:类型侦探

编写 detective.xl:依次声明 auto a = 1、auto b = 1.5、auto c = "s"、auto d = [1,2]、var e = 1,用 type() 打印每个变量的类型,然后给 e 连续换装 string、数组,每换一次打印一次 type(e)。

参考思路:对照 15.3 节表格预测每一行输出再运行——预测错的地方就是理解缺口。

练习 2:var 安检流水线

编写 pipeline.xl:var 数组 [10, "20", 3.5, "bad"],遍历它:int 元素直接累加,string 元素先 int() 解析再累加(解析失败跳过并计数),float 元素 int() 截断累加,其余类型跳过。最后打印总和与跳过数。

参考思路:is 链分流 + as 取出(第 7、8 章全套)——这是 var 数据"入境加工"的标准流水线。

练习 3:代价对比实验

编写 cost.xl 并配合 xl cgen:分别写 int a = 1; int b = 2; println(string(a + b)) 和 var 版本,用 xl cgen cost.xl --emit cost.c 生成 C 代码,找到两处加法对应的代码行,直观对比指令量。

参考思路:搜 user_global_a 与 basic_binary 两个关键词。亲眼见过差距,15.7 节的准则就再也不用背。