1.4 运算符、表达式与控制流


文档摘要

1.4 运算符、表达式与控制流 本节摘要:Lua 的运算符刻意做得少:没有自增自减、没有复合赋值、位运算到 5.3 才转正。本节过完算术、关系、逻辑、连接、长度、位运算六组运算符的行为细节与惯用法(and/or 缺省值是重中之重),给出优先级记忆阶梯,然后补齐控制流四件套与 break、goto 的用法。少即是稳:运算符越少,宿主嵌入与版本迁移的坑越少。 为什么运算符这么少 从 C 系语言过来的读者第一周一定会问三件事:怎么没有 ?怎么没有 ?三元表达式在哪?Lua 的回答是一句反问:这些运算符各自省下的字符数,与它们给词法分析、给教学、给版本迁移增加的规则,哪个更贵? 多打三个字符,换来的是"赋值就是赋值、不存在赋值与运算混合的歧义";没有三元运算符 ,用 表达(下面细讲,有坑);

1.4 运算符、表达式与控制流

本节摘要:Lua 的运算符刻意做得少:没有自增自减、没有复合赋值、位运算到 5.3 才转正。本节过完算术、关系、逻辑、连接、长度、位运算六组运算符的行为细节与惯用法(and/or 缺省值是重中之重),给出优先级记忆阶梯,然后补齐控制流四件套与 break、goto 的用法。少即是稳:运算符越少,宿主嵌入与版本迁移的坑越少。

为什么运算符这么少

从 C 系语言过来的读者第一周一定会问三件事:怎么没有 ++?怎么没有 +=?三元表达式在哪?Lua 的回答是一句反问:这些运算符各自省下的字符数,与它们给词法分析、给教学、给版本迁移增加的规则,哪个更贵?

i = i + 1 多打三个字符,换来的是"赋值就是赋值、不存在赋值与运算混合的歧义";没有三元运算符 ?:,用 a and b or c 表达(下面细讲,有坑);没有复合赋值,左值永远只求值一次的心智负担也随之消失。每删一个运算符,"一页文法"就薄一分,宿主作者把文法抄进手册、玩家社区把语法背进脑袋的成本就低一分。这不是懒惰,是克制的收益。

算术与连接:行为细节

算术运算符七个:+ - * / // % ^,外加一元负号。逐个挑细节说。

除法恒浮点,整除用 //

