4.1 字符串的表示与常用操作


文档摘要

4.1 字符串的表示与常用操作 本节摘要:Lua 字符串是不可变的字节序列,编码由使用者约定。三种定界(单引号、双引号、长括号)覆盖从短标签到多行模板的全部场景;驻留机制让相同文本共享一份内存、比较达引用级。本节按取段、变换、格式化、查探四组过完 string 库核心,讲清 tonumber/tostring 的互转礼仪与 format 的格式符,最后论证为什么循环拼接必须让位给 table.concat。 不可变的房客 先钉牢一个底层事实:Lua 字符串存字节,不存字符。 在内存里是六个字节(UTF-8 编码每字三字节), 数的是字节数, 切出来的是半个汉字的字节碎片。要"按字符"处理文本,得用 5.3 引入的 utf8 库( 、 ),或者自己按 UTF-8 的字节规律切分。

4.1 字符串的表示与常用操作

本节摘要:Lua 字符串是不可变的字节序列,编码由使用者约定。三种定界(单引号、双引号、长括号)覆盖从短标签到多行模板的全部场景;驻留机制让相同文本共享一份内存、比较达引用级。本节按取段、变换、格式化、查探四组过完 string 库核心,讲清 tonumber/tostring 的互转礼仪与 format 的格式符,最后论证为什么循环拼接必须让位给 table.concat。

不可变的房客

先钉牢一个底层事实:Lua 字符串存字节,不存字符"工作" 在内存里是六个字节(UTF-8 编码每字三字节),#s 数的是字节数,s:sub(1, 1) 切出来的是半个汉字的字节碎片。要"按字符"处理文本,得用 5.3 引入的 utf8 库(utf8.lenutf8.codepoint),或者自己按 UTF-8 的字节规律切分。英文协议、十六进制串、Base64 这些"纯字节"场景则完全不需要关心编码——这正是网关与协议脚本偏爱 Lua 的原因之一:字节就是字节,没有隐式转换

第二个事实:不可变。任何"修改"字符串的操作都返回新串,原串纹丝不动:

> local s = "openresty" > local up = s:upper() > print(s, up) openresty OPENRESTY

为什么坚持不可变?看驻留池:

04-01-fig01

驻留(interning)是"不可变"的直接回报:既然内容永远不会变,相同内容就可以放心共享一份。表键查找、协议字段比较、配置名匹配,这些脚本最高频的操作因此获得了接近指针比较的速度。代价同样直接:拼接是重活,每次 .. 都要申请新内存、拷贝两段内容。

用一段代码把这个代价变成肌肉记忆:

