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

运算符全集

表达式是程序的"算式"。这一章把 xlang 的每一个运算符连同边界行为全部过一遍。

本章导读

卷二开篇。变量和数据就位之后,程序要"动起来"靠的是运算符和控制流。本章解决运算符——不只讲每个符号干什么,更讲那些教科书不爱讲、实践中必踩的边界:整数除法往哪边舍、幂运算为什么只接受整数、and/or 到底省掉了什么、除零和溢出各自是什么下场。

本章学习目标

掌握算术、位、比较、逻辑四族运算符及边界行为;会用复合赋值与 ?=;掌握空合并 ??、三元 ? :、安全访问 ?. / ?[];会用 in 做成员判断;背下优先级表的关键次序。

6.1 算术运算

arith.xl
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)。

除零与溢出:两种不同的下场

# 字面量除零:编译期拦下 > xl run dz.xl # println(7 / 0) dz.xl:1:11: [sem] division by zero literal # 变量除零:运行期抛异常 > xl run dz2.xl # int z = 0 后 println(7 / z) 2:11: uncaught ZeroDivisionError: integer division by zero

字面量 / 0、% 0 在编译期直接报错;运行期(变量作除数)则抛出 ZeroDivisionError 异常——异常的捕获处理是第 10 章的内容。溢出则是另一套语义:整数运算溢出按二进制补码环绕,9223372036854775807 + 1 得到 -9223372036854775808,不报错。需要防溢出要自己检查范围(深入细节拾遗 2 讨论)。

6.2 位运算

bit.xl
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 模拟器)是它的主场。

6.3 比较与近似相等

cmp.xl
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 说过浮点不能用 == 比较——~= 就是语言给出的标准答案;
  • 字符串 == 按内容比较,不是引用比较。

6.4 逻辑运算与短路

逻辑运算符是单词:and、or、not(! 与 not 等价)。没有 && 和 ||——写了就是语法错误(见常见错误坑 1)。

logic.xl —— 用副作用验证短路
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,右边才安全。

条件表达式的结果必须是 bool:xlang 没有"truthy"隐式转换,if 1 then 这类写法不合法。值与 null 的比较要写全:x != null。

6.5 复合赋值与 ?=

assign.xl
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 里少写一个 ="的经典事故。

6.6 空安全三件套

处理可能为 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 章)。

安全访问 ?. 与 ?[]

safe.xl
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——这是处理"数据可能缺字段"场景的安全绳。

6.7 in 成员判断

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

6.8 优先级与结合性

完整优先级表(从高到低,数字越小越先算):

级别运算符结合性
1() [] . ?. ?[]左
2一元 + - not ! ~右
3**右
4* / %左
5+ -左
6<< >>左
7< <= > >= in左
8== != ~=左
9&左
10^左
11|左
12and左
13or左
14??右
15? :右

实测验证三个容易记混的次序:

prec.xl
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;比较高于等于判断。拿不准就加括号——括号永远合法,且是最便宜的自我文档。

深入细节

拾遗 1:and/or 的"协议化方案"是什么、为什么没有

功能清单在"明确不属于已实现稳定口径"中列了一条:and/or 短路逻辑的协议化方案。意思是:未来可能允许自定义类型通过协议方法参与 and/or 求值(类似 Python 的 __bool__),让"对象作为条件"有意义。当前版本 and/or 只对 bool 工作,自定义类型不能重载逻辑运算——但这不影响日常使用,因为内置短路(6.4 节实测)完全正常。知道这条边界的意义在于:不要试图给 class 定义"逻辑真值",那是未来版本的事。

拾遗 2:溢出环绕 vs 除零异常——为什么是两套语义

除零抛 ZeroDivisionError,溢出却静默环绕(INT64_MAX + 1 → INT64_MIN,2 ** 63 得 INT64_MIN)。这不是疏忽,而是与 C 对齐的刻意取舍:除零在硬件上产生陷阱,必须处理;整数加减乘的溢出在硬件上就是自然环绕,检查它需要额外指令。xlang 的第一绝对原则是"与 C 完美混编",算术语义因此保持 C 风格。工程推论:涉及金额、计数上限的运算,用前自己查范围;需要任意精度整数时,标准库的大数能力是后续章节的话题。