> print(7 / 2, 6 / 3) 3.5 3.0 > print(7 // 2, -7 // 2) 3 -4

// 向负无穷取整(地板除),所以 -7 // 2-4 而不是 -3。这与 % 的定义配套:a == b * (a // b) + a % b 永远成立。做分页、分桶、环形缓冲下标时,地板除比向零截断更不容易出负下标。

取模的结果符号跟除数走

> print(5 % 3, -5 % 3, 5 % -3) 2 1 -1

C 程序员要特别当心:C 里 -5 % 3-2,Lua 是 1。这个设计的好处是可以直接用取模把任意数折进正区间:

local function wrap(v, n) return v % n end -- 结果必在 0 到 n-1 print(wrap(-1, 24)) --> 23

游戏里的昼夜小时、环形队列的读写指针,全靠这一条性质保持非负。

幂运算 ^ 恒返回浮点

> print(2 ^ 10) 1024.0

要整数幂得 math.floor 收尾。开方也常用 ^x ^ 0.5

拿一个网关现场的算例把这组运算符串起来。需求:把令牌桶容量按流量档位分级,档位之间用对数刻度,且限流阈值向下取整到千位。三行核心逻辑:

local tier = math.floor(math.log(bps + 1, 10)) -- 档位:数量级 local cap = math.floor(bps / 1000 + 0.5) * 1000 -- 四舍五入到千 local burst = cap // 10 -- 突发配额为一成

log 第二个参数给底数;四舍五入 Lua 没有 round 函数(5.3 前),floor(x + 0.5) 是通行写法;// 保证配额是整数。三个运算符各司其职——这正是 Lua 算术运算符"少而够用"的缩影。

字符串会被算术强转。字符串在算术运算里按数字解析:

> print("10" + 1, "0x10" + 0, "3.5" * 2) 11 16 7.0

十六进制字面量也认。但注意这个转换只发生在算术上下文,比较运算里字符串与数字混比在 5.3 之后直接报错(后面讲)。

连接运算符 ..。把两边接成新字符串,数字自动转字:

> print("lua" .. 5.4 .. " 嵌入" .. 99) lua5.4 嵌入99

一个著名的坑:整数直接显示,浮点总带 .0

> print(3 .. "", 3.0 .. "") 3 3.0

从 C 或 JSON 传来的值若被标成浮点,拼进 URL 或命令时会多出 .0,网关脚本的高频事故之一。稳妥做法是格式化输出:("%d"):format(v)。还有优先级细节:.. 的结合是右结合,且与数字在一起时 2 .. 3 会因为词法歧义直接报错(2..3 被先读成 2.),写 2 .. 3 加空格即可。

关系与长度:比较的规矩

关系运算符六个:== ~= < <= > >=。注意不等于写作 ~=,不是 !=——从 C/Java 过来的手指要适应两周。

等于:number 之间比数学值(3 == 3.0 为真);table、function、userdata、thread 之间比引用(是不是同一个对象);不同类型之间 == 永远为假(number 与 string 不会隐转)。

大小:只定义在 number 之间、string 之间(按字节序)。5.3 起,字符串与数字比大小直接抛错

> print(10 < "9") input:1: attempt to compare number with string

5.1 时代这里会按数字转换,升级现场经常在这里翻车。要按数字比就先 tonumber

长度运算符 #:字符串返回字节数,表返回一个"边界"(border)——数组部分有多长。表的长度有著名的"洞"问题(数组中间出现 nil 时 # 的结果实现相关),我们留到第 3 章用整整一节讲透,这里先记住:字符串的 # 数字节,中文每字三字节

逻辑:短路出惯用法

逻辑运算符三个:andornot。它们不返回 boolean,而是返回其中一个操作数,且短路求值:

  • a and b:a 为假(nil/false)返回 a,否则求值并返回 b;
  • a or b:a 为真返回 a,否则求值并返回 b。
> print(nil and 99, false and 99) nil false > print(1 and 99, nil or "default") 99 default > print(not nil, not 0) true false

这套语义撑起了 Lua 最高频的三个惯用法。

缺省值

local port = host_arg or 6379 -- 没传就用默认端口 local name = cfg.name or "worker-" .. os.time()

三元替身与它的坑

local tag = verbose and "DEBUG" or "INFO" -- verbose 为真得 DEBUG

坑在"真值分支"不能是 nil/false:x and nil or y 永远返回 y。安全写法用 if 或调整分支值。

守卫链

if pkg and pkg.entry and pkg.entry.ready then pkg.entry.run() end

逐级下探,任何一级缺席就整体跳过。解析多层配置、调用可能不存在的回调时,这一句能顶一串嵌套判断。反过来,or 链可以给"第一个非空值":

local label = env_label or cfg_label or "unknown"

💡 关键直觉:and/or 不是"逻辑运算",是"带短路的选择运算"。选择运算 + 只有 nil/false 为假,两者相乘就是 Lua 条件表达式的全部语法经济性。

位运算与优先级

5.3 起位运算符转正:& | ~ >> <<,一元 ~ 是按位取反。都是 64 位整数语义。5.1/5.2 需要库(bit 库或 LuaJIT 的 bitops),OpenResty 现场用的 LuaJIT 提供 bit.bandbit.bor 等函数形式——写跨版本脚本时这是兼容层的常客。位运算的典型用途是权限位、哈希散列、协议打包:

local READ, WRITE, EXEC = 1, 2, 4 local perm = READ + WRITE print(perm & EXEC == 0) --> true,无执行权限

运算符优先级一图记牢,从高到低:

01-04-fig01

两个高频误判点:.. 比加减还低,所以 "x=" .. 1 + 2"x=" .. 3,没问题;但 1 + 2 .. "x" 会先算加再接,都是意外地"对"。真正的坑是幂右结合2^3^22^(3^2) 等于 512,不是 64。拿不准就加括号——Lua 社区的风格本来就是括号友善的。

用解释器验证几个容易记错的组合,比背表可靠:

> print(not true == false) -- not 先于比较? true > print(2 .. "" == "2") -- 连接先于比较? true > print(1 + 2 .. "x") -- 加减先于连接 3x > print(-2 ^ 2) -- 幂高于一元负号 -4.0

第三个例子 1 + 2 .. "x" 输出 3x,确认加减优先于连接;第四个 -2 ^ 2 是负的四次方结果 -4.0,因为一元负号的优先级低于幂。这四行值得自己再敲一遍,肌肉记忆比表格牢靠。

控制流四件套

if / elseif / else。注意拼写是 elseif 一个词,写成 else if 会多要求一个 end

local function level(score) if score >= 90 then return "S" elseif score >= 60 then return "B" else return "C" end end print(level(95), level(72), level(30)) --> S B C

while:先判后跑。repeat...until:先跑后判,条件可以用循环体内新造的局部变量:

local line, n = nil, 0 repeat line = read_next() -- 假设宿主注入的读函数 n = n + 1 until not line or n >= 8

数字型 for:起点、终点、步长,起终值只在进入时求值一次:

> for i = 10, 1, -2 do io.write(i, " ") end 10 8 6 4 2 > for i = 1, 3 do io.write(i, " ") end 1 2 3

步长为负则倒着数;循环变量 i 是循环体内的局部量,出了循环不可见。一个浮点细节:步长可以为小数,但浮点累积误差可能让"最后一次"缺席,边界敏感的循环用整数下标再换算。

泛型 for:遍历集合的统一句式,遍历什么由迭代器决定:

local t = { host = "redis", port = 6379, proto = "resp" } for k, v in pairs(t) do print(k, v) end

pairs 遍历整张表(顺序不保证),ipairs 只走 1 到 n 的数组段。这一对的区别与"为什么顺序不保证",第 3 章展开。泛型 for 的本质是"反复调用迭代函数直到返回 nil",第 2 章讲闭包时会手写一个迭代器。

break 与 goto。break 跳出最内层循环;5.2 引入 goto,标签写作 ::name::,最常见用途是模拟 continue(Lua 没有 continue 关键字):

for i = 1, 10 do if i % 3 == 0 then goto continue end io.write(i, " ") ::continue:: end -- 输出:1 2 4 5 7 8 10

goto 不能跳进别的块、不能跳进局部变量的作用域,限制比 C 严——又是"给静态检查留活路"的克制设计。5.1 现场没有 goto,continue 用 repeat ... until true 包一层或改写条件实现:

for i = 1, 10 do repeat if i % 3 == 0 then break end -- 这里的 break 只跳出 repeat io.write(i, " ") until true -- 立即结束,等效 continue end

until true 让 repeat 块跑一遍就结束,块内 break 于是变成"跳过本次剩余逻辑"的 continue。这个惯用法在老版本现场非常常见,读到时不要以为是笔误。

嵌套循环的提前退出。break 只能跳一层,双层循环要从内层直接退到外层,三种写法按年代排列:goto 一跳到底(5.2+,最清晰);布尔标志逐层判断(最通用);把内层抽成函数用 return(最干净,但要重构代码):

-- 双重查找:在矩阵里找第一个负数,找到即整体退出 local matrix = { {1, 2}, {3, -1}, {4, 5} } for _, row in ipairs(matrix) do for _, v in ipairs(row) do if v < 0 then print("found", v) goto done end end end ::done::

输出 found -1。控制流的设计哲学到这一步已经完整露面:Lua 宁可少给跳转工具,也要保证每条跳转在块结构里可静态判定。写惯了自由跳转的语言,刚上手会觉得束手束脚,两个月后你会开始欣赏这种约束——脚本是要被宿主静态分析、按块替换、按行计费的代码,规矩越多,机器越好办事。

把四件套放回寄宿现场:控制流是宿主与脚本之间"每次调用执行多久"的谈判筹码。宿主通常把脚本调用限制在一个事件回调内(一次碰撞、一个请求、一条定时器),循环若写飞了,宿主的帧预算就爆了。所以真实的游戏与网关脚本里,你会看到大量"有界循环":上限计数、超时判断、分批处理。写脚本时养成同样的习惯——你永远住在别人的时间片里。

关于"缺运算符"的三个问答

为什么没有 continue? 官方解释是 goto 加标签已完全覆盖 continue 的语义,再加一个关键字要动文法。社区吵了十几年,最后大家习惯了 goto continue 或反向条件写法。判断:值得,因为 continue 只是一层循环的事,goto 是通用机制,一页文法寸土寸金。

为什么没有复合赋值? 复合赋值要求左值是"可寻址表达式"这个概念,Lua 的赋值语义是"对名字或表字段赋值",刻意保持两侧形式简单。写 i = i + 1 的三个额外字符,换来词法与教学的双份简单。

三元运算符为什么不用 ?: 因为 a and b or c 已经覆盖 95% 的场景,剩下 5%(真值分支为 nil/false)用 if 更清楚。语言设计里有个通则:当一个惯用法已经成型,为剩下边角加语法的性价比极低。Lua 把这条通则执行得最彻底。

表达式经济的最后一课

数一数本节出现的运算符:算术七、关系六、逻辑三、连接一、长度一、位运算六——二十出头,两行就能列完。与之对照的是它们撑起的惯用法密度:缺省值、守卫链、真值表紧凑、取模折正、地板除分页、位标志权限。Lua 的证据是:运算符的数量与语言的表达力不是线性关系,惯用法的数量才是。一个 and/or 短路选择顶一个三元运算符,一个取模折正顶一串 if-else。把本节的惯用法练成肌肉记忆后,你会发现自己写出的 Lua 天然就是"社区标准风格"——不是模仿来的,是这些惯用法本来就是社区在有限运算符上长出的最优解。

控制流同理。四件套没有提供 do-while、没有提供 switch、没有提供 foreach——多出来的那点便利,换来的是每个宿主、每个静态工具都要多认几种形态。形式少,语义清,这六个字是 Lua 语法经济的总纲,也是它三十年来手册薄、移植易、门坎低的全部原因。

本节要点回顾

  • 运算符少是设计:没有 +++=?:,换来文法一页、迁移坑少,条件选择交给 and/or 短路;
  • 地板除与取模配套a == b*(a//b) + a%b),取模符号跟除数走,可直接把下标折进正区间;
  • 字符串在算术里强转、在比较里报错(5.3+),浮点拼接带 .0 是网关高频事故;
  • ~= 不是 !=,表与函数按引用比较,长度符 # 对字符串数字节;
  • and/or 是带短路的选择运算,缺省值、守卫链、三元替身三大惯用法要形成肌肉记忆,真值分支为 nil/false 时慎用三元替身;
  • 优先级记两件事:幂右结合、.. 低于加减;拿不准加括号;
  • 控制流四件套加 break/goto,无 continue 用 goto 模拟;循环要有界——脚本住在宿主的时间片里。

至此入住培训完成。下一章进入函数:Lua 里唯一可移植的"行为单元",也是宿主与脚本互相调用的全部载体。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U