第 7 章 / 共 58 章 卷二 运算与控制

类型转换与 is/as

类型之间怎么换、怎么问、怎么取——xlang 的三套转换机制一次讲清。

本章导读

第 3 章我们见过一条铁律:"n = " + n 报错——xlang 不做隐式类型转换。那想把数字放进字符串怎么办?把字符串 "42" 变回数字呢?var 里装的值怎么拿出来用?

本章给出完整答案。xlang 把"类型之间怎么流动"分成三套边界清晰的机制:安全拓宽(自动、无损、静默)、显式转换(写明清清楚楚的意图)、is/as(运行时的类型问答与取出)。学完你会理解:xlang 不是"不让转",而是要求每一次有风险的转换都由你亲笔签名。

本章学习目标

掌握安全拓宽规则;熟练使用 int()/float()/string()/time() 转换函数族;理解显式窄化的环绕行为;会用 is 判断、as 取出、type() 取类型;看懂混合运算的结果类型。

7.1 三套机制总览

机制写法何时发生失败代价
安全拓宽(隐式)直接赋值/传参编译期检查,永远无损不允许的转换编译期报错
显式构造转换int(x) string(x) 等你亲手写出解析失败按目标类型规则处理
is / asv is T / v as T运行期is 返回 false;as 失败是 fatal

一句话:能自动的永远安全,有风险的必须手写,运行时的事用 is/as。这条主线贯穿全章。

7.2 安全拓宽

目标类型能无损表达源类型的所有值时,赋值、传参、返回自动放行,不需要任何标记:

widen.xl
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:自动(目标严格更宽)

完整规则:

  • 有符号 → 有符号:目标位宽 ≥ 源位宽(int8→int16→int32→int64 链条自由上行);
  • 无符号 → 无符号:目标位宽 ≥ 源位宽;
  • 无符号 → 有符号:目标位宽必须严格大于源位宽(uint8→int16 ✓,uint8→int8 ✗);
  • 有符号 → 无符号:不自动(负数无处可去);
  • bool → 整数、浮点:自动(true→1,false→0);
  • float32 → float64/float:自动;反向不自动;
  • 整数 → 浮点:不自动(int64 不能被 float64 无损表达),必须显式 float(x)(见坑 1);
  • 整数字面量例外:int8 a = -8、uint8 b = 255 合法——字面量在编译期能证明落在范围内;uint8 c = -1 则报错(第 3 章坑 3)。

另一方向的隐式收窄一律拒绝:int64 big = 1000 赋给 int8 会得到熟悉的报错 [sem] 初始化表达式类型和声明类型不匹配。

7.3 显式转换函数族

需要跨"非安全"边界时,用类型名当函数调,把意图写得明明白白:

conv.xl
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)="";
  • 拼接字符串的正确姿势(回看第 1 章坑 4):"n = " + string(n),或更顺手的 f-string / println 多参数。

7.4 窄化:环绕,不报错

显式转换到更窄的整数类型时,行为是 C 风格的截断/环绕:

narrow.xl
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,或者干脆用更宽的类型。

7.5 混合运算的类型推导

不同类型的操作数相遇,结果类型按"公共类型"规则推导。实测制表:

表达式结果类型规则
int8 + int64int64窄侧安全拓宽到宽侧
int64 + 1(字面量)int64字面量归入另一侧类型
uint64 & 0xFFuint64同上
int + float、int64 + float64float算术表达式中整数可向浮点提升
int32 + uint32int落到公共整数类型
int8 << 1int8移位结果固定为左操作数类型

注意一个容易混淆的对比:赋值语境下 float b = int_a 报错(隐式 int→float 不允许,见坑 1),但算术表达式里 int + float 却合法且得 float。规则的记忆锚点:赋值/传参走严格的安全拓宽表;算术表达式以"能算出合理结果"为目标做公共类型推导。

7.6 is 运行时类型判断

var 里装的值是什么类型,用 is 问:

is.xl
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 节模式)。

7.7 as 显式取出

as 把 var 中的值检查并取出为具体类型(checked downcast):

as.xl
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。

as 的职责边界:它只做"运行时检查取出"(var→具体类型、Base→Child),不做数字转换。"42" as int 是 fatal 不是解析——数字转换请用 int("42")。另外 as var、as auto、as null 均非法。

7.8 type() 与类型值

第 3 章起我们一直在用 type(),现在正式认识它:type(expr) 返回表达式的类型值,类型值之间可以比较:

type.xl
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 章。

深入细节

拾遗 1:为什么"整数 → 浮点"赋值不自动,加法里却可以

