文章來自:http://mobile.51cto.com/iphone-286562.htm算法
LUA腳本語言之數據文件與持久化是本文要介紹的內容,當咱們處理數據文件的,通常來講,寫文件比讀取文件內容來的容易。由於咱們能夠很好的控制文件的寫操做,而從文件讀取數據經常碰到不可預知的狀況。安全
一個健壯的程序不只應該能夠讀取存有正確格式的數據還應該可以處理壞文件(譯者注:對數據內容和格式進行校驗,對異常狀況可以作出恰當處理)。正由於如此,實現一 個健壯的讀取數據文件的程序是很困難的。網絡
文件格式能夠經過使用Lua中的table構造器來描述,咱們只須要在寫數據的稍微作一些作一點額外的工做,讀取數據將變得容易不少。方法是:將咱們的數據文件內容做爲Lua代碼寫到Lua程序中去。經過使用table構造器,這些存放在Lua代碼中的數據能夠像其餘普通的文件同樣看起來引人注目。iphone
爲了更清楚地描述問題,下面咱們看看例子。若是咱們的數據是預先肯定的格式,好比CSV(逗號分割值),咱們幾乎沒得選擇。可是若是咱們打算建立一個文件爲了未來使用,除了CSV,咱們可使用Lua構造器來咱們表述咱們數據,這種狀況下,咱們將每個數據記錄描述爲一個Lua構造器。將下面的代碼函數
Donald E. Knuth,Literate Programming,CSLI,1992 Jon Bentley,More Programming Pearls,Addison-Wesley,1990 寫成 Entry{"Donald E. Knuth", "Literate Programming", "CSLI", 1992} Entry{"Jon Bentley", "More Programming Pearls", "Addison-Wesley", 1990}
記住Entry{...}與Entry({...})等價,他是一個以表做爲惟一參數的函數調用。因此,前面那段數據在Lua程序中表示如上。若是要讀取這個段數據,咱們只須要運行咱們的Lua代碼。例以下面這段代碼計算數據文件中記錄數:學習
local count = 0 function Entry (b) countcount = count + 1 end dofile("data") print("number of entries: " .. count)
下面這段程序收集一個做者名列表中的名字是否在數據文件中出現,若是在文件中出現則打印出來。(做者名字是Entry的第一個域;因此,若是b是一個entry的值,b[1]則表明做者名)測試
local authors = {} -- a set to collect authors function Entry (b) authors[b[1]] = true end dofile("data") for name in pairs(authors) do print(name) end
注意,在這些程序段中使用事件驅動的方法:Entry函數做爲回調函數,dofile處理數據文件中的每一記錄都回調用它。當數據文件的大小不是太大的狀況下,咱們可使用name-value對來描述數據:spa
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 }
(若是這種格式讓你想起BibTeX,這並不奇怪。Lua中構造器正是根據來自BibTeX的靈感實現的)這種格式咱們稱之爲自描述數據格式,由於每個數據段都根據他的意思簡短的描述爲一種數據格式。相對CSV和其餘緊縮格式,自描述數據格式更容易閱讀和理解,當須要修改的時候能夠容易的手工編輯,並且不須要改動數據文件。例如,若是咱們想增長一個域,只須要對讀取程序稍做修改便可,當指定的域不存在時,也能夠賦予默認值。使用name-value對描述的狀況下,上面收集做者名的代碼能夠改寫爲:code
local authors = {} -- a set to collect authors function Entry (b) authors[b.author] = true end dofile("data") for name in pairs(authors) do print(name) end
如今,記錄域的順序可有可無了,甚至某些記錄即便不存在author這個域,咱們也只須要稍微改動一下代碼便可:orm
function Entry (b) if b.author then authors[b.author] = true end end
Lua不只運行速度快,編譯速度也快。例如,上面這段蒐集做者名的代碼處理一個2MB的數據文件時間不會超過1秒。另外,這不是偶然的,數據描述是Lua的主要應用之一,從Lua發明以來,咱們花了不少心血使他可以更快的編譯和運行大的chunks。
序列化
咱們常常須要序列化一些數據,爲了將數據轉換爲字節流或者字符流,這樣咱們就能夠保存到文件或者經過網絡發送出去。咱們能夠在Lua代碼中描述序列化的數據,在這種方式下,咱們運行讀取程序便可從代碼中構造出保存的值。
一般,咱們使用這樣的方式varname = <exp>來保存一個全局變量的值。varname部分比較容易理解,下面咱們來看看如何寫一個產生值的代碼。對於一個數值來講:
function serialize (o) if type(o) == "number" then io.write(o) else ... end
對於字符串值而言,原始的寫法應該是:
if type(o) == "string" then io.write("'", o, "'")
然而,若是字符串包含特殊字符(好比引號或者換行符),產生的代碼將不是有效的Lua程序。這時候你可能用下面方法解決特殊字符的問題:
if type(o) == "string" then io.write("[[", o, "]]")
千萬不要這樣作!雙引號是針對手寫的字符串的而不是針對自動產生的字符串。若是有人惡意的引導你的程序去使用" ]]..os.execute('rm *')..[[ "這樣的方式去保存某些東西(好比它可能提供字符串做爲地址)你最終的chunk將是這個樣子:
varname = [[ ]]..os.execute('rm *')..[[ ]]
若是你load這個數據,運行結果可想而知的。爲了以安全的方式引用任意的字符串,string標準庫提供了格式化函數專門提供"%q"選項。它可使用雙引號表示字符串而且能夠正確的處理包含引號和換行等特殊字符的字符串。這樣一來,咱們的序列化函數能夠寫爲:
function serialize (o) if type(o) == "number" then io.write(o) elseif type(o) == "string" then io.write(string.format("%q", o)) else ... end
保存不帶循環的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") 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
儘管代碼很簡單,但很好地解決了問題。只要表結構是一個樹型結構(也就是說,沒有共享的子表而且沒有循環),上面代碼甚至能夠處理嵌套表(表中表)。對於所進不整齊的表咱們能夠少做改進使結果更美觀,這能夠做爲一個練習嘗試一下。(提示:增長一個參數表示縮進的字符串,來進行序列化)。
前面的函數假定表中出現的全部關鍵字都是合法的標示符。若是表中有不符合Lua語法的數字關鍵字或者字符串關鍵字,上面的代碼將碰到麻煩。一個簡單的解決這個難題的方法是將:
io.write(" ", k, " = ") 改成 io.write(" [") serialize(k) io.write("] = ")
這樣一來,咱們改善了咱們的函數的健壯性,比較一下兩次的結果:
result of serialize{a=12, b='Lua', key='another "one"'}
第一個版本
{ a = 12, b = "Lua", key = "another \"one\"", }
第二個版本
{ ["a"] = 12, ["b"] = "Lua", ["key"] = "another \"one\"", }
咱們能夠經過測試每一種狀況,看是否須要方括號,另外,咱們將這個問題留做一個練習給你們。
保存帶有循環的table
針對普通拓撲概念上的帶有循環表和共享子表的table,咱們須要另一種不一樣的方法來處理。構造器不能很好地解決這種狀況,咱們不使用。爲了表示循環咱們須要將表名記錄下來,下面咱們的函數有兩個參數:table和對應的名字。另外,咱們還必須記錄已經保存過的table以防止因爲循環而被重複保存。咱們使用一個額外的table來記錄保存過的表的軌跡,這個表的下表索引爲table,而值爲對應的表名。
咱們作一個限制:要保存的table只有一個字符串或者數字關鍵字。下面的這個函數序列化基本類型並返回結果。
function basicSerialize (o) if type(o) == "number" then return tostring(o) else -- assume it is a string return string.format("%q", o) end end
關鍵內容在接下來的這個函數,saved這個參數是上面提到的記錄已經保存的表的蹤影的table。
function save (name, value, saved) savedsaved = saved or {} -- initial value 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 already saved? -- use its previous name io.write(saved[value], "\n") else saved[value] = name -- save name for next time io.write("{}\n") -- create a new table for k,v in pairs(value) do -- save its fields local fieldname = string.format("%s[%s]", name, basicSerialize(k)) save(fieldname, v, saved) end end else error("cannot save a " .. type(value)) end end
舉個例子:
咱們將要保存的table爲:
a = {x=1, y=2; {3,4,5}} a[2] = a -- cycle aa.z = a[1] -- shared sub-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的saved參數調用save函數,例如咱們建立下面兩個表:
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表來調用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]
上面這種方法是Lua中經常使用的方法,固然也有其餘一些方法能夠解決問題。好比,咱們能夠不使用全局變量名來保存,即便用封包,用chunk構造一個local值而後返回之;經過構造一張表,每張表名與其對應的函數對應起來等。Lua給予你權力,由你決定如何實現。
小結:詳解LUA腳本語言之數據文件與持久化的內容介紹完了,但願經過本文的學習能對你有所幫助!