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

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调用都会触发我们定义的函数,自动处理数据。

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

Lua 复制代码
-- 1. 准备一个容器,存作者名(用table的key去重,值随便设为true)
local authors = {}
-- 2. 定义Entry函数:每条数据触发时,提取author字段存起来
function Entry(book_data)
  -- book_data就是data.lua里每个Entry括号里的table
  authors[book_data.author] = true  -- 存作者名,自动去重
end

-- 3. 运行数据文件,触发所有Entry调用
dofile("data.lua")

-- 4. 打印结果:遍历authors,输出所有作者
print("所有作者:")
for author_name in pairs(authors) do
  print("- " .. 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:

Lua 复制代码
-- 程序里的一个复杂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):

Lua 复制代码
-- 串行化函数:把变量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) do
      io.write("  [")
      serialize(k)  -- 递归处理key
      io.write("] = ")
      serialize(v)  -- 递归处理value
      io.write(",\n")
    end
    io.write("}")
  else
    error("不支持的类型:" .. 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里会生成这样的代码:

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

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

Lua 复制代码
-- 辅助函数:串行化数字和字符串(复用逻辑)
function basicSerialize(o)
  if type(o) == "number" then
    return tostring(o)
  else  -- 假设是字符串
    return string.format("%q", o)  -- 安全转义
  end
end
(2)支持有环 table 的串行化函数
Lua 复制代码
-- 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" then
    if 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表)
      end
    end
  else
    error("无法串行化类型:" .. type(value))
  end
end

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

假设要串行化的有环 table:

Lua 复制代码
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

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 保存无环 table 12.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]
相关推荐
豐儀麟阁贵8 小时前
5.6对象
java·开发语言
郝学胜-神的一滴9 小时前
QAxios研发笔记(二):在Qt环境下基于Promise风格简化Http的Post请求
开发语言·c++·笔记·qt·网络协议·程序人生·http
敲代码的嘎仔9 小时前
数据结构算法学习day3——二分查找
java·开发语言·数据结构·学习·程序人生·算法·职场和发展
安冬的码畜日常9 小时前
【JUnit实战3_23】 第十四章:JUnit 5 扩展模型(Extension API)实战(上)
测试工具·junit·单元测试·jdbc·h2·extension模型·junit5扩展
m5655bj9 小时前
如何使用 Python 转换 Excel 工作表到 PDF 文档
开发语言·c#·excel
ᐇ9599 小时前
Java核心概念深度解析:从包装类到泛型的全面指南
java·开发语言
逻极9 小时前
Rust之旅的起点:为什么选择Rust?
开发语言·后端·rust
Tony Bai9 小时前
从 Python 到 Go:我们失去了什么,又得到了什么?
开发语言·后端·python·golang
华如锦10 小时前
使用SSE进行实时消息推送!替换WebSocket,轻量好用~
java·开发语言·网络·spring boot·后端·websocket·网络协议