当前位置: 首页 > news >正文

Lua--数据文件和持久性

你有没有过这样的困扰?想给 Lua 程序存点结构化数据(比如配置项、用户列表、书籍信息),又不想写复杂的解析逻辑 —— 比如手动处理 CSV 的逗号分隔,或者 XML 的标签嵌套?其实 Lua 早就帮我们想好了解决方案,《Lua 程序设计第二版》第 12 章 “数据文件与持久性”,就教我们用 Lua 自身的特性,轻松实现 “数据存得爽、读得快”,全程不用额外学新格式!

一、先解决第一个问题:怎么存数据才方便?——“数据文件” 的思路

平时我们存数据,可能会写个 txt,一行一条用逗号隔开,比如:

Donald E.Knuth,Literate Programming,CSLI,1992
Jon Bentley,More Programming Pearls,Addison-Wesley,1990

但读的时候得 split 逗号、记字段顺序(哪个是作者、哪个是书名),改数据也容易出错。而 Lua 第 12 章给了个更聪明的办法:用 Lua 的 table 构造式当数据格式

1. 数据文件长啥样?—— 就是合法的 Lua 代码!

我们不用写复杂的自定义格式,直接把每条数据写成Entry(...)的调用,括号里放一个 table(键值对,字段名明明白白)。比如存书籍数据的data.lua

-- data.lua:我的书籍数据文件
Entry({author = "Donald E.Knuth",  -- 作者title = "Literate Programming",  -- 书名publisher = "CSLI",  -- 出版社year = 1992  -- 出版年份
})Entry({author = "Jon Bentley",title = "More Programming Pearls",publisher = "Addison-Wesley",year = 1990
})

你没看错,这就是一个合法的 Lua 程序!Entry是我们后面要定义的 “处理函数”,每个Entry(...)就代表一条数据。

2. 怎么读这个数据文件?—— 回调函数 +dofile

读数据的核心逻辑特别简单:先定义Entry函数(告诉 Lua “每条数据该怎么处理”),再用dofile("data.lua")运行数据文件 —— 此时文件里的每个Entry调用都会触发我们定义的函数,自动处理数据。

举个实际需求:我想统计有多少本书,再把所有作者名打印出来。代码可以这么写:

-- 1. 准备一个容器,存作者名(用table的key去重,值随便设为true)
local authors = {}
-- 2. 定义Entry函数:每条数据触发时,提取author字段存起来
function Entry(book_data)-- book_data就是data.lua里每个Entry括号里的tableauthors[book_data.author] = true  -- 存作者名,自动去重
end-- 3. 运行数据文件,触发所有Entry调用
dofile("data.lua")-- 4. 打印结果:遍历authors,输出所有作者
print("所有作者:")
for author_name in pairs(authors) doprint("- " .. author_name)
end

运行后会输出:

所有作者:
- Donald E.Knuth
- Jon Bentley

3. 这种方式的好处:谁用谁知道!

  • 不用记字段顺序:CSV 要记 “第 1 列是作者、第 2 列是书名”,这里直接用book_data.author,看名字就知道是啥,改数据也不怕顺序乱。
  • 支持复杂数据:如果某本书有 “译者”“评分” 等额外字段,直接在 table 里加translator = "XXX",Entry 函数里按需处理就行,不用改整个格式。
  • Lua 原生支持:不用引入任何库,dofile直接运行,效率还高 —— 处理几 MB 的数据文件也就一秒钟。

二、再解决第二个问题:怎么存 Lua 变量?——“串行化” 的思路

有时候我们不是存静态数据,而是要存 Lua 程序里动态创建的变量,比如一个复杂的 table:

-- 程序里的一个复杂table:存用户信息和他的收藏
local user = {name = "Lua爱好者",age = 25,favorites = {books = {"Programming in Lua", "Lua设计与实现"},games = {"塞尔达传说", "星露谷物语"}}
}

怎么把这个user变量存到文件里,下次运行程序还能恢复?这就需要 “串行化”—— 把 Lua 变量转成字符串(或者 Lua 代码),存到文件;下次读的时候,再把字符串转成原来的变量。

1. 串行化的核心:把变量转成 Lua 代码

Lua 里的串行化很讨巧:因为 Lua 的 table 构造式本身就是合法代码,所以我们可以写一个函数,把变量 “翻译” 成 Lua 代码字符串,存到文件里。下次用dofile运行这个文件,就能恢复变量。

比如一个简化的串行化函数(处理数字、字符串、table):

-- 串行化函数:把变量o转成Lua代码字符串
function serialize(o)if type(o) == "number" then-- 数字直接输出(比如123 → "123")io.write(o)elseif type(o) == "string" then-- 字符串用string.format("%q")转义,避免特殊字符(比如引号、换行)io.write(string.format("%q", o))elseif type(o) == "table" then-- table转成构造式(比如{name="xxx"})io.write("{\n")for k, v in pairs(o) doio.write("  [")serialize(k)  -- 递归处理keyio.write("] = ")serialize(v)  -- 递归处理valueio.write(",\n")endio.write("}")elseerror("不支持的类型:" .. type(o))end
end-- 把user变量串行化到文件user_data.lua
local f = io.open("user_data.lua", "w")
io.output(f)  -- 把输出定向到文件
io.write("user = ")  -- 写变量名,方便恢复
serialize(user)
io.write("\n")
io.close(f)

