Lua 5.3 之前只有一种数字类型——双精度浮点数(double),所有整数运算都在浮点上完成。这在处理大整数 ID、哈希、位掩码时会出问题:超过 2^53 的整数无法精确表示,位运算要靠 bit32 库在 32 位范围内模拟。Lua 5.3 引入了整数子类型(integer subtype),把 64 位有符号整数作为独立类型,并内置了完整的位运算符。
理解这套数值模型是写好底层代码的前提:网络协议的字段解析、状态机的标志位、哈希函数、数据压缩都依赖精确的整数与位运算。本文从类型语义讲起,逐层深入到实战技巧。不同版本的数字类型差异可参考 https://plumephp.com/lua-version-comparison/。
数字子类型:integer 与 float
Lua 5.3+ 的数字有两个子类型,运行时用 math.type 区分:
print(math.type(1)) -- integer
print(math.type(1.0)) -- float
print(math.type(2^53)) -- float(^ 运算总是返回 float)
print(1 == 1.0) -- true(值相等)
关键规则:
- 字面量
1是 integer,1.0是 float; - 除法
/永远返回 float,整除//返回 integer(或 float,取决于操作数); ^(幂)永远返回 float;- 两个 integer 做
+ - *时,溢出会环绕(wrap around)而不是报错或转 float。
print(3 / 2) -- 1.5 (float)
print(3 // 2) -- 1 (integer)
print(7 % 3) -- 1
print(-7 % 3) -- 2 (Lua 的取模结果符号跟随除数)
print(2^10) -- 1024.0 (float)
整数与浮点混算时,integer 会被提升为 float。要显式转换用 math.tointeger 与 //:
print(math.tointeger(3.0)) -- 3
print(math.tointeger(3.5)) -- nil (无法无损转换)
print(3.7 // 1) -- 3.0 (向负无穷取整)
整数溢出的环绕语义
64 位整数范围是 -2^63 到 2^63 - 1。溢出时按二进制补码环绕:
local max = math.maxinteger
print(max) -- 9223372036854775807
print(max + 1) -- -9223372036854775808 (环绕到最小值)
print(max * 2) -- -2
print(math.mininteger) -- -9223372036854775808
环绕在多数场景是"特性"而非 bug:哈希计算、CRC、加密算法都依赖模 2^64 的算术。但在计数、金额这类场景要主动防御:
-- 检测加法溢出:结果符号与操作数不一致
local function add_safe(a, b)
local r = a + b
if (a >= 0 and b >= 0 and r < 0) or
(a < 0 and b < 0 and r >= 0) then
error("整数溢出")
end
return r
end
需要注意的是,LuaJIT 在默认配置下没有整数子类型(LuaJIT 2.1 仍是 double + 可选的 64 位整数 FFI 类型),位运算依赖 bit 库且限定 32 位。跨运行时移植时要特别小心这一点。
位运算符
Lua 5.3+ 内置六个位运算符,直接作用于 64 位整数:
| 运算符 | 含义 | 示例 | 结果 |
|---|---|---|---|
& | 按位与 | 0xF0 & 0x0F | 0 |
| | 按位或 | 0xF0 | 0x0F | 255 |
~ | 按位异或(二元)/ 取反(一元) | 0xFF ~ 0x0F | 240 |
<< | 左移 | 1 << 4 | 16 |
>> | 右移(逻辑) | 256 >> 4 | 16 |
~x | 按位取反 | ~0 | -1 |
print(5 & 3) -- 1 (0101 & 0011)
print(5 | 3) -- 7 (0101 | 0011)
print(5 ~ 3) -- 6 (0101 ^ 0011)
print(~0) -- -1 (全 1,即补码 -1)
print(1 << 63) -- -9223372036854775808 (移入符号位)
位移的边界行为要留意:移位量超过 63 时结果按位宽取模;负移位量会向反方向移动。
print(1 << 64) -- 1 (64 % 64 = 0)
print(1 >> -1) -- 0 (等价于 1 << 1 后再看符号,实际按无符号处理)
位运算只接受整数,传入 float 会尝试转换为整数,无法转换则报错:
print(3.0 & 1) -- 1 (3.0 可无损转 3)
-- print(3.5 & 1) -- error: number has no integer representation
如果要把位运算的结果当无符号数解释(如 32 位哈希),需手动处理符号:
local function to_uint32(n)
return n & 0xFFFFFFFF -- 保留低 32 位
end
print(to_uint32(-1)) -- 4294967295
这套位运算语义与其他语言(如 C、Go)基本一致,通用技巧可对照 位运算操作指南 。
位运算实战:标志位、哈希、压缩
标志位(bit flags)
用单个整数打包多个布尔状态,省内存也省序列化体积:
local Flag = {
ACTIVE = 1 << 0, -- 0b0001
VERIFIED = 1 << 1, -- 0b0010
ADMIN = 1 << 2, -- 0b0100
BANNED = 1 << 3, -- 0b1000
}
local function has(flags, f) return (flags & f) ~= 0 end
local function set(flags, f) return flags | f end
local function clear(flags, f) return flags & ~f end
local function toggle(flags, f) return flags ~ f end
local f = Flag.ACTIVE | Flag.VERIFIED
f = set(f, Flag.ADMIN)
print(has(f, Flag.ADMIN)) -- true
print(has(f, Flag.BANNED)) -- false
f = toggle(f, Flag.ACTIVE)
print(has(f, Flag.ACTIVE)) -- false
快速哈希
异或与乘法构成的哈希函数常用于字符串或整数键的散列:
-- FNV-1a 32 位(截断到 32 位模拟)
local function fnv1a(s)
local h = 2166136261
for i = 1, #s do
h = h ~ s:byte(i)
h = (h * 16777619) & 0xFFFFFFFF
end
return h
end
print(fnv1a("hello")) -- 某个稳定值
位运算版本的哈希比纯算术版本快,且分布更均匀。注意这里用 & 0xFFFFFFFF 把中间结果截到 32 位,模拟 32 位机器的行为。
位域打包
把多个小字段塞进一个整数,是网络协议与存档的常见做法:
-- 打包:版本(4位) | 类型(8位) | 长度(20位)
local function pack_header(ver, kind, len)
return (ver & 0xF) | ((kind & 0xFF) << 4) | ((len & 0xFFFFF) << 12)
end
local function unpack_header(h)
local ver = h & 0xF
local kind = (h >> 4) & 0xFF
local len = (h >> 12) & 0xFFFFF
return ver, kind, len
end
local h = pack_header(2, 17, 1000)
print(unpack_header(h)) -- 2 17 1000
这种"按位切片"的技巧在协议解析里无处不在,配合前面的字节序讨论能完整实现二进制协议。用位域打包的好处是:一个 32 位整数能装下四个 8 位字段,网络包头部从十几个字节压到四字节,且解析只需移位和掩码,比逐个读字段快得多。代价是可读性下降,字段定义必须写成文档或常量表,不能靠猜。
位运算与表索引
在实现稀疏矩阵或棋盘类数据结构时,可以把二维坐标压成一个整数索引,避免嵌套表:
local WIDTH = 1000
local function idx(x, y) return y * WIDTH + x end
local function xy(i) return i % WIDTH, i // WIDTH end
local grid = {}
grid[idx(3, 5)] = "wall"
print(grid[idx(3, 5)]) -- wall
local x, y = xy(idx(3, 5))
print(x, y) -- 3 5
这种"坐标压平"把二维表的哈希查找降为一维,内存占用和访问速度都更优,是游戏地图与图像处理的常用手法。
浮点精度与比较
浮点数遵循 IEEE 754 双精度标准,常见陷阱是十进制小数无法精确表示:
print(0.1 + 0.2) -- 0.30000000000000004
print(0.1 + 0.2 == 0.3) -- false
浮点比较必须引入容差(epsilon):
local EPS = 1e-9
local function feq(a, b)
return math.abs(a - b) < EPS
end
print(feq(0.1 + 0.2, 0.3)) -- true
容差大小的选择取决于数值量级:绝对容差适合小数值,相对容差适合大数值。工程上更稳妥的是混合判据:
local function approx_eq(a, b)
local diff = math.abs(a - b)
return diff < 1e-9 or diff < 1e-9 * math.max(math.abs(a), math.abs(b))
end
IEEE 754 还有几个特殊值需要显式处理:math.huge(正无穷)、-math.huge(负无穷)、0/0(NaN)。NaN 不等于任何值,包括它自己:
local nan = 0 / 0
print(nan == nan) -- false
print(nan ~= nan) -- true (唯一可靠的 NaN 判断)
浮点累加的顺序会影响结果,大数吃小数(absorb)是经典问题:把 1e16 与 1.0 相加,结果仍是 1e16。需要高精度求和时用 Kahan 补偿求和。浮点精度的完整讨论可参考 浮点精度与 IEEE 754 。
数值格式化与转换
输出与解析数字时,格式化选项直接影响可读性与往返精度:
print(string.format("%d", 42)) -- 42 (整数)
print(string.format("%5.2f", 3.14159)) -- 3.14 (宽度5,2位小数)
print(string.format("%x", 255)) -- ff (十六进制)
print(string.format("%X", 255)) -- FF
print(string.format("%e", 1234.5)) -- 1.234500e+03
print(string.format("%g", 0.00001)) -- 1e-05
print(string.format("%q", 'a"b')) -- "a\"b" (带转义)
解析字符串到数字:
print(tonumber("42")) -- 42 (integer)
print(tonumber("3.14")) -- 3.14 (float)
print(tonumber("0xFF")) -- 255 (十六进制)
print(tonumber("1e3")) -- 1000.0
print(tonumber("abc")) -- nil
print(tonumber("101", 2)) -- 5 (按二进制解析)
tonumber 的第二参数指定进制(2~36),这在解析自定义格式时非常有用。要注意 tonumber("42") 返回 integer 而非 float,与 42.0 的 math.type 不同,做序列化时可能影响输出格式。
金额计算建议用整数分(cents)而非浮点元:
-- 不推荐:浮点金额
local price = 19.99
local total = price * 3 -- 59.97000000000001
-- 推荐:以分为单位
local price_cents = 1999
local total_cents = price_cents * 3 -- 5997,精确
print(string.format("%.2f", total_cents / 100)) -- 59.97
常见问题(FAQ)
Lua 5.3 的整数溢出会报错吗?
不会。整数运算溢出时按二进制补码环绕,与 C 的无符号整数行为类似(但 Lua 的整数是有符号的)。需要检测溢出时手动判断符号变化。
LuaJIT 支持位运算符吗?
LuaJIT 2.1 默认仍是 double 数字模型,不支持 &、|、~ 这些运算符,需 require "bit" 使用 bit.band、bit.bor 等函数,且限定 32 位。若代码要同时跑在标准 Lua 和 LuaJIT 上,建议封装一层兼容层。
为什么 0.1 + 0.2 不等于 0.3?
因为 0.1 和 0.2 在二进制浮点中都是无限循环小数,只能近似存储,相加后的舍入误差让结果变成 0.30000000000000004。这是 IEEE 754 的固有特性,不是 Lua 的 bug。比较浮点要用容差。
位运算能处理无符号 64 位整数吗?
Lua 的整数是有符号 64 位,没有无符号类型。要模拟无符号行为,用掩码保留有效位(如 & 0xFFFFFFFF 模拟 32 位无符号),或用 string.pack("I8", n) 打包后再按需解释。
大整数运算超出 64 位怎么办?
标准 Lua 没有大数类型,超出 64 位会环绕。需要任意精度整数时,用纯 Lua 的 lua-bint,或借助 FFI 调用 GMP。浮点场景下则要考虑精度损失,见前文的浮点讨论。
延伸阅读
- https://plumephp.com/lua-performance-optimization/
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。