-- 反面教材:十万次拼接 local s = "" for i = 1, 100000 do s = s .. i .. "," end -- 每一次 .. 都分配一个更长的新串 · 总拷贝量是平方级 -- 正确姿势:先进表,一次成型 local buf = {} for i = 1, 100000 do buf[#buf + 1] = i end local joined = table.concat(buf, ",") print(#joined) --> 588890(十万段逗号相连)

规律一句话:拼接超过几十次就该换 table.concat。日志器、响应组装、批量导出全部遵守这条纪律,游戏脚本里长帧的第一嫌疑人往往就是循环拼接。

三种定界与转义

单引号与双引号完全等价,区别只是视觉习惯——含双引号的文本用单引号包可少转义:

> print('he said "lua"') he said "lua" > print("it's fine") it's fine

转义序列用反斜杠:\n 换行、\t 制表、\\ 反斜杠、\65 按字节码给字符、\u{XXX} 按 Unicode 码点插入(5.3 起,按 UTF-8 编码落字节):

> print("\65\66\67") ABC > print(#"\u{4E2D}") 3

长括号 [[...]]转义失效、换行原样,多行文本的标准写法:

local sql = [[ SELECT node, load_avg FROM cluster_state WHERE load_avg > 0.7 AND ts > now() - interval '5 min' ]]

相邻文本可能出现 ]](比如嵌 SQL 或 XML)就用等号升级定界:[==[ ... ]==],与 1.2 节注释的等号分级同一套词法。顺带一个卫生细节:长括号开头的第一个换行会被忽略,所以习惯把 [[ 后立刻换行,内容从第二行开始。

string 库四组核心

字符串库的全部函数都能用冒号形式在字符串值上直呼:s:len()s:sub(1,3)——背后是字符串元表挂了 string 库(机制第 5 章补全)。

取段与测度

> local s = "gateway:8080" > print(#s, s:len()) 13 13 > print(s:sub(1, 7), s:sub(-5)) gateway :8080 > print(s:byte(1), s:char(103)) 103 g

sub 的负下标从尾部倒数;sub(i, j) 含头含尾,越界自动夹取不报错——协议切片时既当 substring 又当安全截断用。bytechar 在字节与数值间互转,解析二进制协议、算校验和的基本件。

变换upper/lowerrep(复制)、reverse

> print(("ab"):rep(3, "-")) ab-ab-ab > print(("warn"):upper()) WARN

格式化 format:与 C 的 printf 家族同源,格式符最常用的五个:

> print(string.format("%s=%d (%.1f%%)", "ratio", 7, 86.666)) ratio=7 (86.7%) > print(string.format("%08.3f", 3.14159)) 0003.142 > print(string.format("%q", 'a"b')) "a\"b" > print(string.format("%x", 255)) ff

%d 整数(浮点无整数表示会报错,1.3 节的坑)、%f 定小数位、%s 字串(自动 tostring)、%q 安全引号化(能被 Lua 重新读回的转义形式,写配置与序列化的利器)、%x 十六进制。所有"数值出文本"的场合都应过 format.. 直拼只在确认整型或已 string 化时使用。

查探与模式find 找位置、match 取捕获、gsub 替换、gmatch 迭代——这四个是 4.2 节的主角,先给一个开胃的:

> local path = "/api/v2/user/1001" > print(path:match("/api/(%w+)/(%w+)/(%d+)")) v2 user 1001

三个圆括号捕获三段文本,match 全部返回——多返回值在文本处理处的第一场演出。

数值与字符串的互转礼仪

1.3 节埋的线这里收拢。**进来(文本转数值)**用 tonumber,转不动回 nil,永远是输入校验的第一行:

> print(tonumber("6379"), tonumber("0x1f"), tonumber(" 42 "), tonumber("n/a")) 6379 31 42 nil

注意三点:认十六进制、容忍首尾空白、失败给 nil 不抛错。**出去(数值转文本)**分场合:给人看用 tostring(浮点带 .0);给程序看用 format 精确控制。两个高频事故再列一遍——浮点直拼多出 .0%d 收到无整数表示的浮点直接报错——它们都是"整浮分家"的下游账单。

还有一个易被忽略的陷阱:字符串在算术里会强转,在比较里不会(1.4 节)。从宿主或配置进来的值若可能是字符串形态的数字,先 tonumber 再参与运算,5.3 之后混比直接抛错,早转早踏实。

序列化的一块基石:%q 与安全引号化

配置回写、缓存落盘、调试打印,都要把 Lua 值变回"能被 load 读回来的文本"。%q 就是干这个的格式符——它把字符串转成语法安全的形式:

> print(string.format("%q", 'he said "hi"\nnew line')) "he said \"hi\"\ new line"

引号、反斜杠、换行、制表符全部转义成 Lua 字面量语法,回读无损。整数用 %d、浮点用 %.17g(保证精度往返)配着用,就能拼出简易序列化器:

local function serialize(t, indent) indent = indent or "" local parts = {} for k, v in pairs(t) do local key = type(k) == "string" and ("[%q]"):format(k) or ("[%d]"):format(k) local val if type(v) == "table" then val = serialize(v, indent .. " ") elseif type(v) == "string" then val = ("%q"):format(v) else val = tostring(v) end parts[#parts + 1] = ("%s%s = %s"):format(indent, key, val) end return "{\n" .. table.concat(parts, ",\n") .. "\n" .. indent .. "}" end print(serialize({ host = "10.0.0.8", port = 6379, tags = { "a", "b" } }))

输出是合法的表构造器文本,load("return " .. text) 能完整读回。真实的序列化库要处理循环引用、函数与 userdata 的拒绝策略——但骨架就是 %q 加递归。自己拼序列化时千万别手工加引号:值里带引号或换行就炸,%q 一律代劳。

字符串比较与排序的语义

字符串比较按字节序(不是本地化字母序):

> print("abc" < "abd", "Z" < "a", "2" < "10") true true false

"Z" < "a" 为真(大写 Z 的字节码更小)、"2" < "10" 为假(按位比字符,'2' 大于 '1')——数字字符串的"大小"与数值大小无关,按数值排先 tonumber。字节序的好处是确定、快、无本地化依赖;代价是人类语感不适用(中文按 UTF-8 字节排,拼音序别想)。协议字段名、常量键的比较全在字节序的舒适区,这也是它被选中的原因。

UTF-8 实战:三个高频操作

中文世界的脚本绕不开 UTF-8,三个高频操作给出可直接抄的实现。

按字符计数utf8.len 一步到位,但它同时是校验器——遇到非法字节返回 nil 加位置:

> print(utf8.len("网关脚本")) 4 > local n, pos = utf8.len("网\xff关") -- 混入非法字节 > print(n, pos) nil 2

校验外部输入的编码合法性,utf8.len 是最便宜的手段(返回 nil 即拒绝)。

按字符截断:先数后切,切点必须在字符边界:

local function utf8_sub(s, i, j) local n = utf8.len(s) i = i or 1; j = j or n if i < 0 then i = n + i + 1 end if j < 0 then j = n + j + 1 end i, j = math.max(i, 1), math.min(j, n) if i > j then return "" end local start = utf8.offset(s, i) -- 第 i 个字符的字节位置 local finis = utf8.offset(s, j + 1) or #s + 1 return s:sub(start, finis - 1) end print(utf8_sub("网关脚本语言入门", 1, 4)) --> 网关脚本 print(utf8_sub("网关脚本语言入门", -2, -1)) --> 入门

utf8.offset 把"字符序号"翻译成"字节位置",转换完成后再用 sub 切——utf8 库负责翻译、string 库负责动刀,两者配合就是全部工作流。

逐字符遍历:utf8.codes 迭代(位置加码点):

for pos, cp in utf8.codes("Lua脚本") do io.write(("%d:%d "):format(pos, cp)) end -- 输出:1:76 2:117 3:97 4:25253 9:26412

注意位置跳变(第 4 个字符占三字节,下一个位置是 9)——码点值可以进 utf8.char 反向变回字符,做拼音首字母映射、全半角转换这类"按字符"的处理时,这个循环是标准入口。

字符串性能的三个数

给 4.1 节的纪律配上量级感:其一,单次 .. 连接的开销与结果长度成正比——连接两个百字节串是纳秒级,但一万次循环连接的累计是平方级,从"感知不到"变成"拖慢一帧"。其二,table.concat 的开销与总长度线性,加上缓冲表的 insert 开销,仍是循环拼接的数百分之一量级。其三,驻留比较与构造哈希的差距:两个相同内容的短字符串比较是一次指针对比,构造一个百字节的新串则要分配加哈希——热路径上"复用已构造的键"比"每次拼接新键"快一个数量级。三个数都指向同一条军规:字符串的省钱之道是少造新串

本节要点回顾

  • 字符串是字节序列# 数字节、sub 切字节,按字符处理上 utf8 库;
  • 不可变换驻留:相同文本共享一份、比较与哈希近引用级;代价是修改必造新串;
  • 循环拼接是平方级灾难,几十次以上一律 table.concat 进缓冲表;
  • 三种定界:单双引号按内容选、长括号多行原样、等号分级避让内嵌 ]]
  • 四组函数:取段(sub 的负下标与安全截断)、变换(rep 带)、格式化(五个格式符,数值出文本的正门)、查探(find/match/gsub/gmatch 留给下一节);
  • 互转礼仪:进来 tonumber 先校验、出去 format 精确控制,整浮分家的账单在拼接处最贵;
  • %q 是序列化基石:语法安全转义、回读无损,手工拼引号是事故温床;
  • 比较按字节序:确定且快,但数字串比大小、本地化字母序都是它的盲区。

下一节进入模式匹配——用几百行实现换来的文本武器。


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