运行后,user_data.lua里会生成这样的代码:

user = {[age] = 25,[name] = "Lua爱好者",[favorites] = {[books] = {[1] = "Programming in Lua",[2] = "Lua设计与实现",},[games] = {[1] = "塞尔达传说",[2] = "星露谷物语",},},
}

下次恢复变量时,只需要dofile("user_data.lua"),程序里就有user变量了,和之前的一模一样!

2. 关键注意点:安全第一

如果变量里有特殊字符串(比如包含引号、换行),直接写文件会出错。比如字符串He said "Lua is great!",用string.format("%q")转义后会变成"He said \"Lua is great!\"",这样存到文件里才是合法的 Lua 代码,不会出错。

另外,章节里还提到处理 “有环的 table”(比如 a 的字段指向 b,b 的字段又指向 a),需要记录已经处理过的 table,避免递归死循环 —— 不过日常中小规模数据,上面的简化函数基本够用了。

三、保存无环的 table:树状结构的直接串行化

1. 核心场景与问题

无环的 table 指没有循环引用、没有共享子表的 table,结构呈 “树状”(如嵌套的数组、普通记录式 table)。这类 table 的串行化不需要处理 “重复引用” 问题,只需通过递归遍历,将每个键值对转换为 Lua 构造式即可。

2. 实现思路:递归遍历 + 安全转义

核心是编写serialize函数,通过递归遍历 table 的所有键值对,将数据转换为 Lua 代码形式(table 构造式),关键细节包括:

  • 基础类型处理:数字直接输出(如1212);字符串必须用string.format("%q")安全转义(避免双引号、换行等特殊字符导致代码非法,如He said "Lua""He said \"Lua\"")。
  • table 递归处理:遇到 table 时,先输出{,再遍历所有键值对(pairs遍历),对键和值分别调用serialize(递归),最后输出}。需注意:若键不是合法的 Lua 标识符(如数字、含特殊字符的字符串),需用方括号[]包裹(如键为2[2],键为"my key"["my key"])。

3. 关键代码示例(文档核心实现)

lua

-- 无环table的串行化函数
function serialize(o)if type(o) == "number" thenio.write(o)  -- 数字直接输出elseif type(o) == "string" then-- 字符串安全转义,避免特殊字符问题io.write(string.format("%q", o))elseif type(o) == "table" thenio.write("{\n")  -- 开始table构造式for k, v in pairs(o) do-- 处理键:递归序列化键,用方括号包裹(兼容非标识符键)io.write("  [")serialize(k)io.write("] = ")-- 处理值:递归序列化值serialize(v)io.write(",\n")endio.write("}")  -- 结束table构造式elseerror("无法串行化类型:" .. type(o))  -- 不支持其他类型end
end

4. 特点与局限

  • 优点:逻辑简单,无需额外追踪,生成的 Lua 代码直观,可直接dofile重建数据。
  • 局限:仅适用于无环、无共享子表的 table;若 table 有循环(如a[2]=a)或共享子表(如a.z=a[1]b.z=a[1]),会导致递归无限循环或重复保存共享部分。

四、保存有环的 table:用 “已保存表追踪” 解决循环与共享

1. 核心场景与问题

有环的 table 指存在循环引用(如a[2]=a,table 引用自身)或共享子表(如a.z=a[1]b.z=a[1],多个 table 引用同一个子表)的 table。此时简单递归会无限循环(循环引用),或重复保存共享子表(浪费空间且无法还原共享关系),需额外机制追踪已保存的 table。

2. 实现思路:“已保存表追踪表”+ 命名引用

核心是在串行化函数中加入 “已保存表追踪表”(通常命名为saved),解决循环和共享问题,具体逻辑:

  • 追踪表saved:这是一个辅助 table,以 “已保存的 table” 为键,对应的 “table 名称” 为值(如saved[a] = "a"saved[a[1]] = "a[1]")。作用:遍历 table 时,先检查当前 table 是否在saved中 —— 若存在,直接引用其名称(避免重复保存);若不存在,记录到saved中,再递归处理其键值对。
  • table 命名规则:为每个 table 的键生成唯一名称(如根 table 名为a,其键1对应的子表名为a[1],键"x"对应的字段名为a["x"]),确保引用时能准确定位。

3. 关键代码示例(文档核心实现)

(1)基础类型串行化辅助函数

lua

