#
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
|