12.1 数据文件
像10.1节介绍的那样,可以由table构造式来定义一种文件格式。这项技术也就是将数据
作为Lua代码来输出,当运行这些代码时,程序也就读取了数据。示例——原来的数据文件格式:
Donald E.Knuth, Literate Programming, CSLI, 1992
Jon Bentley, More Programming Pearls, Addisom-Wesley,1990
现在可以改写为:
Entry{"Donald E.Knuth",
"Literate Programming",
"CSLI",
1992}
Entry{"Jon Bentley",
"MoreProgramming Pearls",
"Addison-Wesley",
1950}
注意: Entry{<code>} 与Entry({<code>}) 是完全等价的,都是以一个table作为参数来调用函数Entry。
为了读取该文件我们只需定义一个合适的Entry,然后运行此程序就可以了。
以下这个程序计算了数据文件中条目的数量:
local count = 0
function Entry(_) count = count + 1 end
dofile("data") -- 文件data里的数据就是上面Entry里面的数据
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}
Entry{
author ="Jon Bentley",
title ="More Programming Pearls",
year = 1990,
publisher ="Addison-Wesley"}
这样收集作者姓名的程序改为:
local authors = {}
function Entry(b)
if b.author then authors[b.author] = true end
end
dofile("data")
for name in pairs(authors) do print(name) end
12.2 串行化
串行化数据(serialization)就是将数据转换为一个字节流或字符流。然后就可以将其存储到一个文件中,
或者通过网络连接发送出去。
串行化后的数据可以用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))
else
<其它情况>
end
end
string.format("%q", str) 函数会用双引号来括住字符串,并且正确地转移其中的双引号和换行符等其它特殊字符。
此外Lua5.1 还提供了另外一种可以以一种安全的方法来括住任意字符串的方法。这种新的标记方式[=[ ... ]=],
用于长字符串。然而这种新方式主要是为手写的代码提供方便的,通过它就不需要改变任何字符串的内容。
但在自动生成的代码中还是推荐使用string.format("%q", str),更方便.在使用这种方法时要注意两个细节问题。
首先,必须使用正确数量的等号。这个正确的数量应比字符串中出现的最长的等号序列还大1。
其次,Lua总是会忽略所有长字符串开头的换行符,避免这个问题的简单方法就是在字符串起始处添加一个换行符。
以下quote函数就是根据这两个注意点编写的处理函数:
function quote(s) -- 查找最长的等号序列
local n = -1
for w instring.gmatch(s, "]=*") do
n =math.max(n, #w - 1)
end
-- 产生n+1个等号
local eq =string.rep("=", n+1)
-- 生成长字符串的字面表示,在字符串开始的地方加了一个换行符
returnstring.format("[%s[\n%s]%s]", eq, s, eq)
end
12.2.1 保存无环的table
下一个任务是保存table。保存table的方法有几种,选用哪种方法取决于table的结构。简单的table
不仅需要简单的算法,而且更需要完美地输出结果。
第一个算法如下:
function serialize(o)
if type(o) =="number" then
io.write(o)
elseiftype(o) == "string" then
io.write(string.format("%q",o))
elseiftype(o) == "table" then
io.write("{\n")
for k, vin pairs(o) do
io.write("", k, " = ")
serialize(v)
io.write(",\n")
end
io.write("}\n")
else
error("cannotserialize a " .. type(o))
end
end
上面函数假设了一个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\"",
}
12.2.2 保存有环的table
如果要处理任意拓扑结构(带环的table或共享子table)的table,就需要采用另外一种方法。
保存函数要求将待保存的值及其名称一起作为参数传入;此外还必须持有一份所有已保持过的table的
名称记录,以此来检测环并复用其中的table。
代码如下:
function basicSerialize(o)
if type(o) =="number" then
return tostring(o)
else -- 假设 o 是字符串
returnstring.format("%q", o)
end
end
functin save(name, value saved)
saved = savedor {} --初始值
io.write(name,"=")
iftype(value) == "number" or type(value) == "string" then
io.write(basicSerialize(value),"\n")
elseiftype(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("cannotsave a ".. type(value))
end
end
示例——假设有一个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中的共同部分,只需调用save时使用相同的saved
参数。