拾遗 3:~ 取反为什么 ~5 = -6

二进制补码表示下,按位取反等价于 ~x = -x - 1。5 是 ...0101,取反得 ...1010,即 -6。记住公式比记结论可靠:~x 和 -x-1 永远相等。这个恒等式在位掩码代码里经常用到,例如 flags & ~MASK 是"清除某些位"的标准写法。

⚠️ 常见错误

以下报错均为真实输出。

坑 1:写 && 和 ||——xlang 没有这两个符号
# bad.xl 内容:if true && true then > xl run bad.xl bad.xl:1:10: [parse] 语法错误

原因:xlang 的逻辑运算是单词 and/or/not,C 风格的 &&、|| 不在词法中。正确做法:if true and true then。位运算的 &、| 另有用处,不要拿它们当逻辑运算(它们不短路,且优先级完全不同)。

坑 2:给 ** 喂浮点数
# bad.xl 内容:println(2.0 ** 2) > xl run bad.xl bad.xl:1:13: [sem] power operator operands must be integer

原因:** 只接受整数、返回 int,这是当前语言的明确口径。正确做法:浮点幂用标准库数学函数(math.pow,第 36 章);或写整数幂再转浮点。

坑 3:用 "ab" * 3 重复字符串
# bad.xl 内容:println("ab" * 3) > xl run bad.xl bad.xl:1:14: [sem] numeric operator operands must be numeric

原因:Python 里 "ab" * 3 是 ababab,但 xlang 的 * 只做数值乘法,字符串唯一的拼接运算是 +。正确做法:短重复直接 "ab" + "ab" + "ab";长重复用循环或标准库字符串函数(第 18 章)。

坑 4:在条件里写成赋值 if a = 2
# bad.xl 内容: # int a = 1 # if a = 2 then > xl run bad.xl bad.xl:2:6: [parse] 语法错误

原因:赋值是语句不是表达式,无法出现在条件位置——C 家族最恶名昭著的 bug 在 xlang 里根本编译不过。正确做法:比较用 ==:if a == 2 then。

坑 5:把 ?? 直接嵌在调用表达式里
# bad.xl 内容: # var nick = null # println(nick ?? "匿名") > xl run bad.xl bad.xl:2:14: [checked] dynamic var null coalesce requires statement-level lowering

原因:当前 ?? 的 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;拿不准加括号。

复习题

✍️ 先遮住答案想一想
  1. 7 / 2、-7 / 2、7.0 / 2、7 % -2 各是多少?
  2. 2 ** 3 ** 2 为什么等于 512 而不是 64?
  3. 字面量除零和变量除零,错误分别在什么阶段以什么形式出现?
  4. 为什么 if x != null and x.length > 0 then 是安全的,而交换两边就危险?
  5. user?.name 与 user.name 在 user 为 null 时各自的行为是什么?

编程练习

练习 1:整数除法探测器

编写 div_lab.xl:打印 7/2、-7/2、7/-2、-7/-2 和对应的 % 结果,用输出验证"向零取整"和"余数符号跟被除数"两条规则。

参考思路:把四组结果排成两列打印,观察符号规律;把其中一组改成浮点,对比差异。

练习 2:位运算工具箱

编写 bits.xl:给定 int flags = 0b1011,用位运算完成:设置第 3 位(变 1)、清除第 1 位(变 0)、翻转第 2 位、测试第 0 位是否为 1,每步用 println 输出结果(提示用 0b 字面量对照)。

参考思路:设置用 | mask,清除用 & ~mask,翻转用 ^ mask,测试用 & mask != 0。这是嵌入式和二进制协议的四板斧。

练习 3:短路证明器

编写 short.xl:写一个带打印的 check(string name) as bool 函数,分别构造 and/or 各两个表达式,使四种情况(短路/不短路 × and/or)各出现一次,运行后解释每一行输出为什么出现或不出现。

参考思路:false and check(...)、true or check(...) 短路;true and check(...)、false or check(...) 不短路。用打印行是否出现作证。