变量是程序的"储物格"。这一章讲清格子怎么申请、贴什么标签、放在哪一层架子上。
第 3 章认识了"值",这一章解决"值放在哪"。变量看似是最基础的话题,但每个语言在三个问题上的回答都不同:声明时必须给值吗?不给的话里面是什么?同名变量在不同位置谁说了算?
xlang 的回答简洁而严格:可以不初始化(有确定默认值)、同一作用域不允许重复声明、内层块可以遮蔽外层同名变量。本章逐一验证这些规则,最后透过 xl cgen 看一眼全局变量和局部变量在生成 C 代码里的真实差别。
掌握变量声明与初始化、const 常量、各类型默认初始值;理解顶层/函数/块三级作用域与遮蔽规则;会用 auto q, r = f() 解构多返回值;了解 export 变量;理解全局与局部变量在代码生成层面的差异。
声明的基本形态是"C 风格类型前置",等号初始化可选:
int a // 声明,不初始化 int b = 1 // 声明并初始化 var v = 123 // 动态类型 auto c = b + 1 // 编译期推断为 int println(a, b, v, c)
四条规则:
变量可以声明在两个位置:文件顶层(模块级,全文件可见)和函数/块内部(局部,仅当前块可见)。4.4 节专门讲这个差别。
xlang 没有"未定义值"。声明不初始化时,每种类型都有明确的零值:
int a int b string s bool f float d println(a, b, s, f, d)
对照表:
| 类型 | 默认初始值 |
|---|---|
| int / 定宽整型 | 0 |
| float / float32 / float64 | 0.0 |
| bool | false |
| string | 空字符串 ""(注意上面输出中 s 位置是"看不见的空") |
| class / 容器 | null(第 21 章详解) |
| var | null |
"总是有确定初值"是 xlang 相比 C 的重要改进——C 的未初始化局部变量是随机垃圾值,xlang 从语言层面消灭了这类 bug。
用 const 声明的名字在初始化后不可再赋值:
const int MAX = 10 const string APP = "demo" println(MAX, APP)
试图给 const 重新赋值,编译期直接报错:[sem] const 不能被重新赋值(见常见错误坑 2)。
什么时候用 const?一个简单标准:值在写代码时就能确定、且整个程序运行期间不该变——配置上限、数学常量、应用名。const 让"这个不会变"成为编译器强制保证的事实,而不是口头约定。
作用域回答"这个名字在哪些代码里有效"。xlang 的作用域按块嵌套,分三层:
if、for、while 等块内声明的变量,块结束即失效。int counter = 100 // 顶层:全文件可见 func bump() as int int local = counter + 1 // 函数级:能读顶层 counter return local end if true then int inner = 42 // 块级:仅 if 块内有效 println(inner) end println(counter, bump())
两个关键事实:
counter;但在 if 块外访问 inner 会得到 undeclared identifier(见常见错误坑 3);内层块允许声明与外层同名的变量,内层的会"遮住"外层的——这叫遮蔽(shadowing):
func main(...) as int int x = 1 if true then int x = 2 // 遮蔽外层的 x println("块内:", x) end println("块外:", x) return 0 end
遮蔽合法且有时有用(短小的临时用途),但读代码时容易混淆。工程建议:能用新名字就不用遮蔽,团队规范通常禁止它(第 42 章)。
写作本章时实测:在文件顶层的 if/for 块内遮蔽顶层同名变量(例如顶层 int x = 1,顶层 if 块内再 int x = 2),xl check 语义检查通过,但 run/build 时生成的 C 代码报 'x' undeclared——这是代码生成器的已知缺陷。函数内的遮蔽(上面的示例)完全正常。修复版本发布前,请避免在顶层块内遮蔽顶层变量。
函数可以返回多个值(第 11 章正式讲),接收时可以用解构声明一次声明多个变量:
func pair() as (int, string) return 1, "one" end auto q, r = pair() println(q, r) // 1 one println(type(q), type(r)) // int string
auto q, r = pair() 把第一个返回值装进 q(推断为 int)、第二个装进 r(推断为 string)。每个名字独立推断类型,与逐个声明完全等价,只是更紧凑。
auto q, r = 1, 2 这种"右侧直接写字面量列表"的写法不是合法语法(实测报 [parse] 语法错误)——解构声明的右侧必须是返回多值的函数调用。想一次声明多个普通变量,就分行写。顶层变量和常量可以用 export 标记导出,供其他模块或 C 代码使用:
export int exportedCounter = 1 export const int exportedMax = 10 println(exportedCounter, exportedMax)
对普通单文件程序,export 不改变任何行为——它的作用在"被外部看见":导出到 package 供 import(第 35 章)、或生成 C ABI 符号供 C 代码访问(第 33 章)。现在只需认识这个关键字,知道"顶层变量默认是模块私有的,export 让它公开"。
用 xl cgen 观察 4.4 节那样的程序(顶层 int counter = 100 + 函数内 int local),生成的 C 里两者的地位完全不同(节选真实输出):
三个要点:顶层变量被生成为带 __xl6_user_global_ 前缀的 static C 全局变量(static 保证不污染外部符号);它的初始化表达式在模块初始化段统一执行,而不是在声明点;局部变量就是普通的 C 局部变量。这也解释了为什么顶层变量的生命周期等于整个程序运行期,而函数级变量随函数进出而生灭。
实测一个反直觉行为:顶层的 println(x) 写在 int x = 1 之前可以编译运行,输出 0;而函数体内的同样写法报 undeclared identifier。原因就藏在拾遗 1 里:顶层变量是 C 全局变量,程序启动时就是零值,初始化表达式在模块初始化段执行——所以"提前"读到的是默认值 0。甚至可以写 int x = x + 1(自引用初始化),结果是 1。这是语言允许的,但属于坏味道:读到默认值往往不是本意。请遵守"先声明、后使用"的顺序,把这个行为当作理解模块初始化模型的钥匙,而不是可利用的特性。
继续看 cgen 输出可以发现,顶层 string 变量的初始化不是简单的指针赋值,而是 __xl6_user_global_APP = xrtCopyStr("demo", 0u)——运行时为它复制了一份堆内存;程序结束时模块清理段会调用 xrtFree 释放。整数等值类型无需这样的处理。这就是"资源类型生命周期检查"的一个缩影:xlang 在模块边界自动安排资源的获取与释放,第 27 章会完整展开这套机制。
以下报错均为真实输出。
原因:同一层作用域里一个名字只能声明一次。这是刻意的设计——重复声明几乎总是笔误或复制粘贴事故。正确做法:想改值就直接赋值 a = 2(不写类型);确实需要新变量就换个名字。
原因:const 的承诺由编译器强制兑现。正确做法:值会变化就不要声明成 const;如果一处"常量"在程序运行中需要调整,它本来就是变量。
原因:inner 的生命周期在 end 处就结束了,块外这个名字根本不存在。正确做法:把需要带出块的变量声明在块外,块内只赋值:
原因:局部变量严格按文本顺序生效,声明之前的代码里这个名字不存在(顶层变量的"提前可见"是另一套模块机制,见深入细节拾遗 2,不要混淆)。正确做法:把声明挪到使用之前——这也是可读性最好的顺序。
| 知识点 | 速查 |
|---|---|
| 声明 | int a / int b = 1;类型前置;同一作用域不可重复声明。 |
| 默认初始值 | 数值 0 / 0.0、bool false、string 空串、class/容器/var 为 null;无"未定义值"。 |
| const | 初始化后不可再赋值,编译期强制;运行期不变的值才用 const。 |
| 作用域三层 | 顶层(全文件)→ 函数级 → 块级;内层可见外层,反之不行。 |
| 遮蔽 | 内层块可声明同名变量遮蔽外层(函数内正常;顶层块遮蔽有 RC0 缺陷,避免使用)。 |
| 解构声明 | auto q, r = pair() 接收多返回值,逐个推断类型;右侧必须是多返回函数调用。 |
| export | 顶层变量/常量导出给 package 或 C;单文件程序中无行为差异。 |
| cgen 差异 | 顶层变量 → static + __xl6_user_global_ 前缀,初始化在模块初始化段;局部变量 → 普通 C 局部变量。 |
| 声明顺序 | 局部变量先声明后使用(硬性);顶层变量提前可见读到默认值(允许但应避免)。 |
int a 不给初值时,a 的值是什么?xlang 的这一设计与 C 有什么不同?int x = 1 在生成的 C 代码里变成什么?它的初始化在何时执行?编写 defaults.xl:声明 int、float、bool、string、var 五种未初始化变量,用 f-string 逐行打印"int 默认值是 0"这样的调查报告(string 的空值用 [、] 括起来让它现形)。
参考思路:println(f"string 默认值是 [{s}]");var 的默认值是 null,直接插值即可。
编写 scope_lab.xl:顶层声明 int level = 0,函数内声明 int level = 1,函数内 if 块再声明 int level = 2,三个位置各打印一次,验证遮蔽链;然后在函数外打印顶层的 level。
参考思路:三次输出应为 2、1、0。把任意一层 int 去掉(变成赋值),观察输出变化,体会"声明"与"赋值"的区别。
编写一个返回 (int, string) 的函数 stats(),用 auto n, label = stats() 接收并打印;然后执行 xl cgen stats.xl --emit stats.c,在生成的 C 中找到顶层变量(如顶层有声明)的 __xl6_user_global_ 前缀。
参考思路:多返回写法是 return 42, "answer";cgen 文件里搜索 user_global 即可定位。这个练习把 4.6 和拾遗 1 串了起来。