一个让类型随值流动,一个让类型由编译器代笔——两个最容易被误用的"省事"关键字。
从第 4 章起,var 和 auto 就反复出现:var 装过容器、接过 JSON 式的字典,auto 接过几乎一切。它们看着都像"不用写类型",本质却南辕北辙:var 是动态类型——变量的类型随所装的值流动;auto 是类型推断——编译器替你写下那个静态类型,之后与普通静态变量毫无区别。本章把这对"假双胞胎"彻底分开,顺便回答一个工程问题:var 灵活,代价是什么?
理解 var 的 xvalue 动态本质;掌握 auto 的推断规则与边界;背下 var vs auto 对照表;明确 var 运算结果进静态类型的限制;理解 var 容器的共享引用行为;会用 type() 做运行时判断;建立"何时用 var"的工程准则。
var 变量没有固定类型——它的类型就是"此刻装着的值的类型",随时可变:
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 = 1 | static long long | 直接写机器整数 |
var a = 1 | static xvalue | xvoCreateInt(...) 装箱 + 共享化处理 |
这就是 var 的全部真相:静态类型是编译期的承诺,var 是运行期的容器。好处是无所不装(JSON 式数据、异构数组、可变参 arg),代价是每次触碰都要开箱验货(15.4 节与拾遗 1)。
第 7 章的 is/as 是 var 的"安检与取货";第 12 章的可变参 arg 是 var 数组;第 13 章 var 可装函数值。var 是 xlang 动态世界的通用货币。
auto 让编译器从初始化器推断类型,推断结果一经确定就是静态类型,终身锁定:
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 完全同等;var v = 1; auto x = v 中 x 是 var(动态性"传染"),想静态必须 auto x = v as int。auto 的定位是少写字、不少类型:长类型名(array<dict<int>>、第 13 章的函数签名)和一眼明了的初始化(auto m = max(3, 7))是它的主场。
把两节的结论压成一张表——这是本章最需要刻进脑子的一页:
| 维度 | var | auto |
|---|---|---|
| 本质 | 动态类型(xvalue 盒) | 静态类型(编译器代写) |
| 类型何时定 | 运行期,随值流动 | 编译期,一次锁定 |
| 换类型赋值 | 合法(v = 1 后 v = "s") | [sem] 报错 |
| 无初始化器 | 合法(值为 null) | 语法错误 |
| 参与运算 | 运行期分派,结果为 var | 按推断出的静态类型走 |
| type() 返回值 | 当前内容物的类型 | 推断出的固定类型 |
| 性能 | 装箱 + 运行期分派 | 与手写类型完全一致 |
| 典型用途 | 异构数据、可变参、动态 API | 省去冗长类型名 |
一句话:auto 是"帮我写类型",var 是"先别管类型"。前者不损失任何静态安全,后者用安全换灵活——换得值不值,15.7 节谈。
var 可以参与运算——第 8 章复习题已见过 println(v + 1) 合法。规则实测如下:
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 章反复出现):
var 世界的产出要进入静态世界,只有一座桥:as 显式取出(int n = (v as int) + 1),而 as 的纪律第 8 章已立——先 is 后 as。这就是 xlang 的"关税制度":动态世界内部自由贸易,跨境必申报。
var 装容器时是动态容器([1, 2]、{"a": 1} 直接赋给 var 的形态),读写自由:
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 动态容器 | 天然对应数据形态 |
第 7 章学过:type() 返回值的确切类型身份。在 var 身上,它是观察"盒子里现在装的是什么"的窗口:
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 变量"实际装着什么"不再靠猜:
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)——问"是不是动态值"没有意义,要问就问内容。
var 的代价清单(证据见拾遗 1):每次运算经过 XRT 运行期分派;每次装箱/拆箱有内存与引用计数开销;编译期检查全面让位(类型错误推迟到运行期);IDE 式的静态导航无从谈起。灵活从来不是免费的。
工程准则——该用:
不该用:
一句话收束:var 是边境口岸,不是居民区——数据从动态世界进来,尽快 as 成静态类型再加工。
同样是 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"的硬证据:不是风格建议,是生成代码里看得见摸得着的成本。
新手常把 auto 理解成"带保护的 var"。错。auto 在编译期结束后从程序里消失——生成的 C 代码里 auto x = 42 与 int x = 42 一字不差(都是 static long long)。var 则一路活到运行期,xvalue 盒子伴随进程始终。判断用哪个的自测题:"这个变量的类型,我此刻写得出来吗?"写得出来只是嫌长→auto;写不出来(类型随数据走)→var。答错题的代价:该 auto 的用了 var,失去全部静态保护;该 var 的用了 auto,编译器会直接拒收。
观察本章的规则网络会发现一条主线:var 的运算结果是 var;从 var 推断的 auto 是 var;var 进静态变量必须 as;is var / as var 被禁止。合起来就是一道"隔离堤"——动态性只能停留在 var 世界内部,每次跨境都要显式签名(as)。这与第 7 章"有风险的转换必须亲笔签名"、第 12 章"byref 调用点必写标记"一脉相承:xlang 不反对你进入动态世界,但要求你在每一个边界留下笔迹。理解了这条哲学,var 的所有规则都不用背。
以下报错全部来自真实编译器输出(文件路径已缩短)。
原因:auto 推断为 int 后就是 int,终身锁定。想换类型用 var——但请先读 15.7 节确认你真的需要动态性。
原因:没有初始化器编译器无从推断。正确做法:写清类型(int x,第 4 章的声明可后赋值),或给 auto 一个初值。
原因:var 运算结果的静态类型是 var,跨境必须 as:int n = (v as int) + 1。记住第 8 章纪律——as 之前先用 is 确认内容类型,别让 as 裸奔。
原因:auto 推断的是初始化器的静态类型——v 的静态类型是 var,x 就是 var,动态性完成了"传染"。正确做法:auto x = v as int,在推断的源头就完成跨境。
| 知识点 | 规则 | 要点 |
|---|---|---|
| var 本质 | xvalue 动态盒 | 类型随值流动;type() 看内容 |
| auto 本质 | 编译期推断,静态锁定 | 与手写类型零差别 |
| auto 边界 | 必须初始化;从 var 推断得 var | 嫌类型长才用,不是躲类型 |
| var 运算 | 合法,结果静态类型为 var | 进静态世界必须 as |
| var 容器 | 动态、异构、共享引用 | 函数内修改对调用方可见 |
| 性能 | 装箱 + 运行期分派 | 热路径禁 var(cgen 证据) |
| 选型 | 写得出的类型用 auto/显式,写不出才 var | var 是口岸不是居民区 |
auto x = 42 与 var x = 42 在生成代码层面有何差别?auto x = v(v 是 var)推断出的 x 是 var?这是 bug 还是规则?int n = v + 1 又报错?两个说法矛盾吗?static long long 与直接赋值,和手写 int 完全相同;var 版生成 static xvalue 与装箱调用(xvoCreateInt),盒子伴随整个运行期。编写 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 节表格预测每一行输出再运行——预测错的地方就是理解缺口。
编写 pipeline.xl:var 数组 [10, "20", 3.5, "bad"],遍历它:int 元素直接累加,string 元素先 int() 解析再累加(解析失败跳过并计数),float 元素 int() 截断累加,其余类型跳过。最后打印总和与跳过数。
参考思路:is 链分流 + as 取出(第 7、8 章全套)——这是 var 数据"入境加工"的标准流水线。
编写 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 节的准则就再也不用背。