类型之间怎么换、怎么问、怎么取——xlang 的三套转换机制一次讲清。
第 3 章我们见过一条铁律:"n = " + n 报错——xlang 不做隐式类型转换。那想把数字放进字符串怎么办?把字符串 "42" 变回数字呢?var 里装的值怎么拿出来用?
本章给出完整答案。xlang 把"类型之间怎么流动"分成三套边界清晰的机制:安全拓宽(自动、无损、静默)、显式转换(写明清清楚楚的意图)、is/as(运行时的类型问答与取出)。学完你会理解:xlang 不是"不让转",而是要求每一次有风险的转换都由你亲笔签名。
掌握安全拓宽规则;熟练使用 int()/float()/string()/time() 转换函数族;理解显式窄化的环绕行为;会用 is 判断、as 取出、type() 取类型;看懂混合运算的结果类型。
| 机制 | 写法 | 何时发生 | 失败代价 |
|---|---|---|---|
| 安全拓宽(隐式) | 直接赋值/传参 | 编译期检查,永远无损 | 不允许的转换编译期报错 |
| 显式构造转换 | int(x) string(x) 等 | 你亲手写出 | 解析失败按目标类型规则处理 |
| is / as | v is T / v as T | 运行期 | is 返回 false;as 失败是 fatal |
一句话:能自动的永远安全,有风险的必须手写,运行时的事用 is/as。这条主线贯穿全章。
目标类型能无损表达源类型的所有值时,赋值、传参、返回自动放行,不需要任何标记:
int8 small = 100 int64 big = small // int8 → int64:拓宽,自动 bool flag = true int n = flag // bool → int:自动,得 1 uint8 u = 200 int64 s = u // uint8 → int64:自动(目标严格更宽)
完整规则:
bool → 整数、浮点:自动(true→1,false→0);float32 → float64/float:自动;反向不自动;float(x)(见坑 1);int8 a = -8、uint8 b = 255 合法——字面量在编译期能证明落在范围内;uint8 c = -1 则报错(第 3 章坑 3)。另一方向的隐式收窄一律拒绝:int64 big = 1000 赋给 int8 会得到熟悉的报错 [sem] 初始化表达式类型和声明类型不匹配。
需要跨"非安全"边界时,用类型名当函数调,把意图写得明明白白:
int a = int("42") // 字符串解析 → 42 float b = float(3) // 整数 → 3.0 string c = string(123) // 数字 → "123" time d = time("2026-06-18") // 字符串解析 → 日期 println(int(3.99)) // 3 —— 浮点截断取整 println(float("2.5")) // 2.5 println(string(true)) // true println(int(null)) // 0 —— null 按目标默认值 println(string(null)) // (空字符串)
要点:
int(3.99) 是截断不是四舍五入;null 在显式转换中按目标类型零值处理:int(null)=0、string(null)="";"n = " + string(n),或更顺手的 f-string / println 多参数。显式转换到更窄的整数类型时,行为是 C 风格的截断/环绕:
println(int8(200)) // -56 —— 200 超出 int8,按 256 环绕 int64 big = 300 println(int8(big)) // 44 —— 300 - 256 int x = -1 println(string(uint8(x))) // 255 —— 负值环绕到无符号
读法:int8(v) 取 v 的低 8 位按有符号解释。值在范围内时转换是无损的;超出范围就静默环绕——编译器相信"你写显式转换,你就知道自己要什么"。这与溢出语义(第 6 章拾遗 2)一脉相承。需要范围检查就自己写 if,或者干脆用更宽的类型。
不同类型的操作数相遇,结果类型按"公共类型"规则推导。实测制表:
| 表达式 | 结果类型 | 规则 |
|---|---|---|
int8 + int64 | int64 | 窄侧安全拓宽到宽侧 |
int64 + 1(字面量) | int64 | 字面量归入另一侧类型 |
uint64 & 0xFF | uint64 | 同上 |
int + float、int64 + float64 | float | 算术表达式中整数可向浮点提升 |
int32 + uint32 | int | 落到公共整数类型 |
int8 << 1 | int8 | 移位结果固定为左操作数类型 |
注意一个容易混淆的对比:赋值语境下 float b = int_a 报错(隐式 int→float 不允许,见坑 1),但算术表达式里 int + float 却合法且得 float。规则的记忆锚点:赋值/传参走严格的安全拓宽表;算术表达式以"能算出合理结果"为目标做公共类型推导。
var 里装的值是什么类型,用 is 问:
var v = 123 println(v is int) // true println(v is string) // false println(v is float) // false var n = null println(n is null) // true —— is null 合法 int i = 5 println(i is int) // true —— 静态类型也可问(编译期尽量折叠)
要点:
expr is Type 返回 bool,判断的是运行时值的实际类型;is null 合法;is var、is auto 非法(坑 4);is 也判断 class 继承关系(child is Base)——第 22 章展开;if v is int then 块内 v 的静态类型仍是 var,要用值仍需 as(7.7 节模式)。as 把 var 中的值检查并取出为具体类型(checked downcast):
var v = 123 int n = v as int println(n, type(n)) // 123 int // 标准用法:is 问、as 取 var w = 1.5 if w is float then float f = w as float println(f * 2) // 3.0 end
as 失败的代价是 fatal:var v = 123 后 v as string,实测进程直接以退出码 4 终止,不输出错误消息、也不能用 try/catch 捕获(功能清单原文:"as 失败时触发运行时 fatal,当前阶段不返回 nullable/option")。这与除零异常不同——ZeroDivisionError 可以被 try/catch 捕获(第 10 章),as 的 fatal 不行。
由此形成铁律:as 之前先用 is 问。不确定类型的 var 永远走"is 判断 → as 取出"两步,绝不裸 as。
"42" as int 是 fatal 不是解析——数字转换请用 int("42")。另外 as var、as auto、as null 均非法。第 3 章起我们一直在用 type(),现在正式认识它:type(expr) 返回表达式的类型值,类型值之间可以比较:
println(type(1) == int) // true println(type("x") == string) // true println(type(1) == type(2)) // true var v = 1.5 println(type(v)) // float —— 运行时实际类型
type() 与 is 的分工:type() 回答"精确类型是什么"(精确身份,不做子类型判断);is 回答"是否满足某个类型"(含继承关系)。调试用 type() 打印,分支判断用 is。类型值的更多玩法(typedef、泛型中的类型参数)在第 15 章。
设计文档的原文口径是:普通赋值/传参只允许精确匹配与安全拓宽,而 int64→float64 会损失精度(2⁶³ 不能被 float64 精确表达),所以 float b = intVar 被拒绝,必须 float(intVar) 显式签名。但算术表达式的目标不同——int + float 的合理结果显然是浮点,强行要求显式会让每个算式都磕磕绊绊,编译器因此按公共类型规则推导为 float。笔者实测确认:int64 + float64 的运算合法且结果为 float。这是"赋值从严、运算从便"的双轨制,理解这条主线比背表格可靠。
xlang 的转换规则不是编译器自己拍脑袋,而是落到 XRT 运行时的统一 API:xrtTypeCanWiden(判断安全拓宽)、xrtTypeConvertValue(按 EXACT / SAFE_WIDEN / EXPLICIT 三档标志执行转换)。编译器语义检查、重载解析、代码生成和 XRT 层使用同一套边界——这意味着你在 C 宿主里(第 38 章)对 xlang 值做转换时,规则与语言内完全一致。"一套规则,两处可用"是 XRT 优先设计原则(第 1 章拾遗)的又一个实例。
本章出现了两种运行期失败:ZeroDivisionError(可 try/catch 的异常)和 as 失败的fatal(退出码 4、无消息、不可捕获)。区别在设计意图:异常用于"调用方可能有合理处理"的可恢复错误;fatal 用于"继续执行只会造成更大破坏"的程序 bug——as 失败意味着你对类型的判断错了,这是逻辑错误而非环境问题。工程推论:自己的代码绝不让 as 裸奔(先 is 后 as);库代码该抛异常还是该 fatal,第 10 章和第 42 章还会回来讨论。
以下报错均为真实输出。
原因:int→float 不在安全拓宽表里(可能损失精度),赋值语境必须显式。正确做法:float b = float(a)。注意对比:表达式 a + 1.5 合法(算术公共类型推导),规则因语境而异。
原因:显式转换是截断——直接砍掉小数部分。int(-3.99) 同理得 -3(向零取整)。正确做法:要四舍五入用标准库数学函数(math.round 族,第 36 章);要向下/向上取整用对应函数。
原因:as 失败是运行时 fatal——没有报错文本、不能 try/catch,进程直接结束。排障时看到程序"无声退出"先检查裸 as。正确做法:永远 if v is string then ... v as string ... end 两步走。
原因:var/auto 不是"具体类型",作为 is/as 的目标没有意义,语言直接禁止。同理 as null 非法(但 is null 合法)。正确做法:is/as 的目标写具体类型:int、string、float、bool、time、class 名等。
| 知识点 | 速查 |
|---|---|
| 三套机制 | 安全拓宽(自动无损)/ 显式转换(手写签名)/ is·as(运行时问答)。 |
| 安全拓宽 | 同符号升宽自动;uint→int 需严格更宽;int→uint 不自动;bool→数值自动;float32→float64 自动。 |
| 不自动项 | int→float 赋值、float64→float32、任何窄化——均需显式。 |
| 转换函数 | int() 解析+截断、float()、string()、time();null 按目标零值。 |
| 显式窄化 | int8(200)=-56、uint8(-1)=255:C 风格环绕,不报错。 |
| 混合运算 | 窄→宽拓宽;int+float→float;移位结果取左操作数类型。 |
| is | v is T 返回 bool;is null 合法;is var/auto 非法;不做类型收窄。 |
| as | checked 取出;失败 fatal(退出码 4、无消息、不可捕获);先 is 后 as。 |
| type() | 精确类型身份,可比较;调试打印用 type(),分支用 is。 |
int("42") 和 "42" as int 的区别是什么?各自什么下场?float b = a(a 是 int)报错,而 a + 1.5 合法?type(v) == int 与 v is int 何时不等价?编写 convert.xl:对值 "3.7" 依次做 float() → int() 两步转换打印结果;对 255 做 string() 后与 "px" 拼接打印;对 "2026-12-31" 做 time() 后打印。每步用 type() 验证类型。
参考思路:先 float("3.7") 得 3.7 再 int() 得 3;注意 int(float) 是截断。体会"解析 → 数值 → 文本"的完整链路。
编写 guard.xl:函数 describe(var v) 用 if/elseif 分别处理 v 是 int、string、float、null 四种情况各打印一句话,其他类型打印"未知类型"。用一组不同值调用验证。
参考思路:if v is int then ... elseif v is string then ... end;null 用 v is null。这是处理 var 数据的标准安检模式,第 11 章函数讲透后会更好用。
编写 mixed.xl:先预测再实测——int8+int64、uint8+int8、int+float、int16<<2、bool+1 五个表达式的结果类型,用 type() 打印对照,把预测错的整理成笔记。
参考思路:auto r = 表达式; println(type(r));bool+1 的结果可能出乎你的意料(bool 可拓宽到 int)。预测错的地方就是规则没吃透的地方。