表达式是程序的"算式"。这一章把 xlang 的每一个运算符连同边界行为全部过一遍。
卷二开篇。变量和数据就位之后,程序要"动起来"靠的是运算符和控制流。本章解决运算符——不只讲每个符号干什么,更讲那些教科书不爱讲、实践中必踩的边界:整数除法往哪边舍、幂运算为什么只接受整数、and/or 到底省掉了什么、除零和溢出各自是什么下场。
掌握算术、位、比较、逻辑四族运算符及边界行为;会用复合赋值与 ?=;掌握空合并 ??、三元 ? :、安全访问 ?. / ?[];会用 in 做成员判断;背下优先级表的关键次序。
println(7 + 2, 7 - 2, 7 * 2) // 9 5 14 println(7 / 2) // 3 —— 整数除法 println(7.0 / 2) // 3.5 —— 有一个浮点就是浮点除法 println(7 % 2, -7 % 2) // 1 -1 —— 取余符号跟被除数 println(2 ** 10) // 1024 —— 幂运算 println(2 ** 3 ** 2) // 512 —— 右结合:2 ** (3 ** 2)
逐条要点:
7 / 2 = 3,-7 / 2 = -3。想要小数结果,让至少一侧是浮点:7.0 / 2 = 3.5;% 取余的结果符号跟随被除数(与 C 一致);** 是幂运算,只接受整数操作数、返回 int:2.0 ** 2 编译期就报错(见常见错误坑 2)。负指数合法,整数语义下 2 ** -1 = 0;** 右结合:2 ** 3 ** 2 等于 2 ** (3 ** 2) = 512,不是 64;+ 同时是字符串拼接符,但两侧必须都是 string(第 1 章坑 4 已经见过);字符串没有 * 重复运算(坑 3)。字面量 / 0、% 0 在编译期直接报错;运行期(变量作除数)则抛出 ZeroDivisionError 异常——异常的捕获处理是第 10 章的内容。溢出则是另一套语义:整数运算溢出按二进制补码环绕,9223372036854775807 + 1 得到 -9223372036854775808,不报错。需要防溢出要自己检查范围(深入细节拾遗 2 讨论)。
println(0b1100 & 0b1010) // 8 —— 按位与 println(0b1100 | 0b1010) // 14 —— 按位或 println(0b1100 ^ 0b1010) // 6 —— 按位异或 println(~5) // -6 —— 按位取反:~x = -x-1 println(1 << 4, 256 >> 4) // 16 16 —— 左移/右移
位运算面向整数,逐位操作。配合二进制字面量(0b 前缀,第 3 章)可读性最好。左移 n 位等于乘 2ⁿ,右移 n 位等于整除 2ⁿ——这也是它的主要用途:高性能的 2 的幂乘除。标志位、掩码、二进制协议解析(第 56 章 Chip-8 模拟器)是它的主场。
println(3 == 3, 3 != 3, 3 < 5, 3 <= 3) // true false true true println(0.1 + 0.2 == 0.3) // false —— 浮点精度陷阱! println(0.1 + 0.2 ~= 0.3) // true —— 近似相等 println(1000000 ~= 1000001) // true —— 整数也有容差 println("abc" == "abc", "abc" != "abd") // true true —— 字符串按内容比较
六件套 == != < <= > >= 与所有语言一致。两个 xlang 特色:
~= 近似相等:按类型分派到容差比较(浮点用 XRT 的 xrtNumApprox,整数用 xrtIntApprox)。第 3 章拾遗 2 说过浮点不能用 == 比较——~= 就是语言给出的标准答案;== 按内容比较,不是引用比较。逻辑运算符是单词:and、or、not(! 与 not 等价)。没有 && 和 ||——写了就是语法错误(见常见错误坑 1)。
func sideEffect(string name) as bool println("调用了", name) return true end bool r1 = false and sideEffect("f1") // f1 不会被调用 bool r2 = true or sideEffect("f2") // f2 不会被调用 bool r3 = true and sideEffect("f3") // 输出: 调用了 f3 println(r1, r2, r3) // false true true
短路:and 左边为 false 就不算右边;or 左边为 true 就不算右边。上面实测里 f1、f2 的打印根本没出现。短路不只是省性能,更是写"先检查再访问"代码的基础:if x != null and x.length > 0 then——左边挡住 null,右边才安全。
if 1 then 这类写法不合法。值与 null 的比较要写全:x != null。int a = 10 a += 5 // 15 a -= 3 // 12 a *= 2 // 24 a /= 4 // 6 a %= 5 // 1 a = 2 a **= 3 // 8 —— 幂赋值 var b b ?= "第一次" // b 是 null,赋值生效 println(b) // 第一次 b ?= "第二次" // b 已非 null,不生效 println(b) // 第一次
+= -= *= /= %= **= 是"运算并写回"的简写,与展开形式完全等价。?= 是空值赋值:仅当左边为 null 时才把右边赋给它——给配置项填默认值时一行搞定:config ?= "默认配置"。
if a = 2 then 是语法错误(坑 4)。xlang 从语法上根除了 C 家族"if 里少写一个 ="的经典事故。处理可能为 null 的值,xlang 给了三个工具:
??:给 null 一个替补var nick = null auto shown = nick ?? "匿名" // nick 为 null,取替补 println(shown) // 匿名 int a = 5 auto r = a ?? 0 println(r) // 5 —— 非 null 取自身
当前实现口径:?? 用于声明/赋值语句位置;把它直接嵌在函数调用等表达式里会得到 [checked] ... requires statement-level lowering 报错(见常见错误坑 5)。先赋给一个名字再使用即可。
? :int score = 85 string grade = score >= 60 ? "及格" : "不及格" println(grade) // 及格
条件必须是 bool。? : 是表达式(会产出值),适合"二选一赋值";多分支判断请用 if(第 8 章)。
?. 与 ?[]var user = {"name": "小明"} println(user?.name) // 小明 var empty = null println(empty?.name) // null —— 接收者是 null 则整体为 null var arr = [10, 20] println(arr?[0]) // 10 var noarr = null println(noarr?[0]) // null —— 不会崩溃
?.(安全成员)和 ?[](安全索引)在接收者为 null 时返回 null 而不是报错。当前接收者要求是 var 或动态 dict(静态 class 类型不适用)。长链 user?.profile?.email 中任何一环为 null,整体为 null——这是处理"数据可能缺字段"场景的安全绳。
println(2 in [1, 2, 3]) // true —— 值在数组中 println(5 in [1, 2, 3]) // false println("a" in {"a": 1}) // true —— 键在字典中 println("b" in {"a": 1}) // false println("ell" in "hello") // true —— 子串 println(5 in range(0, 10)) // true println(15 in range(0, 10)) // false —— 不含右端点
in 是 Python 风格的成员判断,四种用法:值 in 数组/列表、键 in 字典、子串 in 字符串、整数 in range(含左端点、不含右端点)。range 与 for 循环的完整玩法在第 9 章。
完整优先级表(从高到低,数字越小越先算):
| 级别 | 运算符 | 结合性 |
|---|---|---|
| 1 | () [] . ?. ?[] | 左 |
| 2 | 一元 + - not ! ~ | 右 |
| 3 | ** | 右 |
| 4 | * / % | 左 |
| 5 | + - | 左 |
| 6 | << >> | 左 |
| 7 | < <= > >= in | 左 |
| 8 | == != ~= | 左 |
| 9 | & | 左 |
| 10 | ^ | 左 |
| 11 | | | 左 |
| 12 | and | 左 |
| 13 | or | 左 |
| 14 | ?? | 右 |
| 15 | ? : | 右 |
实测验证三个容易记混的次序:
println(1 + 2 * 3 ** 2) // 19 —— ** > * > + println(not true and false) // false —— not > and println(true or false and false) // true —— and > or println(1 < 2 == true) // true —— < 先于 ==
三个记忆要点:** 比一元负号低(-2 ** 2 = -(2 ** 2) = -4)、比乘除高且右结合;and 高于 or;比较高于等于判断。拿不准就加括号——括号永远合法,且是最便宜的自我文档。
功能清单在"明确不属于已实现稳定口径"中列了一条:and/or 短路逻辑的协议化方案。意思是:未来可能允许自定义类型通过协议方法参与 and/or 求值(类似 Python 的 __bool__),让"对象作为条件"有意义。当前版本 and/or 只对 bool 工作,自定义类型不能重载逻辑运算——但这不影响日常使用,因为内置短路(6.4 节实测)完全正常。知道这条边界的意义在于:不要试图给 class 定义"逻辑真值",那是未来版本的事。
除零抛 ZeroDivisionError,溢出却静默环绕(INT64_MAX + 1 → INT64_MIN,2 ** 63 得 INT64_MIN)。这不是疏忽,而是与 C 对齐的刻意取舍:除零在硬件上产生陷阱,必须处理;整数加减乘的溢出在硬件上就是自然环绕,检查它需要额外指令。xlang 的第一绝对原则是"与 C 完美混编",算术语义因此保持 C 风格。工程推论:涉及金额、计数上限的运算,用前自己查范围;需要任意精度整数时,标准库的大数能力是后续章节的话题。
二进制补码表示下,按位取反等价于 ~x = -x - 1。5 是 ...0101,取反得 ...1010,即 -6。记住公式比记结论可靠:~x 和 -x-1 永远相等。这个恒等式在位掩码代码里经常用到,例如 flags & ~MASK 是"清除某些位"的标准写法。
以下报错均为真实输出。
原因:xlang 的逻辑运算是单词 and/or/not,C 风格的 &&、|| 不在词法中。正确做法:if true and true then。位运算的 &、| 另有用处,不要拿它们当逻辑运算(它们不短路,且优先级完全不同)。
原因:** 只接受整数、返回 int,这是当前语言的明确口径。正确做法:浮点幂用标准库数学函数(math.pow,第 36 章);或写整数幂再转浮点。
原因:Python 里 "ab" * 3 是 ababab,但 xlang 的 * 只做数值乘法,字符串唯一的拼接运算是 +。正确做法:短重复直接 "ab" + "ab" + "ab";长重复用循环或标准库字符串函数(第 18 章)。
原因:赋值是语句不是表达式,无法出现在条件位置——C 家族最恶名昭著的 bug 在 xlang 里根本编译不过。正确做法:比较用 ==:if a == 2 then。
原因:当前 ?? 的 lowering 只覆盖声明/赋值语句位置,嵌在函数调用等表达式中会报 [checked] 错误。正确做法:先赋给中间变量——auto shown = nick ?? "匿名",再 println(shown)。
| 知识点 | 速查 |
|---|---|
| 算术 | + - * / % **;整数除法向零取整;% 符号跟被除数;** 仅整数、右结合。 |
| 除零/溢出 | 字面量除零编译期报错;运行期抛 ZeroDivisionError;溢出按补码环绕不报错。 |
| 位运算 | & | ^ ~ << >>;~x = -x-1;左移乘 2ⁿ、右移除 2ⁿ。 |
| 比较 | == != < <= > >=;字符串按内容比;浮点用 ~= 近似相等。 |
| 逻辑 | and or not(! 同 not);短路;无 && ||;条件必须 bool。 |
| 赋值 | = += -= *= /= %= **=;?= 空值赋值;赋值是语句不是表达式。 |
| 空安全 | ?? 空合并(声明/赋值位置);? : 三元;?. ?[] 安全访问返回 null。 |
| in | 值 in 数组、键 in 字典、子串 in 字符串、整数 in range(不含右端点)。 |
| 优先级关键 | ** 高于乘除且右结合;一元高于 **;and 高于 or;拿不准加括号。 |
7 / 2、-7 / 2、7.0 / 2、7 % -2 各是多少?2 ** 3 ** 2 为什么等于 512 而不是 64?if x != null and x.length > 0 then 是安全的,而交换两边就危险?user?.name 与 user.name 在 user 为 null 时各自的行为是什么?编写 div_lab.xl:打印 7/2、-7/2、7/-2、-7/-2 和对应的 % 结果,用输出验证"向零取整"和"余数符号跟被除数"两条规则。
参考思路:把四组结果排成两列打印,观察符号规律;把其中一组改成浮点,对比差异。
编写 bits.xl:给定 int flags = 0b1011,用位运算完成:设置第 3 位(变 1)、清除第 1 位(变 0)、翻转第 2 位、测试第 0 位是否为 1,每步用 println 输出结果(提示用 0b 字面量对照)。
参考思路:设置用 | mask,清除用 & ~mask,翻转用 ^ mask,测试用 & mask != 0。这是嵌入式和二进制协议的四板斧。
编写 short.xl:写一个带打印的 check(string name) as bool 函数,分别构造 and/or 各两个表达式,使四种情况(短路/不短路 × and/or)各出现一次,运行后解释每一行输出为什么出现或不出现。
参考思路:false and check(...)、true or check(...) 短路;true and check(...)、false or check(...) 不短路。用打印行是否出现作证。