#
1. 基礎字符串函數: 字符串庫中有一些函數非常簡單,如: 1). string.len(s) 返回字符串s的長度; 2). string.rep(s,n) 返回字符串s重復n次的結果; 3). string.lower(s) 返回s的副本,其中所有的大寫都被轉換為了小寫形式,其他字符不變; 4). string.upper(s) 和lower相反,將小寫轉換為大寫; 5). string.sub(s,i,j) 提取字符串s的第i個到第j個字符。Lua中,第一個字符的索引值為1,最后一個為-1,以此類推,如: print(string.sub("[hello world]",2,-2)) --輸出hello world 6). string.format(s,...) 返回格式化后的字符串,其格式化規則等同于C語言中printf函數,如: print(string.format("pi = %.4f",math.pi)) --輸出pi = 3.1416 7). string.char(...) 參數為0到多個整數,并將每個整數轉換為對應的字符。然后返回一個由這些字符連接而成的字符串,如: print(string.char(97,98,99)) --輸出abc 8). string.byte(s,i) 返回字符串s的第i個字符的Ascii值,如果沒有第二個參數,缺省返回第一個字符的Ascii值。 print(string.byte("abc")) --輸出97 print(string.byte("abc",-1)) --輸出99 由于字符串類型的變量都是不可變類型的變量,因此在所有和string相關的函數中,都無法改變參數中的字符串值,而是生成一個新值返回。
2. 模式匹配函數: Lua的字符串庫提供了一組強大的模式匹配函數,如find、match、gsub和gmatch。 1). string.find函數: 在目標字符串中搜索一個模式,如果找到,則返回匹配的起始索引和結束索引,否則返回nil。如: 1 s = "hello world" 2 i, j = string.find(s,"hello") 3 print(i, j) --輸出1 5 4 i, j = string.find(s,"l") 5 print(i, j) --輸出3 3 6 print(string.find(s,"lll")) --輸出nil string.find函數還有一個可選參數,它是一個索引,用于告訴函數從目標字符串的哪個位置開始搜索。主要用于搜索目標字符串中所有匹配的子字符串,且每次搜索都從上一次找到的位置開始。如: 1 local t = {} 2 local i = 0 3 while true do 4 i = string.find(s,"\n",i+1) 5 if i == nil then 6 break 7 end 8 t[#t + 1] = i 9 end 2). string.match函數: 該函數返回目標字符串中和模式字符串匹配的部分。如: 1 date = "Today is 2012-01-01" 2 d = string.match(date,"%d+\-%d+\-%d+") 3 print(d) --輸出2012-01-01 3). string.gsub函數: 該函數有3個參數,目標字符串、模式和替換字符串。基本用法是將目標字符串中所有出現模式的地方替換為替換字符串。如: print(string.gsub("Lua is cute","cute","great")) --輸出Lua is great 該函數還有可選的第4個參數,即實際替換的次數。 print(string.gsub("all lii","l","x",1)) --輸出axl lii print(string.gsub("all lii","l","x",2)) --輸出axx lii 函數string.gsub還有另一個結果,即實際替換的次數。 count = select(2, string.gsub(str," "," ")) --輸出str中空格的數量
4). string.gmatch函數: 返回一個函數,通過這個返回的函數可以遍歷到一個字符串中所有出現指定模式的地方。如: 1 words = {} 2 s = "hello world" 3 for w in string.gmatch(s,"%a+") do 4 print(w) 5 words[#words + 1] = w 6 end 7 --輸出結果為: 8 --hello 9 --world 3. 模式: 下面的列表給出了Lua目前支持的模式元字符; 模式元字符 | 描述 | . | 所有字符 | %a | 字母 | %c | 控制字符 | %d | 數字 | %l | 小寫字母 | %p | 標點符號 | %s | 空白字符 | %u | 大寫字母 | %w | 字母和數字字符 | %x | 十六進制數字 | %z | 內部表示為0的字符 |
這些元字符的大寫形式表示它們的補集,如%A,表示所有非字母字符。 print(string.gsub("hello, up-down!","%S",".")) --輸出hello..up.down. 4 上例中的4表示替換的次數。 除了上述元字符之外,Lua還提供了另外幾個關鍵字符。如:( ) . % + - * ? [ ] ^ $ 其中%表示轉義字符,如%.表示點(.),%%表示百分號(%)。 方括號[]表示將不同的字符分類,即可創建出屬于自己的字符分類,如[%w_]表示匹配字符、數字和下劃線。 橫線(-)表示連接一個范圍,比如[0-9A-Z] 如果^字符在方括號內,如[^\n],表示除\n之外的所有字符,即表示方括號中的分類的補集。如果^不在方括號內,則表示以后面的字符開頭,$和它正好相反,表示以前面的字符結束。如:^Hello%d$,匹配的字符串可能為Hello1、Hello2等。 在Lua中還提供了4種用來修飾模式中的重復部分,如:+(重復1次或多次)、*(重復0次或多次)、-(重復0次或多次)和?(出現0或1次)。如: print(string.gsub("one, and two; and three","%a+","word")) --輸出word, word word; word word print(string.match("the number 1298 is even","%d+")) --輸出1298 星號(*)和橫線(-)的主要差別是,星號總是試圖匹配更多的字符,而橫線則總是試圖匹配最少的字符。
4. 捕獲(capture): 捕獲功能可根據一個模式從目標字符串中抽出匹配于該模式的內容。在指定捕獲是,應將模式中需要捕獲的部分寫到一對圓括號內。對于具有捕獲的模式,函數string.match會將所有捕獲到的值作為單獨的結果返回。即它會將目標字符串切成多個捕獲到的部分。如: 1 pair = "name = Anna" 2 key,value = string.match(pair,"(%a+)%s*=%s*(%a+)") 3 print(key,value) --輸出name anna 4 5 date = "Today is 2012-01-02" 6 y,m,d = string.match(date,"(%d+)\-(%d+)\-(%d+)") 7 print(y,m,d) --輸出2012 01 02 還可以對模式本身使用捕獲。即%1表示第一個捕獲,以此類推,%0表示整個匹配,如: 1 print(string.gsub("hello Lua","(.)(.)","%2%1")) --將相鄰的兩個字符對調,輸出為ehll ouLa 2 print(string.gsub("hello Lua!","%a","%0-%0")) --輸出為h-he-el-ll-lo-o L-Lu-ua-a! 5. 替換: string.gsub函數的第三個參數不僅可以是字符串,也可以是函數或table,如果是函數,string.gsub會在每次找到匹配時調用該函數,調用時的參數就是捕獲到的內容,而該函數的返回值則作為要替換的字符串。當用一個table來調用時,string.gsub會用每次捕獲到的內容作為key,在table中查找,并將對應的value作為要替換的字符串。如果table中不包含這個key,那么string.gsub不改變這個匹配。如:
1 function expand(s) 2 return (string.gsub(s,"$(%w+)",_G)) 3 end 4 5 name = "Lua"; status = "great" 6 print(expand("$name is $status, isn't it?")) --輸出 Lua is great, isn't it? 7 print(expand("$othername is $status, isn't it?")) --輸出 $othername is great, isn't it? 8 9 function expand2(s) 10 return (string.gsub(s,"$(%w+)",function(n) return tostring(_G[n]) end)) 11 end 12 13 print(expand2("print = $print; a = $a")) --輸出 print = function: 002B77C0; a = nil
Lua采用了基于垃圾收集的內存管理機制,因此對于程序員來說,在很多時候內存問題都將不再困擾他們。然而任何垃圾收集器都不是萬能的,在有些特殊情況下,垃圾收集器是無法準確的判斷是否應該將當前對象清理。這樣就極有可能導致很多垃圾對象無法被釋放。為了解決這一問題,就需要Lua的開發者予以一定程度上的配合。比如,當某個table對象被存放在容器中,而容器的外部不再有任何變量引用該對象,對于這樣的對象,Lua的垃圾收集器是不會清理的,因為容器對象仍然引用著他。如果此時針對該容器的應用僅限于查找,而不是遍歷的話,那么該對象將永遠不會被用到。事實上,對于這樣的對象我們是希望Lua的垃圾收集器可以將其清理掉的。見如下代碼: 1 a = {} 2 key = {} 3 a[key] = 1 4 key = {} 5 a[key] = 2 6 collectgarbage() 7 for k,v in pairs(a) do 8 print(v) 9 end 10 --輸出1和2 在執行垃圾收集之后,table a中的兩個key都無法被清理,但是對value等于1的key而言,如果后面的邏輯不會遍歷table a的話,那么我們就可以認為該對象內存泄露了。在Lua中提供了一種被稱為弱引用table的機制,可以提示垃圾收集器,如果某個對象,如上面代碼中的第一個table key,只是被弱引用table引用,那么在執行垃圾收集時可以將其清理。 Lua中的弱引用表提供了3中弱引用模式,即key是弱引用、value是弱引用,以及key和value均是弱引用。不論是哪種類型的弱引用table,只要有一個key或value被回收,那么它們所在的整個條目都會從table中刪除。 一個table的弱引用類型是通過其元表的__mode字段來決定的。如果該值為包含字符"k",那么table就是key弱引用,如果包含"v",則是value若引用,如果兩個字符均存在,就是key/value弱引用。見如下代碼: 1 a = {} 2 b = {__mode = "k"} 3 setmetatable(a,b) 4 key = {} 5 a[key] = 1 6 key = {} 7 a[key] = 2 8 collectgarbage() 9 for k,v in pairs(a) do 10 print(v) 11 end 12 --僅僅輸出2 在上面的代碼示例中,第一個key在被存放到table a之后,就被第二個key的定義所覆蓋,因此它的唯一引用來自key弱引用表。事實上,這種機制在Java中也同樣存在,Java在1.5之后的版本中也提供了一組弱引用容器,其語義和Lua的弱引用table相似。 最后需要說明的是,Lua中的弱引用表只是作用于table類型的變量,對于其他類型的變量,如數值和字符串等,弱引用表并不起任何作用。
1. 備忘錄(memoize)函數: 用“空間換時間”是一種通用的程序運行效率優化手段,比如:對于一個普通的Server,它接受到的請求中包含Lua代碼,每當其收到請求后都會調用Lua的loadstring函數來動態解析請求中的Lua代碼,如果這種操作過于頻率,就會導致Server的執行效率下降。要解決該問題,我們可以將每次解析的結果緩存到一個table中,下次如果接收到相同的Lua代碼,就不需要調用loadstirng來動態解析了,而是直接從table中獲取解析后的函數直接執行即可。這樣在有大量重復Lua代碼的情況下,可以極大的提高Server的執行效率。反之,如果有相當一部分的Lua代碼只是出現一次,那么再使用這種機制,就將會導致大量的內存資源被占用而得不到有效的釋放。在這種情況下,如果使用弱引用表,不僅可以在一定程度上提升程序的運行效率,內存資源也會得到有效的釋放。見如下代碼: 1 local results = {} 2 setmetatable(results,{__mode = "v"}) --results表中的key是字符串形式的Lua代碼 3 function mem_loadstring(s) 4 local res = results[s] 5 if res == nil then 6 res = assert(loadstring(s)) 7 results[s] = res 8 end 9 return res 10 end
Lua中的table就是一種對象,但是如果直接使用仍然會存在大量的問題,見如下代碼: 1 Account = {balance = 0} 2 function Account.withdraw(v) 3 Account.balance = Account.balance - v 4 end 5 --下面是測試調用函數 6 Account.withdraw(100.00) 在上面的withdraw函數內部依賴了全局變量Account,一旦該變量發生改變,將會導致withdraw不再能正常的工作,如: 1 a = Account; Account = nil 2 a.withdraw(100.00) --將會導致訪問空nil的錯誤。 這種行為明顯的違反了面向對象封裝性和實例獨立性。要解決這一問題,我們需要給withdraw函數在添加一個參數self,他等價于Java/C++中的this,見如下修改: 1 function Account.withdraw(self,v) 2 self.balance = self.balance - v 3 end 4 --下面是基于修改后代碼的調用: 5 a1 = Account; Account = nil 6 a1.withdraw(a1,100.00) --正常工作。 針對上述問題,Lua提供了一種更為便利的語法,即將點(.)替換為冒號(:),這樣可以在定義和調用時均隱藏self參數,如: 1 function Account:withdraw(v) 2 self.balance = self.balance - v 3 end 4 --調用代碼可改為: 5 a:withdraw(100.00) 1. 類: Lua在語言上并沒有提供面向對象的支持,因此想實現該功能,我們只能通過table來模擬,見如下代碼及關鍵性注釋:
 1 --[[ 2 在這段代碼中,我們可以將Account視為class的聲明,如Java中的: 3 public class Account 4 { 5 public float balance = 0; 6 public Account(Account o); 7 public void deposite(float f); 8 } 9 --]] 10 --這里balance是一個公有的成員變量。 11 Account = {balance = 0} 12 13 --new可以視為構造函數 14 function Account:new(o) 15 o = o or {} --如果參數中沒有提供table,則創建一個空的。 16 --將新對象實例的metatable指向Account表(類),這樣就可以將其視為模板了。 17 setmetatable(o,self) 18 --在將Account的__index字段指向自己,以便新對象在訪問Account的函數和字段時,可被直接重定向。 19 self.__index = self 20 --最后返回構造后的對象實例 21 return o 22 end 23 24 --deposite被視為Account類的公有成員函數 25 function Account:deposit(v) 26 --這里的self表示對象實例本身 27 self.balance = self.balance + v 28 end 29 30 --下面的代碼創建兩個Account的對象實例 31 32 --通過Account的new方法構造基于該類的示例對象。 33 a = Account:new() 34 --[[ 35 這里需要具體解釋一下,此時由于table a中并沒有deposite字段,因此需要重定向到Account, 36 同時調用Account的deposite方法。在Account.deposite方法中,由于self(a對象)并沒有balance 37 字段,因此在執行self.balance + v時,也需要重定向訪問Account中的balance字段,其缺省值為0。 38 在得到計算結果后,再將該結果直接賦值給a.balance。此后a對象就擁有了自己的balance字段和值。 39 下次再調用該方法,balance字段的值將完全來自于a對象,而無需在重定向到Account了。 40 --]] 41 a:deposit(100.00) 42 print(a.balance) --輸出100 43 44 b = Account:new() 45 b:deposit(200.00) 46 print(b.balance) --輸出200  2. 繼承: 繼承也是面向對象中一個非常重要的概念,在Lua中我們仍然可以像模擬類那樣來進一步實現面向對象中的繼承機制,見如下代碼及關鍵性注釋:
 1 --需要說明的是,這段代碼僅提供和繼承相關的注釋,和類相關的注釋在上面的代碼中已經給出。 2 Account = {balance = 0} 3 4 function Account:new(o) 5 o = o or {} 6 setmetatable(o,self) 7 self.__index = self 8 return o 9 end 10 11 function Account:deposit(v) 12 self.balance = self.balance + v 13 end 14 15 function Account:withdraw(v) 16 if v > self.balance then 17 error("Insufficient funds") 18 end 19 self.balance = self.balance - v 20 end 21 22 --下面將派生出一個Account的子類,以使客戶可以實現透支的功能。 23 SpecialAccount = Account:new() --此時SpecialAccount仍然為Account的一個對象實例 24 25 --派生類SpecialAccount擴展出的方法。 26 --下面這些SpecialAccount中的方法代碼(getLimit/withdraw),一定要位于SpecialAccount被Account構造之后。 27 function SpecialAccount:getLimit() 28 --此時的self將為對象實例。 29 return self.limit or 0 30 end 31 32 --SpecialAccount將為Account的子類,下面的方法withdraw可以視為SpecialAccount 33 --重寫的Account中的withdraw方法,以實現自定義的功能。 34 function SpecialAccount:withdraw(v) 35 --此時的self將為對象實例。 36 if v - self.balance >= self:getLimit() then 37 error("Insufficient funds") 38 end 39 self.balance = self.balance - v 40 end 41 42 --在執行下面的new方法時,table s的元表已經是SpecialAccount了,而不再是Account。 43 s = SpecialAccount:new{limit = 1000.00} 44 --在調用下面的deposit方法時,由于table s和SpecialAccount均未提供該方法,因此訪問的仍然是 45 --Account的deposit方法。 46 s:deposit(100) 47 48 49 --此時的withdraw方法將不再是Account中的withdraw方法,而是SpecialAccount中的該方法。 50 --這是因為Lua先在SpecialAccount(即s的元表)中找到了該方法。 51 s:withdraw(200.00) 52 print(s.balance) --輸出-100  3. 私密性: 私密性對于面向對象語言來說是不可或缺的,否則將直接破壞對象的封裝性。Lua作為一種面向過程的腳本語言,更是沒有提供這樣的功能,然而和模擬支持類與繼承一樣,我們仍然可以在Lua中通過特殊的編程技巧來實現它,這里我們應用的是Lua中的閉包函數。該實現方式和前面兩個示例中基于元表的方式有著很大的區別,見如下代碼示例和關鍵性注釋:
1 --這里我們需要一個閉包函數作為類的創建工廠 2 function newAccount(initialBalance) 3 --這里的self僅僅是一個普通的局部變量,其含義完全不同于前面示例中的self。 4 --這里之所以使用self作為局部變量名,也是為了方便今后的移植。比如,以后 5 --如果改為上面的實現方式,這里應用了self就可以降低修改的工作量了。 6 local self = {balance = initialBalance} --這里我們可以將self視為私有成員變量 7 local withdraw = function(v) self.balance = self.balance - v end 8 local deposit = function(v) self.balance = self.balance + v end 9 local getBalance = function() return self.balance end 10 --返回對象中包含的字段僅僅為公有方法。事實上,我們通過該種方式,不僅可以實現 11 --成員變量的私有性,也可以實現方法的私有性,如: 12 --local privateFunction = function() --do something end 13 --只要我們不在輸出對象中包含該方法的字段即可。 14 return {withdraw = withdraw, deposit = deposit, getBalance = getBalance} 15 end 16 17 --和前面兩個示例不同的是,在調用對象方法時,不再需要self變量,因此我們可以直接使用點(.), 18 --而不再需要使用冒號(:)操作符了。 19 accl = newAccount(100.00) 20 --在函數newAccount返回之后,該函數內的“非局部變量”表self就不再能被外部訪問了,只能通過 21 --該函數返回的對象的方法來操作它們。 22 accl.withdraw(40.00) 23 print(acc1.getBalance()) 事實上,上面的代碼只是給出一個簡單的示例,在實際應用中,我們可以將更多的私有變量存放于上例的局部self表中。
從Lua 5.1開始,我們可以使用require和module函數來獲取和創建Lua中的模塊。從使用者的角度來看,一個模塊就是一個程序庫,可以通過require來加載,之后便得到一個類型為table的全局變量。此時的table就像名字空間一樣,可以訪問其中的函數和常量,如: 1 require "mod" 2 mod.foo() 3 local m2 = require "mod2" 4 local f = mod2.foo 5 f() 1. require函數: require函數的調用形式為require "模塊名"。該調用會返回一個由模塊函數組成的table,并且還會定義一個包含該table的全局變量。在使用Lua中的標準庫時可以不用顯示的調用require,因為Lua已經預先加載了他們。 require函數在搜素加載模塊時,有一套自定義的模式,如: ?;?.lua;c:/windows/?;/usr/local/lua/?/?.lua 在上面的模式中,只有問號(?)和分號(;)是模式字符,分別表示require函數的參數(模塊名)和模式間的分隔符。如:調用require "sql",將會打開以下的文件: sql sql.lua c:/windows/sql /usr/local/lua/sql/sql.lua Lua將require搜索的模式字符串放在變量package.path中。當Lua啟動后,便以環境變量LUA_PATH的值來初始化這個變量。如果沒有找到該環境變量,則使用一個編譯時定義的默認路徑來初始化。如果require無法找到與模塊名相符的Lua文件,就會找C程序庫。C程序庫的搜索模式存放在變量package.cpath中。而這個變量則是通過環境變量LUA_CPATH來初始化的。 2. 編寫模塊的基本方法: 見如下代碼和關鍵性注釋: 1 --將模塊名設置為require的參數,這樣今后重命名模塊時,只需重命名文件名即可。 2 local modname = ... 3 local M = {} 4 _G[modname] = M 5 6 M.i = {r = 0, i = 1} --定義一個模塊內的常量。 7 function M.new(r,i) return {r = r, i = i} end 8 function M.add(c1,c2) 9 return M.new(c1.r + c2.r,c1.i + c2.i) 10 end 11 12 function M.sub(c1,c2) 13 return M.new(c1.r - c2.r,c1.i - c2.i) 14 end 15 --返回和模塊對應的table。 16 return M 3. 使用環境: 仔細閱讀上例中的代碼,我們可以發現一些細節上問題。比如模塊內函數之間的調用仍然要保留模塊名的限定符,如果是私有變量還需要加local關鍵字,同時不能加模塊名限定符。如果需要將私有改為公有,或者反之,都需要一定的修改。那又該如何規避這些問題呢?我們可以通過Lua的函數“全局環境”來有效的解決這些問題。見如下修改的代碼和關鍵性注釋: 1 --模塊設置和初始化。這一點和上例一致。 2 local modname = ... 3 local M = {} 4 _G[modname] = M 5 6 --聲明這個模塊將會用到的全局函數,因為在setfenv之后將無法再訪問他們, 7 --因此需要在設置之前先用本地變量獲取。 8 local sqrt = mat.sqrt 9 local io = io 10 11 --在這句話之后就不再需要外部訪問了。 12 setfenv(1,M) 13 14 --后面的函數和常量定義都無需模塊限定符了。 15 i = {r = 0, i = 1} 16 function new(r,i) return {r = r, i = i} end 17 function add(c1,c2) 18 return new(c1.r + c2.r,c1.i + c2.i) 19 end 20 21 function sub(c1,c2) 22 return new(c1.r - c2.r,c1.i - c2.i) 23 end 24 --返回和模塊對應的table。 25 return M 4. module函數: 在Lua 5.1中,我們可以用module(...)函數來代替以下代碼,如:
1 local modname = ... 2 local M = {} 3 _G[modname] = M 4 package.loaded[modname] = M 5 --[[ 6 和普通Lua程序塊一樣聲明外部函數。 7 --]] 8 setfenv(1,M) 由于在默認情況下,module不提供外部訪問,必須在調用它之前,為需要訪問的外部函數或模塊聲明適當的局部變量。然后Lua提供了一種更為方便的實現方式,即在調用module函數時,多傳入一個package.seeall的參數,如: module(...,package.seeall)
Lua將其所有的全局變量保存在一個常規的table中,這個table被稱為“環境”。它被保存在全局變量_G中。 1. 全局變量聲明: Lua中的全局變量不需要聲明就可以使用。盡管很方便,但是一旦出現筆誤就會造成難以發現的錯誤。我們可以通過給_G表加元表的方式來保護全局變量的讀取和設置,這樣就能降低這種筆誤問題的發生幾率了。見如下示例代碼: 1 --該table用于存儲所有已經聲明過的全局變量名 2 local declaredNames = {} 3 local mt = { 4 __newindex = function(table,name,value) 5 --先檢查新的名字是否已經聲明過,如果存在,這直接通過rawset函數設置即可。 6 if not declaredNames[name] then 7 --再檢查本次操作是否是在主程序或者C代碼中完成的,如果是,就繼續設置,否則報錯。 8 local w = debug.getinfo(2,"S").what 9 if w ~= "main" and w ~= "C" then 10 error("attempt to write to undeclared variable " .. name) 11 end 12 --在實際設置之前,更新一下declaredNames表,下次再設置時就無需檢查了。 13 declaredNames[name] = true 14 end 15 print("Setting " .. name .. " to " .. value) 16 rawset(table,name,value) 17 end, 18 19 __index = function(_,name) 20 if not declaredNames[name] then 21 error("attempt to read undeclared variable " .. name) 22 else 23 return rawget(_,name) 24 end 25 end 26 } 27 setmetatable(_G,mt) 28 29 a = 11 30 local kk = aa 31 32 --輸出結果為: 33 --[[ 34 Setting a to 11 35 lua: d:/test.lua:21: attempt to read undeclared variable aa 36 stack traceback: 37 [C]: in function 'error' 38 d:/test.lua:21: in function <d:/test.lua:19> 39 d:/test.lua:30: in main chunk 40 [C]: ? 41 --]] 2. 非全局的環境: 全局環境存在一個剛性的問題,即它的修改將影響到程序的所有部分。Lua 5為此做了一些改進,新的特征可以支持每個函數擁有自己獨立的全局環境,而由該函數創建的closure函數將繼承該函數的全局變量表。這里我們可以通過setfenv函數來改變一個函數的環境,該函數接受兩個參數,一個是函數名,另一個是新的環境table。第一個參數除了函數名本身,還可以指定為一個數字,以表示當前函數調用棧中的層數。數字1表示當前函數,2表示它的調用函數,以此類推。見如下代碼:
1 a = 1 2 setfenv(1,{}) 3 print(a) 4 5 --輸出結果為: 6 --[[ 7 lua: d:/test.lua:3: attempt to call global 'print' (a nil value) 8 stack traceback: 9 d:/test.lua:3: in main chunk 10 [C]: ? 11 --]] 為什么得到這樣的結果呢?因為print和變量a一樣,都是全局表中的字段,而新的全局表是空的,所以print調用將會報錯。 為了應對這一副作用,我們可以讓原有的全局表_G作為新全局表的內部表,在訪問已有全局變量時,可以直接轉到_G中的字段,而對于新的全局字段,則保留在新的全局表中。這樣即便是函數中的誤修改,也不會影響到其他用到全局變量(_G)的地方。見如下代碼: 1 a = 1 2 local newgt = {} --新環境表 3 setmetatable(newgt,{__index = _G}) 4 setfenv(1,newgt) 5 print(a) --輸出1 6 7 a = 10 8 print(a) --輸出10 9 print(_G.a) --輸出1 10 _G.a = 20 11 print(a) --輸出10 最后給出的示例是函數環境變量的繼承性。見如下代碼: 1 function factory() 2 return function() return a end 3 end 4 a = 3 5 f1 = factory() 6 f2 = factory() 7 print(f1()) --輸出3 8 print(f2()) --輸出3 9 10 setfenv(f1,{a = 10}) 11 print(f1()) --輸出10 12 print(f2()) --輸出3
摘要: Lua中提供的元表是用于幫助Lua數據變量完成某些非預定義功能的個性化行為,如兩個table的相加。假設a和b都是table,通過元表可以定義如何計算表達式a+b。當Lua試圖將兩個table相加時,它會先檢查兩者之一是否有元表,然后檢查該元表中是否存在__add字段,如果有,就調用該字段對應的值。這個值就是所謂的“元方法”,這個函數用于計算table的和。&n... 閱讀全文
1. 數據文件: 我們可以利用Lua中table的構造式來定義一種文件格式,即文件中的數據是table構造并初始化的代碼,這種方式對于Lua程序而言是非常方便和清晰的,如: Entry { "Stephen Liu", "Male", "Programmer", "BS" } Entry { "Jerry Tian", "Male", "Programmer", "BS" } 需要注意的是,Entry{<code>}等價于Entry({<code>}),對于上面的數據條目,如果我們能夠定義一個合適的Entry函數,就可以讓這些數據成為我們Lua代碼的一部分了。見如下代碼及其注釋: 1 local count = 0 2 --這里預先定義了Entry函數,以便在執行dofile中的數據代碼時,可以找到匹配的該函數。 3 function Entry() count = count + 1 end 4 dofile("d:/lua_data.conf") 5 print("number of entries: " .. count) 6 7 --輸出結果為: 8 --number of entries: 2 相比于上面數據文件的格式,我們還可以定義一種更為清晰的“自描述的數據”格式,其中每項數據都伴隨一個表示其含義的簡短描述。采用這樣的格式,即便今后數據項發生了變化,我們仍然可以在改動極小的情況下保持向后的兼容性。見如下數據格式和相關的代碼: Entry { name = "Stephen Liu", gender = "Male", job = "Programmer", education = "BS" } Entry { name = "Jerry Tian", gender = "Male", job = "Programmer", education = "BS" } 1 local personInfo = {} 2 function Entry(b) 3 --這里將table對象b的name字段值作為personInfo的key信息。 4 if b.name then 5 personInfo[b.name] = true 6 end 7 end 8 9 dofile("d:/lua_data.conf") 10 for name in pairs(personInfo) do 11 print(name) 12 end 13 14 --輸出結果為: 15 --Jerry Tian 16 --Stephen Liu 可以看出這些代碼片段都采用了事件驅動的做法。Entry函數作為一個回調函數,在執行dofile時為數據文件中的每個條目所調用。 Lua不僅運行速度快,而且編譯速度也快。這主要是因為Lua在設計之初就將數據描述作為Lua的主要應用之一所致。 2. 序列化: 相信有Java或C#開發經驗的人對于這一術語并不陌生。就是將數據對象轉換為字節流后在通過IO輸出到文件或網絡,讀取的時候再將這些數據重新構造為與原始對象具有相同值的新對象。或者我們也可以將一段可執行的Lua代碼作為序列化后的數據格式。比如:varname = <expr>,這里的<expr>表示計算變量varname的表達式。下面的示例代碼用于序列化無環的table: 1 function serialize(o) 2 if type(o) == "number" then 3 io.write(o) 4 elseif type(o) == "string" then 5 --string.format函數的"%q"參數可以轉義字符串中的元字符。 6 io.write(string.format("%q",o)) 7 elseif type(o) == "table" then 8 io.write("{\n") 9 --迭代table中的各個元素,同時遞歸的寫出各個字段的value。 10 --由此可以看出,這個簡單例子可以支持嵌套的table。 11 for k,v in pairs(o) do 12 --這樣做是為了防止k中包含非法的Lua標識符。 13 io.write(" ["); serialize(k); io.write("] = ") 14 serialize(v) 15 io.write(",\n") 16 end 17 io.write("}\n") 18 else 19 error("cannot serialize a " .. type(o)) 20 end 21 end
Lua中的table不是一種簡單的數據結構,它可以作為其它數據結構的基礎。如數組、記錄、線性表、隊列和集合等,在Lua中都可以通過table來表示。 1. 數組: 使用整數來索引table即可在Lua中實現數組。因此,Lua中的數組沒有固定的大小,如: 1 a = {} 2 for i = 1, 1000 do 3 a[i] = 0 4 end 5 print("The length of array 'a' is " .. #a) 6 --The length of array 'a' is 1000 在Lua中,可以讓任何數作為數組的起始索引,但通常而言,都會使用1作為其起始索引值。而且很多Lua的內置功能和函數都依賴這一特征,因此在沒有充分理由的前提下,盡量保證這一規則。下面的方法是通過table的構造器來創建并初始化一個數組的,如: squares = {1, 4, 9, 16, 25}
2. 二維數組: 在Lua中我們可以通過兩種方式來利用table構造多維數組。其中,第一種方式通過“數組的數組”的方式來實現多維數組的,即在一維數組上的每個元素也同樣為table對象,如: 1 mt = {} 2 for i = 1, N do 3 mt[i] = {} 4 for j = 1, M do 5 mt[i][j] = i * j 6 end 7 end 第二種方式是將二維數組的索引展開,并以固定的常量作為第二維度的步長,如: 1 mt = {} 2 for i = 1, N do 3 for j = 1, M do 4 mt[(i - 1) * M + j] = i * j 5 end 6 end 3. 鏈表: 由于table是動態的實體,所以在Lua中實現鏈表是很方便的。其中,每個結點均以table來表示,一個“鏈接”只是結點中的一個字段,該字段包含對其它table的引用,如:
1 list = nil 2 for i = 1, 10 do 3 list = { next = list, value = i} 4 end 5 6 local l = list 7 while l do 8 print(l.value) 9 l = l.next 10 end 4. 隊列與雙向隊列: 在Lua中實現隊列的簡單方法是使用table庫函數insert和remove。但是由于這種方法會導致后續元素的移動,因此當隊列的數據量較大時,不建議使用該方法。下面的代碼是一種更高效的實現方式,如:
1 List = {} 2 3 function List.new() 4 return {first = 0, last = -1} 5 end 6 7 function List.pushFront(list, value) 8 local first = list.first - 1 9 list.first = first 10 list[first] = value 11 end 12 13 function List.pushBack(list, value) 14 local last = list.last + 1 15 list.last = last 16 list[last] = value 17 end 18 19 function List.popFront(list) 20 local first = list.first 21 if first > list.last then 22 error("List is empty") 23 end 24 local value = list[first] 25 list[first] = nil 26 list.first = first + 1 27 return value 28 end 29 30 function List.popBack(list) 31 local last = list.last 32 if list.first > last then 33 error("List is empty") 34 end 35 local value = list[last] 36 list[last] = nil 37 list.last = last - 1 38 return value 39 end 5. 集合和包(Bag): 在Lua中用table實現集合是非常簡單的,見如下代碼: reserved = { ["while"] = true, ["end"] = true, ["function"] = true, } if not reserved["while"] then --do something end 在Lua中我們可以將包(Bag)看成MultiSet,與普通集合不同的是該容器中允許key相同的元素在容器中多次出現。下面的代碼通過為table中的元素添加計數器的方式來模擬實現該數據結構,如:
1 function insert(bag, element) 2 bag[element] = (bag[element] or 0) + 1 3 end 4 5 function remove(bag, element) 6 local count = bag[element] 7 bag[element] = (count and count > 1) and count - 1 or nil 8 end 6. StringBuilder: 如果想在Lua中將多個字符串連接成為一個大字符串的話,可以通過如下方式實現,如:
1 local buff = "" 2 for line in io.lines() do 3 buff = buff .. line .. "\n" 4 end 上面的代碼確實可以正常的完成工作,然而當行數較多時,這種方法將會導致大量的內存重新分配和內存間的數據拷貝,由此而帶來的性能開銷也是相當可觀的。事實上,在很多編程語言中String都是不可變對象,如Java,因此如果通過該方式多次連接較大字符串時,均會導致同樣的性能問題。為了解決該問題,Java中提供了StringBuilder類,而Lua中則可以利用table的concat方法來解決這一問題,見如下代碼: 1 local t = {} 2 for line in io.lines() do 3 t[#t + 1] = line .. "\n" 4 end 5 local s = table.concat(t) 6 7 --concat方法可以接受兩個參數,因此上面的方式還可以改為: 8 local t = {} 9 for line in io.lines() do 10 t[#t + 1] = line 11 end 12 local s = table.concat(t,"\n")
1. 編譯: Lua中提供了dofile函數,它是一種內置的操作,用于運行Lua代碼塊。但實際上dofile只是一個輔助函數,loadfile才是真正的核心函數。相比于dofile,loadfile只是從指定的文件中加載Lua代碼塊,然后編譯這段代碼塊,如果有編譯錯誤,就返回nil,同時給出錯誤信息,但是在編譯成功后并不真正的執行這段代碼塊。因此,我們可以將dofile實現為: 1 function dofile(filename) 2 local f = assert(loadfile(filename)) 3 return f() 4 end 這里如果loadfile執行失敗,assert函數將直接引發一個錯誤。通過dofile的代碼,我們還可以看出,如果打算多次運行一個文件中的Lua代碼塊,我們可以只執行loadfile一次,之后多次運行它返回的結果即可,這樣就可以節省多次編譯所帶來的開銷。這一點也是loadfile和dofile在性能上的區別。 Lua中還提供了另外一種動態執行Lua代碼的方式,即loadstring函數。顧名思義,相比于loadfile,loadstring的代碼源來自于其參數中的字符串,如: f = loadstring("i = i + 1") 此時f就變成了一個函數,每次調用時就執行"i = i + 1",如: 1 i = 0 2 f() 3 print(i) --將輸出1 4 f() 5 print(i) --將輸出2 loadstring確實是一個功能強大的函數,但是由此而換來的性能開銷也是我們不得不考慮的事情。所以對于很多常量字符串如果仍然使用loadstring方式,那就沒有太大意義了,如上面的例子f = loadstring("i = i + 1"),因為我們完全可以通過f = function () i = i + 1 end的形式取而代之。而后者的執行效率要遠遠高于前者。畢竟后者只編譯一次,而前者則在每次調用loadstring時均被編譯。對于loadstring,我們還需要注意的是,該函數總是在全局環境中編譯它的字符串,因此它將無法文件局部變量,而是只能訪問全局變量,如: 1 i = 32 2 local i = 0 3 f = loadstring("i = i + 1; print(i)") 4 g = function() i = i + 1; print(i) end 5 f() --f函數中的i為全局變量i,因此輸出33 6 g() --g函數中的i為局部變量i,因此輸出1 對于loadstring返回的函數,如果需要對一個表達式求值,則必須在其之前添加return,這樣才能構成一條語句,返回表達式的值,如: 1 i = 32 2 f = loadstring("i = i + 1; return i * 2") 3 print(f()) --輸出66 4 print(f()) --輸出68。由于loadstring返回的就是正規的函數,因此可以被反復調用。 Lua將所有獨立的程序塊視為一個匿名函數的函數體,并且該匿名函數還具有可變長實參,因此在調用loadstring時,可以為其傳遞參數,如: 1 local i = 30 2 --下面的...表示變長實參,將值賦給局部變量x。 3 local f = assert(loadstring("local x = ...; return (x + 10) * 2")) 4 for i = 1, 20 do 5 print(string.rep("*",f(i))) 6 end 2. C代碼: 上一小節介紹的是動態加載Lua代碼,而事實上,Lua本身也支持動態加載C動態庫中的代碼,要完成該操作,我們需要借助于Lua內置的系統函數package.loadlib。該函數有兩個字符串參數,分別是動態庫的全文件名和該庫包含的函數名稱,典型的調用代碼如下: local path = "/usr/local/lib/test.so" local f = package.loadlib(path,"test_func") 由于loadlib是非常底層的函數,因為在調用時必須提供完整的路徑名和函數名稱。
3. 錯誤: Lua作為一種嵌入式腳本語言,在發生錯誤時,不應該只是簡單的退出或崩潰。相反,一旦有錯誤發生,Lua就應該結束當前程序塊并返回到應用程序。 在Lua中我們可以通過error()函數獲取錯誤消息,如: print "enter a number:" n = io.read("*number") if not n then error("invalid input") end 上面代碼中的最后一行我們可以通過Lua提供的另外一個內置函數assert類輔助完成,如: print "enter a number:" n = assert(io.read("*number"),"invalid input") assert函數將檢查其第一個參數是否為true,如果是,則簡單的返回該參數,否則就引發一個錯誤。第二個參數是可選字符串。 對于所有的編程語言而言,錯誤處理都是一個非常重要的環節。在實際的開發中,沒有統一的指導原則,只能是在遇到問題后,經過縝密的分析在結合當時的應用場景,最后結合自己的經驗再給出錯誤的具體處理方式。在有些情況下,我們可以直接返回錯誤碼,而在另外一些情況下,則需要直接拋出錯誤,讓開發者能夠快速定位導致錯誤的代碼源。
4. 錯誤處理與異常: Lua提供了錯誤處理函數pcall,該函數的第一個參數為需要“保護執行”的函數,如果該函數執行失敗,pcall將返回false及錯誤信息,否則返回true和函數調用的返回值。見如下代碼:
1 function foo() 2 local a = 10 3 print(a[2]) 4 end 5 6 r, msg = pcall(foo) 7 if r then 8 print("This is ok.") 9 else 10 print("This is error.") 11 print(msg) 12 end 13 --輸出結果為: 14 --This is error. 15 --d:/test.lua:3: attempt to index local 'a' (a number value) 我們也可以給pcall函數直接傳遞匿名函數,如: 1 r, msg = pcall(function() error({code = 121}) end) 2 if r then 3 print("This is ok.") 4 else 5 print("This is error.") 6 print(msg.code) 7 end 8 --輸出結果為: 9 --This is error. 10 --121 5. 錯誤消息與追溯: 通常在錯誤發生時,希望得到更多的調試信息,而不是只有發生錯誤的位置。至少等追溯到發生錯誤時和函數調用情況,顯示一個完整的函數調用棧軌跡。要完成這一功能,我們需要使用Lua提供的另外一個內置函數xpcall。該函數除了接受一個需要被調用的函數之外,還接受第二個參數,即錯誤處理函數。當發生錯誤時,Lua會在調用棧展開前調用錯誤處理函數。這樣,我們就可以在這個函數中使用debug庫的debug.traceback函數,它會根據調用棧來構建一個擴展的錯誤消息。如:
1 function errorFunc() 2 local a = 20 3 print(a[10]) 4 end 5 6 function errorHandle() 7 print(debug.traceback()) 8 end 9 10 if xpcall(errorFunc,errorHandle) then 11 print("This is OK.") 12 else 13 print("This is error.") 14 end 15 16 --輸出結果為: 17 --[[stack traceback: 18 d:/test.lua:7: in function <d:/test.lua:6> 19 d:/test.lua:3: in function <d:/test.lua:1> 20 [C]: in function 'xpcall' 21 d:/test.lua:10: in main chunk 22 [C]: ? 23 This is error. 24 --]]
1. 迭代器與Closure: 在Lua中,迭代器通常為函數,每調用一次函數,即返回集合中的“下一個”元素。每個迭代器都需要在每次成功調用之間保持一些狀態,這樣才能知道它所在的位置和下一次遍歷時的位置。從這一點看,Lua中closure機制為此問題提供了語言上的保障,見如下示例: 1 function values(t) 2 local i = 0 3 return function() 4 i = i + 1 5 return t[i] 6 end 7 end 8 t = {10, 20, 30} 9 it = values(t) 10 while true do 11 local element = it() 12 if element == nil then 13 break 14 end 15 print(element) 16 end 17 --另外一種基于foreach的調用方式(泛型for) 18 t2 = {15, 25, 35} 19 for element in values(t2) do 20 print(element) 21 end 22 --輸出結果為: 23 --10 24 --20 25 --30 26 --15 27 --25 28 --35 從上面的應用示例來看,相比于while方式,泛型for的方式提供了更清晰的實現邏輯。因為Lua在其內部替我們保存了迭代器函數,并在每次迭代時調用該隱式的內部迭代器,直到迭代器返回nil時結束循環。
2. 泛型for的語義: 上面示例中的迭代器有一個明顯的缺點,即每次循環時都需要創建一個新的closure變量,否則第一次迭代成功后,再將該closure用于新的for循環時將會直接退出。 這里我們還是先詳細的講解一下Lua中泛型(for)的機制,之后再給出一個無狀態迭代器的例子,以便于我們的理解。如果我們的迭代器實現為無狀態迭代器,那么就不必為每一次的泛型(for)都重新聲明一個新的迭代器變量了。 泛型(for)的語法如下: for <var-list> in <exp-list> do <body> end 為了便于理解,由于我們在實際應用中<exp-list>通常只是包含一個表達式(expr),因此簡單起見,這里的說明將只是包含一個表達式,而不是表達式列表。現在我們先給出表達式的原型和實例,如: 1 function ipairs2(a) 2 return iter,a,0 3 end 該函數返回3個值,第一個為實際的迭代器函數變量,第二個是一個恒定對象,這里我們可以理解為待遍歷的容器,第三個變量是在調用iter()函數時為其傳入的初始值。 下面我們再看一下iter()函數的實現,如: 1 local function iter(a, i) 2 i = i + 1 3 local v = a[i] 4 if v then 5 return i, v 6 else 7 return nil, nil 8 end 9 end 在迭代器函數iter()中返回了兩個值,分別對應于table的key和value,其中key(返回的i)如果為nil,泛型(for)將會認為本次迭代已經結束。下面我們先看一下實際用例,如: 1 function ipairs2(a) 2 return iter,a,0 3 end 4 5 6 local function iter(a, i) 7 i = i + 1 8 local v = a[i] 9 if v then 10 return i, v 11 else 12 return nil, nil 13 end 14 end 15 16 a = {"one","two","three"} 17 for k,v in ipairs2(a) do 18 print(k, v) 19 end 20 --輸出結果為: 21 --1 one 22 --2 two 23 --3 three 這個例子中的泛型(for)寫法可以展開為下面的基于while循環的方式,如: 1 local function iter(a, i) 2 i = i + 1 3 local v = a[i] 4 if v then 5 return i, v 6 else 7 return nil, nil 8 end 9 end 10 11 function ipairs2(a) 12 return iter,a,0 13 end 14 15 a = {"one","two","three"} 16 do 17 local _it,_s,_var = ipairs2(a) 18 while true do 19 local var_1,var_2 = _it(_s,_var) 20 _var = var_1 21 if _var == nil then --注意,這里只判斷迭代器函數返回的第一個是否為nil。 22 break 23 end 24 print(var_1,var_2) 25 end 26 end 27 --輸出結果同上。 3. 無狀態迭代器的例子: 這里的示例將實現遍歷鏈表的迭代器。
1 local function getnext(list, node) --迭代器函數。 2 if not node then 3 return list 4 else 5 return node.next 6 end 7 end 8 9 function traverse(list) --泛型(for)的expression 10 return getnext,list,nil 11 end 12 13 --初始化鏈表中的數據。 14 list = nil 15 for line in io.lines() do 16 line = { val = line, next = list} 17 end 18 19 --以泛型(for)的形式遍歷鏈表。 20 for node in traverse(list) do 21 print(node.val) 22 end 這里使用的技巧是將鏈表的頭結點作為恒定狀態(traverse返回的第二個值),而將當前節點作為控制變量。第一次調用迭代器函數getnext()時,node為nil,因此函數返回list作為第一個結點。在后續調用中node不再為nil了,所以迭代器返回node.next,直到返回鏈表尾部的nil結點,此時泛型(for)將判斷出迭代器的遍歷已經結束。 最后需要說明的是,traverse()函數和list變量可以反復的調用而無需再創建新的closure變量了。這主要是因為迭代器函數(getnext)實現為無狀態迭代器。
4. 具有復雜狀態的迭代器: 在上面介紹的迭代器實現中,迭代器需要保存許多狀態,可是泛型(for)卻只提供了恒定狀態和控制變量用于狀態的保存。一個最簡單的辦法是使用closure。當然我們還以將所有的信息封裝到一個table中,并作為恒定狀態對象傳遞給迭代器。雖說恒定狀態變量本身是恒定的,即在迭代過程中不會換成其它對象,但是該對象所包含的數據是否變化則完全取決于迭代器的實現。就目前而言,由于table類型的恒定對象已經包含了所有迭代器依賴的信息,那么迭代器就完全可以忽略泛型(for)提供的第二個參數。下面我們就給出一個這樣的實例,見如下代碼: 1 local iterator 2 function allwords() 3 local state { line = io.read(), pos = 1 } 4 return iterator, state 5 end 6 --iterator函數將是真正的迭代器 7 function iterator(state) 8 while state.line do 9 local s,e = string.find(state.line,"%w+",state.pos) 10 if s then 11 state.pos = e + 1 12 return string.sub(state.line,s,e) 13 else 14 state.line = io.read() 15 state.pos = 1 16 end 17 end 18 return nil 19 end
|