-- 辅助函数:串行化数字和字符串(复用逻辑)
function basicSerialize(o)if type(o) == "number" thenreturn tostring(o)else  -- 假设是字符串return string.format("%q", o)  -- 安全转义end
end
(2)支持有环 table 的串行化函数
-- name:当前table的名称(如"a"、"a[1]")
-- value:当前要串行化的table/值
-- saved:已保存表追踪表(默认空table)
function save(name, value, saved)saved = saved or {}  -- 初始化追踪表(首次调用时创建)io.write(name, " = ")  -- 输出“名称 = ”if type(value) == "number" or type(value) == "string" then-- 基础类型:直接串行化io.write(basicSerialize(value), "\n")elseif type(value) == "table" thenif saved[value] then-- 情况1:当前table已保存过→直接引用之前的名称(避免循环/重复)io.write(saved[value], "\n")else-- 情况2:当前table未保存→记录到追踪表,再处理键值对saved[value] = name  -- 记录“table→名称”映射io.write("{}\n")  -- 创建新table(Lua构造式)-- 遍历table的所有键值对for k, v in pairs(value) do-- 生成当前键的唯一名称(如"a[1]"、"a[\"x\"]")local fieldName = string.format("%s[%s]", name, basicSerialize(k))save(fieldName, v, saved)  -- 递归串行化子键值对(传递saved表)endendelseerror("无法串行化类型:" .. type(value))end
end

4. 典型示例(文档中的循环 table 案例)

假设要串行化的有环 table:

a = {x=1, y=2, {3,4,5}}  -- a[1]是子表{3,4,5}
a[2] = a  -- 循环引用:a的索引2指向自身
a.z = a[1]  -- 共享子表:a.z引用a[1]

调用save("a", a)后,生成的 Lua 代码(顺序可能因遍历不同变化,但确保依赖已定义):

lua

a = {}
a[1] = {}  -- 处理子表a[1](未保存过,记录到saved)
a[1][1] = 3
a[1][2] = 4
a[1][3] = 5
a[2] = a  -- 循环引用:a[2]已保存过(saved[a]="a"),直接引用"a"
a["y"] = 2  -- 处理字段y
a["x"] = 1  -- 处理字段x
a["z"] = a[1]  -- 共享子表:a[1]已保存过,直接引用"a[1]"

5. 特点与价值

  • 解决核心问题:通过saved表避免循环递归和共享子表重复保存,确保串行化后的代码能正确还原循环和共享关系。
  • 灵活性:支持任意拓扑结构的 table(含环、共享子表);若多个 table 共享子表,只需传递同一个saved表即可实现共享引用(如save("a", a, t)save("b", b, t),b 的共享子表会引用 a 的子表名称)。

三、两者核心差异对比

维度12.2.1 保存无环 table12.2.2 保存有环 table
适用场景无循环、无共享子表的树状 table有循环引用、有共享子表的任意拓扑 table
关键依赖递归遍历 table递归 +saved表(追踪已保存 table)
核心风险无(无循环,不会无限递归)若无saved表,会无限循环或重复保存
生成代码特点纯 table 构造式,结构紧凑含 table 命名引用(如a[2] = a
典型案例{name="Lua", version=5.1, features={"轻量","灵活"}}a={x=1,y=2;{3,4,5}}; a[2]=a; a.z=a[1]
http://www.dtcms.com/a/553036.html

相关文章:

  • Spark SQL 解锁电商数据密码:窗口函数大显身手
  • 用R语言生成指定品种与对照的一元回归直线(含置信区间)
  • NVR(网络视频录像机)和视频网关的工作方式
  • 如何架设网站服务器网络搭建百度百科
  • opencv 学习: 04 通过ROI处理图片局部数据,以添加水印为例
  • 中小企业网站模板诚信网站平台建设方案
  • chatgpt崩溃了,gpt怎么了
  • [MySQL] 页与索引
  • 2025年AI生成PPT工具评测:以“全链路一体化服务能力”为尺,ChatPPT再登顶
  • 天津特定网站建设推广搜素引擎优化
  • 直播美颜sdk特效功能架构全解析:从图像处理到AI渲染的技术演进
  • 2025强网杯web wp
  • 超融合架构下,如何智能调度让每台虚拟机都“跑得更快”?
  • 【Web应用安全】SQLmap实战DVWA SQL注入(从环境搭建到爆库,完整步骤+命令解读)
  • 从零打造 Telegram 中文生态:界面汉化 + 中文Bot + @letstgbot 搜索引擎整合实战
  • QT 给Qimage数据赋值,显示异常,像素对齐的坑
  • wordpress 整站下载万江做网站
  • 谈谈设计和建设网站体会摄影网站建设的功能有哪些
  • ESP 8684模组上的IO6引脚相关问题处理办法
  • 李宏毅机器学习笔记37
  • 大模型-Qwen-Agent框架:系列Agent功能介绍 (1)
  • 18. React的受控和非受控组件
  • cocos 在animation播放后调整widget右对齐能避免动画position影响对齐
  • RAG_混合检索
  • Pytorch 预训练网络加载与迁移学习基本介绍
  • 企业官网响应式网站作品网站
  • Ubuntu20.04操作系统搭建gitlab详细教程
  • MySQL:with窗口函数说明及使用案例
  • 【C++学习】对象特性--继承
  • 常州外贸网站浦东区建设工程监督网站