设计文档的原文口径是:普通赋值/传参只允许精确匹配与安全拓宽,而 int64→float64 会损失精度(2⁶³ 不能被 float64 精确表达),所以 float b = intVar 被拒绝,必须 float(intVar) 显式签名。但算术表达式的目标不同——int + float 的合理结果显然是浮点,强行要求显式会让每个算式都磕磕绊绊,编译器因此按公共类型规则推导为 float。笔者实测确认:int64 + float64 的运算合法且结果为 float。这是"赋值从严、运算从便"的双轨制,理解这条主线比背表格可靠。

拾遗 2:XRT 统一转换 API——转换规则的单一事实来源

xlang 的转换规则不是编译器自己拍脑袋,而是落到 XRT 运行时的统一 API:xrtTypeCanWiden(判断安全拓宽)、xrtTypeConvertValue(按 EXACT / SAFE_WIDEN / EXPLICIT 三档标志执行转换)。编译器语义检查、重载解析、代码生成和 XRT 层使用同一套边界——这意味着你在 C 宿主里(第 38 章)对 xlang 值做转换时,规则与语言内完全一致。"一套规则,两处可用"是 XRT 优先设计原则(第 1 章拾遗)的又一个实例。

拾遗 3:fatal 与异常——两种"失败"的哲学

本章出现了两种运行期失败:ZeroDivisionError(可 try/catch 的异常)和 as 失败的fatal(退出码 4、无消息、不可捕获)。区别在设计意图:异常用于"调用方可能有合理处理"的可恢复错误;fatal 用于"继续执行只会造成更大破坏"的程序 bug——as 失败意味着你对类型的判断错了,这是逻辑错误而非环境问题。工程推论:自己的代码绝不让 as 裸奔(先 is 后 as);库代码该抛异常还是该 fatal,第 10 章和第 42 章还会回来讨论。

⚠️ 常见错误

以下报错均为真实输出。

坑 1:把 int 直接赋给 float
# bad.xl 内容: # int a = 5 # float b = a > xl run bad.xl bad.xl:2:11: [sem] 初始化表达式类型和声明类型不匹配: target float, value int

原因:int→float 不在安全拓宽表里(可能损失精度),赋值语境必须显式。正确做法:float b = float(a)。注意对比:表达式 a + 1.5 合法(算术公共类型推导),规则因语境而异。

坑 2:以为 int(3.99) 是四舍五入
> xl run t.xl # println(int(3.99)) 3

原因:显式转换是截断——直接砍掉小数部分。int(-3.99) 同理得 -3(向零取整)。正确做法:要四舍五入用标准库数学函数(math.round 族,第 36 章);要向下/向上取整用对应函数。

坑 3:裸 as 导致进程静默终止
# bad.xl 内容: # var v = 123 # string s = v as string # println(s) > xl run bad.xl (无任何输出,进程退出码 4)

原因:as 失败是运行时 fatal——没有报错文本、不能 try/catch,进程直接结束。排障时看到程序"无声退出"先检查裸 as。正确做法:永远 if v is string then ... v as string ... end 两步走。

坑 4:写 is var / as var
# bad.xl 内容: # var v = 123 # println(v is var) > xl run bad.xl bad.xl:2:14: [sem] is 目标类型不能是 auto/var

原因: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;移位结果取左操作数类型。
isv is T 返回 bool;is null 合法;is var/auto 非法;不做类型收窄。
aschecked 取出;失败 fatal(退出码 4、无消息、不可捕获);先 is 后 as。
type()精确类型身份,可比较;调试打印用 type(),分支用 is。

复习题

✍️ 先遮住答案想一想
  1. 安全拓宽中"无符号到有符号"为什么要求目标严格更宽?举一反例说明。
  2. int("42") 和 "42" as int 的区别是什么?各自什么下场?
  3. 为什么赋值 float b = a(a 是 int)报错,而 a + 1.5 合法?
  4. as 失败与 ZeroDivisionError 的处理方式有何不同?由此得出什么编码纪律?
  5. type(v) == int 与 v is int 何时不等价?

编程练习

练习 1:转换函数全家桶

编写 convert.xl:对值 "3.7" 依次做 float() → int() 两步转换打印结果;对 255 做 string() 后与 "px" 拼接打印;对 "2026-12-31" 做 time() 后打印。每步用 type() 验证类型。

参考思路:先 float("3.7") 得 3.7 再 int() 得 3;注意 int(float) 是截断。体会"解析 → 数值 → 文本"的完整链路。

练习 2:is/as 安检门

编写 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 章函数讲透后会更好用。

练习 3:混合类型推导实验

编写 mixed.xl:先预测再实测——int8+int64、uint8+int8、int+float、int16<<2、bool+1 五个表达式的结果类型,用 type() 打印对照,把预测错的整理成笔记。

参考思路:auto r = 表达式; println(type(r));bool+1 的结果可能出乎你的意料(bool 可拓宽到 int)。预测错的地方就是规则没吃透的地方。