第十二课 数据文件与持久性

数据文件
可以借由table构造式来定义一种文件格式。只需要在写数据时做一点额外的工作,读取数据就会变得相当容易。这项技术也就是将数据作为Lua代码 输出,当运行这些代码时,程序也就读取了数据。而table的构造式可以使这些输出代码看上去更像是一个普通的数据文件。
如果数据文件是一种预定义的格式,例如CSV(逗号分割值)或XML,那么可以选择的做法就很少。不过,如果是为了应用而创建的数据文件的话,那么就可以使用Lua的构造式作为格式。在这种格式中,每条数据记录表示一个Lua构造式。这样,原来以这种形式书写的数据文件:
Donald E.Knuth,Literate Programming,CSLI,1992
现在可以改为:
Entry{"Donald E.Knuth",
"Literate Programming",
"CSLI",
1992}
记住,Entry{<code>}和Entry({<code>})是完全等价的,都是以一个table作为参数来调用函数Entry。因此,上面这段数据也是一个Lua程序。为了读取该文件,我们只需要定义一个合适的Entry函数,然后运行此程序就可以了。例如,以下程序计算了数据文件中条目的数量:
local count = 0
function Entry (...) count = count + 1 end
dofile("data")
print("number of entries:" .. count)
收集数据文件中所有作者的姓名:
local authors = {} --作者姓名的集合
function Entry(b) authors[b[1]] = true end
dofile("data")
for name in pairs(authors) do print(name) end
可以看到这些代码都是采用了事件驱动的做法。Entry函数作为回调函数,在dofile时为数据文件中的每个条目所调用。
若文件不是非常大,可以使用名值对来表示每个字段:
Entry{
author = "Donald E.Knuth",
title = "Literate Programming",
publisher = "CSLI",
year = 1992
}
这种格式就是”自描述的数据“格式,其中每项数据都伴随一个表示其含义的简单描述。

串行化
通常需要串行化一些数据,也就是说将数据转换为一个字节流或字符流。然后就可以将其存储到一个文件中,或者通过网络连接发送出去了。串行化的数据可以用Lua代码来表示,这样当运行这些代码时,存储的数据就可以在读取程序中得到重构了。
如果想要恢复一个全局变量的值,那么串行化的结果或许可以是“varname = <exp>”,其中<exp>是一段用于 创建该值的Lua代码,而varname只是一个简单的标识符。
function serialize (o)
if type(o) == "number" then
io.write(o)
elseif type(o) == "string" then
io.write(string.format("%q", o)) --使用format函数可以正确地转义其中的双引号和换行符等其他特殊字符
else <其他情况>
end
end

保存无环的table
保存table有几种方法,选用哪种方法取决于对table的结构作出了哪些限制性的假设。没有一种算法适用于所有的情况。简单的table不仅需要更简单的算法,而且需要更完美的输出结果。
function serialize (o)
if type(o) == "table" then
io.write("{\n")
for k, v in pairs(o) do
io.write(" ", k, "=")
serialize(v)
io.write(",\n")
end
io.write("}\n")
else
error("cannot serialize a " .. type(o))
end
end
尽管这个函数很简单,但却可以完成基本的工作。只要table的结构是一个树结构,它甚至还能处理嵌套的table。这里我们假设 了一个table中的所有key都是合法的标识符。但如果一个table的key为数字或者非法的Lua标识符,那么就会出现问题。一个简单的解决方法就是将这行:
io.write(" ", k, "=")
改为:
io.write(" ["); serialize(k); io.write("] = ")
这样增强了函数的健壮性,但却损失了结果文件的美观性。例如对于调用:
serialize{a = 12, b = 'Lua', key = 'another "one"'}
第一个版本的serialize会输出:
{
a = 12,
b = "Lua",
key = "another \"one\"",
}
而第二个版本则输出:
{
["a"] = 12,
["b"] = "Lua",
["key"] = "another \"one\"",
}

保存有环的table
若要处理具有任意拓扑结构(带环的table或共享子table)的table,就需要采用另外一种方法了,table构造式是无法表示这类table的。所以为了表示“环”,则需要引入名称,接下来这个保存函数要求将待保存的值以及其名称一起作为参数传入。此外,还必须持有一份所有已保持过的table的名称记录,以此来检测环并复用其中的table。使用一个额外的table用作此项记录,这个table以其他table作为key,并以其他table的名称作为value。代码如下:
function basicSerialize (o)
if type(o) == "number" then
return tostring(o)
else --assume it is a string
return string.format("%q", o)
end
end

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 --该value是否已经保存过
io.write(saved[value], "\n") --使用先前的名字
else
saved[value] = name --为下次使用 保持名字
io.write("{}\n") --创建一个新的table
for k, v in pairs(value) do --保存其字段
k = basicSerialize(k)
local fname = string.format("%s[%s]", name, k)
save(fname, v, saved)
end
end
else
error("cannot save a " .. type(value))
end
end
假设准备保存的table的key只为数字或者字符串。函数basicSerialize用于串行化这些基本类型,返回串行化结果。而另一个函数save则完成真正的工作。saved参数是一个table,用于记录已保存过的table。假设有一个table如下:
a = {x = 1, y = 2; {3, 4, 5}}
a[2] = a --环
a.z = a[1] --共享子table
然后调用save("a", a)将它保存为:
a = {}
a[1] = {}
a[1][1] = 3
a[1][2] = 4
a[1][3] = 5
a[2] = a
a["y"] = 2
a["x"] = 1
a["z"] = a[1]
这些赋值语句的实际顺序可能会有所不同,这取决于一个table的遍历顺序。不过,该算法可以保证在一句新的定义中所用到的变量都已经定义过了。
如果想以共享的方式来保存几个table中的共同部分,只需要在调用save时使用相同的saved参数就行。加入有两个table:
a = {{"one", "two"}, 3}
b = {k = a[1]}
如果以独立的方式保存它们,那么结果中不会有共同部分:
save("a", a)
save("b", b)
输出:
a = {}
a[1] = {}
a[1][1] = "one"
a[1][2] = "two"
a[2] = 3
b = {}
b["k"] = {}
b["k"][1] = "one"
b["k"][2] = "two"
然而当使用同一个saved table来调用save时,串行化结果就会共享共同部分:
local t = {}
save("a", a, t)
save("b", b, t)
输出:
a = {}
a[1] = {}
a[1][1] = "one"
a[1][2] = "two"
a[2] = 3
b = {}
b["k"] = a[1]









  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值