1 数据结构
1.1 数组
对table使用数字索引,即把table当数组使用。约定数组下标从1开始。
若索引中间有空洞,table并不会为空洞索引分配空间。
只有当表是顺序表时,#tab返回的长度才有意义。table.maxn() 返回最大的索引值。
对于非顺序表,遍历时使用pairs是一个好方法。
1.2 多维数组
即table的元素也是table。其能比较好的支持稀疏矩阵的存储。
1.3 链表
对于每个链表结点结构,可以用一个表{value=v,prev=p,next=n}来抽象,以及可以实现链表的相关算法。这样实现的链表将是一些table串连起来的结果。
对于在链表基础上的队列和双端队列,使用链表来实现性能会较差。可以充分利用顺序数组的特性来实现只在链表两端进行操作的数据结构。这样的数据结构,只有一个table来构成,通过整数索引来标识元素,通过head / tail分别存储两端的索引值。这样实现的队列会较为高效。
除了自己定义外,还可以直接使用table.insert() table.remove()接口来实现队列。
1.4 集合
table使用key作为索引,即可轻松作为集合来使用。
1.5 buffer
由于Lua中的字符串是不可修改的,因此在处理大文件时,如果使用字符串追加的方式,将导致每次都创建新的字符串并复制内容的大量开销。因此table.concat提供了连接多个字符串的功能,以避免每次都创建字符串。使用方式如下:
2 数据文件
2.1 数据文件
由于dofile的存在,文件中的数据可以以代码的行式执行,读入到代码中。例如
3 元表与元方法
元表用于预定义一个类型上的一系列操作集合,只有table的元表可以修改,其他类型的元表需要用C代码修改。table/userdata可以有多个独立的元表,但其他一个类型只有一个元表。
元表就是普通的table,可以设置特定的域来改变操作的行为。key为事件,而value为对应的元方法。
getmetatable() 用来获取特定元素的元表
setmetatable() 设置一个table元素的元表
3.1 算术类元方法
__add() + 运算对应的元方法
__sub()
__mul
__div
__mod
__pow
__unm 相反数
__concat .. 运算对应的元方法
当出现相应的运算时,如果第1操作数定义了相应的元方法,则执行第一操作数的方法,否则执行第2操作数的方法。如果两个操作数都不存在相应的元方法,则抛出错误。
3.2关系类元方法
__eq == 运算对应的方法
__lt <
__le <=
其他三个~= > >=则会转换为前三个操作符的非操作
关系运算不支持混合类型,否则会抛出错误。
3.3 table的元方法
__index t[x] t.x 元素访问操作,当索引不存在时执行,可以使用__index来实现单一继承。
__newindex t[x]= t.x= 元素设置操作,当索引不存在时执行
下面代码展示了__index的查找过程,从算法中可以看到,对于一个table的__index触发时,如果自己不存在key,则查找元表的__index方法。如果是一个非table对象,则总是计算其元表上的__index。
下面的示例通过原型来实现一层继承:
使用__index来实现继承成了一种模式,Lua支持直接向__index赋予一张table来实现与函数同样的功能。
通过元表可以为table实现一个代理表,从而监控表的操作,以及实现只读的表。
3.4 其它元方法
__tostring() 对象转换为字符串方法
__call 函数调用对象操作
__len # 运算对应的元方法
4 环境
Lua将全局变量保存一个常规的table中,命名为_G。对于全局变量的变量和数据,可以通过_G[]来高效的访问。
setfenv(level,tbl) 设置调用栈函数的环境为tbl,level1为当前函数,level2为调用当前函数的函数
setfenv(func,tbl) 设置函数func的运行环境
对于闭包函数而言,每当创建闭包时,都会继承一份其创建时的运行环境,因而可以利用闭包保存需要的环境。这也同时意味着,创建闭包的函数如果改变了环境,则后续所有创建的闭包都将继承此环境。这个特性对于命名空间的创建比较有用。
5 模块与包
从使用者的角度看,一个模块就是一个程序库,可以使用require来加载,规范的模块通过require返回一个table,同时将其添加到全局环境中去。table的内容就是模块中导出的所有函数、常量等。
标准库总会被预先加载,但也可以显式的加载,并对返回的表重命名。
5.1 require
require采用的搜索路径是一串模式,其中每项都是一种将模块名转换为文件名的方式。每个模式之间用;分隔。
例如:?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua 当require "socket"时,将把所有?替换为socket
lua文件的搜索模式存放于package.path中,在Lua启动时会使用LUA_PATH变量来初始化,若此变量为NIL,则使用编译时luaconf.h中的定义来初始化。
对于LUA_PATH中的;;模式,则替换为默认路径。
c库文件的搜索模式存放于package.cpath中,对应的变量为LUA_CPATH。.so文件会导出若干C函数,良好定义的.so库会导出一个名为"luaopen_<module>"的函数。require会链接完程序,并尝试调用此函数。
对require的实现逻辑分析后,需要注意两点:1 如果准备用lua脚本实现模块,则不要与静态预加载的C模块名重名,否则只会加载系统的内置C模块。2 如果加载的lua模块的文件名与文件内使用module声明的模块名不相同时,将导致返回一个NIL值。只有二者相同时,才返回一个同名的table。
5.2 编写模块
创建模块的基本方法是,创建一个table,将需要导出的函数放入其 ,最后返回这个table。以下示例代码展示了创建模块的模板:
需要注意的是,如果在模块内访问此模块内部的函数,需要进行限定性的调用,即需要添加模块名前缀。
5.3 使用环境
通过setfenv()可以改进上面的编写模板,使得在调用时不再需要使用模块名前缀。此外不也会导致全局命名空间的污染。
此方法导致将全局空间继承到本模块上,通过本模块也能访问到其它变量。另一种作法是将全局环境保存在新的_G中,当需要访问全局环境时,必须通过限定前缀来访问。
当然,如果不怕麻烦,可以在setfenv前,将需要导入的外部变量或模块保存到本地变量环境中。
以上三种技术的访问速度是递增的。
5.4 module函数
基于5.3节中的模式,Lua5.1导入了module函数,可以完成上面代码的功能。只需要在模块代码前加一行:
5.5 子模块与包
Lua支持层级模块名,可以使用.来分隔名称的层级。require在查询时,将.替换为系统目录的分隔符号。
6 面向对象
6.1 对象
Lua中的table实例就是一个对象。table中可定义属性和方法。示例:
代码中的self可以替换为Account,这样函数就不需要传递第一个self参数,但这样将导致对象只能在Account变量名存在的时候进行使用。通过使用self,可以解决这个问题。
也可以通过::来声明及调用,其会向声明函数和调用函数中,增加一个隐藏的self参数。
6.2 类
为了表示一个类,只需要创建一个专用作类的实例的原型。原型是一组对象间共享行为的机制。如果一个对象的原表被设置为一个原型,那么可以称原型为对象的类。仍以Account为例:
6.3 继承
由于作为类的表也是对象,它们也可以从其他类获取方法。因此可以用来实现多层继承。
6.4 多重继承
实现多重继承的关键在于__index将赋值为一个通用函数。通用函数负责在多个父table中查找相应的字段或操作。
6.5 封装
Lua本身定位于开发小型系统,并没有显式的提供可见性的控制。但可以通过以下的方法来模拟封装的特性。
一个对象由两个表组成,一个表记录对象的属性,一个表记录对象的方法。对象本身对过第二个表来访问。表示属性的表保存在方法的闭包中。这要求对象的创建使用特殊的函数。示例
7 弱引用table
Lua具有内存回收的功能,但垃圾回收器也需要帮忙。例如对于只用一次的全局变量,需要设置为NIL才能被正常回收。
为了避免需要人为的去置为NIL,LUA提供了弱引用表的机制,用户能用它来告诉Lua一个引用不应该阻碍一个对象的回收。
所谓弱引用,就是一种会被收集器忽视的对象引用。如果一个对象所有引用都是弱引用,那么Lua就可以回收这个对象了,并且还可以删除这些弱引用本身。
弱引用表用来实现弱引用 ,其中的条目是弱引用条目。当一个对象只被一个弱引用表所持有时,Lua便可以回收这个对象。
常规的表,其key和value都是不能被回收的。而弱引用表中,可以设置key或 value或二者都是弱引用的。不管什么模式,只一个key或value被回收,那么整个kv对就被回收了。
需要注意的是,弱引用table中的对象才能被回收,数字和字符串这种值,是不可回收的。tbl[1]这种的将不会被回收,除非其对应的值被回收了。
table = {--mode="v" | --mode="k" | --mode="kv"} 来设置针对弱引用的模式。
7.1 memorize模式
此模式支持用空间换时间,可以将计算过程中的结果记录在表中,以便后续使用,以此为加速计算过程。
1.1 数组
对table使用数字索引,即把table当数组使用。约定数组下标从1开始。
若索引中间有空洞,table并不会为空洞索引分配空间。
只有当表是顺序表时,#tab返回的长度才有意义。table.maxn() 返回最大的索引值。
对于非顺序表,遍历时使用pairs是一个好方法。
1.2 多维数组
即table的元素也是table。其能比较好的支持稀疏矩阵的存储。
1.3 链表
对于每个链表结点结构,可以用一个表{value=v,prev=p,next=n}来抽象,以及可以实现链表的相关算法。这样实现的链表将是一些table串连起来的结果。
对于在链表基础上的队列和双端队列,使用链表来实现性能会较差。可以充分利用顺序数组的特性来实现只在链表两端进行操作的数据结构。这样的数据结构,只有一个table来构成,通过整数索引来标识元素,通过head / tail分别存储两端的索引值。这样实现的队列会较为高效。
- List = {}
- List.new = function()
- return {first= 1 ,last= 1}
- end
- List.pushback = function(list,value)
- local last = list.last + 1;
- list.last = last;
- List[last] = value;
- end
- List.pop = function(list)
- local first = list.frist;
- if first > list.last then
- error("list is empty")
- end
- local value = list[first];
- list[first] = nil;
- return value;
- end
除了自己定义外,还可以直接使用table.insert() table.remove()接口来实现队列。
1.4 集合
table使用key作为索引,即可轻松作为集合来使用。
1.5 buffer
由于Lua中的字符串是不可修改的,因此在处理大文件时,如果使用字符串追加的方式,将导致每次都创建新的字符串并复制内容的大量开销。因此table.concat提供了连接多个字符串的功能,以避免每次都创建字符串。使用方式如下:
- local buff = {};
- for line in io.lines() do
- t[#t + 1] = line;
- end
- local all = table.concat(t,'\n');
- io.read("*all") -- 采取了上面算法来创建大的字符串。
2 数据文件
2.1 数据文件
由于dofile的存在,文件中的数据可以以代码的行式执行,读入到代码中。例如
- --data文件内容--
- Entry({"Jim","Green","Male","14","American"})
- --代码文件
- count = 0
- Entry = function(item)
- for k,v in pairs(item) do
- print(v);
- count = count+1;
- end
- end
- dofile('data');
3 元表与元方法
元表用于预定义一个类型上的一系列操作集合,只有table的元表可以修改,其他类型的元表需要用C代码修改。table/userdata可以有多个独立的元表,但其他一个类型只有一个元表。
元表就是普通的table,可以设置特定的域来改变操作的行为。key为事件,而value为对应的元方法。
getmetatable() 用来获取特定元素的元表
setmetatable() 设置一个table元素的元表
3.1 算术类元方法
__add() + 运算对应的元方法
__sub()
__mul
__div
__mod
__pow
__unm 相反数
__concat .. 运算对应的元方法
当出现相应的运算时,如果第1操作数定义了相应的元方法,则执行第一操作数的方法,否则执行第2操作数的方法。如果两个操作数都不存在相应的元方法,则抛出错误。
3.2关系类元方法
__eq == 运算对应的方法
__lt <
__le <=
其他三个~= > >=则会转换为前三个操作符的非操作
关系运算不支持混合类型,否则会抛出错误。
3.3 table的元方法
__index t[x] t.x 元素访问操作,当索引不存在时执行,可以使用__index来实现单一继承。
__newindex t[x]= t.x= 元素设置操作,当索引不存在时执行
下面代码展示了__index的查找过程,从算法中可以看到,对于一个table的__index触发时,如果自己不存在key,则查找元表的__index方法。如果是一个非table对象,则总是计算其元表上的__index。
- function gettable_event (table, key)
- local h
- if type(table) == "table" then
- local v = rawget(table, key)
- -- if key is present, return raw value
- if v ~= nil then
- return v
- end
- h = metatable(table).__index
- if h == nil then
- return nil
- end
- else
- h = metatable(table).__index
- if h == nil then
- error(···)
- end
- end
- if type(h) == "function" then
- return (h(table, key)) -- call the handler
- else
- return h[key] -- or repeat operation on it
- end
- end
下面的示例通过原型来实现一层继承:
- Window = {}
- Window.prototype = {x=0,y=0,width=100,height=100}
- Window.mt = {__index=function(tbl,key) return Window.prototype[key] end}
- Window.new = function(obj)
- setmetatable(obj,Windows.mt);
- return obj;
- end
- win = Window.new({x=30,y=30});
- print(win.width) -- 100
使用__index来实现继承成了一种模式,Lua支持直接向__index赋予一张table来实现与函数同样的功能。
- Window.mt = {__index=Window.prototype}
通过元表可以为table实现一个代理表,从而监控表的操作,以及实现只读的表。
3.4 其它元方法
__tostring() 对象转换为字符串方法
__call 函数调用对象操作
__len # 运算对应的元方法
4 环境
Lua将全局变量保存一个常规的table中,命名为_G。对于全局变量的变量和数据,可以通过_G[]来高效的访问。
setfenv(level,tbl) 设置调用栈函数的环境为tbl,level1为当前函数,level2为调用当前函数的函数
setfenv(func,tbl) 设置函数func的运行环境
对于闭包函数而言,每当创建闭包时,都会继承一份其创建时的运行环境,因而可以利用闭包保存需要的环境。这也同时意味着,创建闭包的函数如果改变了环境,则后续所有创建的闭包都将继承此环境。这个特性对于命名空间的创建比较有用。
5 模块与包
从使用者的角度看,一个模块就是一个程序库,可以使用require来加载,规范的模块通过require返回一个table,同时将其添加到全局环境中去。table的内容就是模块中导出的所有函数、常量等。
标准库总会被预先加载,但也可以显式的加载,并对返回的表重命名。
5.1 require
require采用的搜索路径是一串模式,其中每项都是一种将模块名转换为文件名的方式。每个模式之间用;分隔。
例如:?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua 当require "socket"时,将把所有?替换为socket
lua文件的搜索模式存放于package.path中,在Lua启动时会使用LUA_PATH变量来初始化,若此变量为NIL,则使用编译时luaconf.h中的定义来初始化。
对于LUA_PATH中的;;模式,则替换为默认路径。
c库文件的搜索模式存放于package.cpath中,对应的变量为LUA_CPATH。.so文件会导出若干C函数,良好定义的.so库会导出一个名为"luaopen_<module>"的函数。require会链接完程序,并尝试调用此函数。
当需要使用同一个库的不同版本,可以利用require的一个特性:如果模块名中包含连字符,如1.4-zlib.so,那么其只使用连字符后的内容来创建调用函数,本例为luaopen_zlib
- function require(name)
- if not package.loaded[name] then
- local loader = findloader(name)
- if loader == nil then
- error("unable to load module"..name)
- end
- package.loaded[name] = true
- local res = loader(name)
- if res ~= nil then
- package.loaded[name] = res
- end
- end
- return package.loaded[name]
- end
5.2 编写模块
创建模块的基本方法是,创建一个table,将需要导出的函数放入其 ,最后返回这个table。以下示例代码展示了创建模块的模板:
- local M = {}
- _G['modname'] = M
- package.loaded['modname'] = M
- --module code ...
需要注意的是,如果在模块内访问此模块内部的函数,需要进行限定性的调用,即需要添加模块名前缀。
5.3 使用环境
通过setfenv()可以改进上面的编写模板,使得在调用时不再需要使用模块名前缀。此外不也会导致全局命名空间的污染。
- local M = {}
- _G['modname'] = M
- package.loaded['modname'] = M
- setmetatable(M,{__index=_G}) --local _G = _G
- setfenv(1,M)
- --module code ...
此方法导致将全局空间继承到本模块上,通过本模块也能访问到其它变量。另一种作法是将全局环境保存在新的_G中,当需要访问全局环境时,必须通过限定前缀来访问。
当然,如果不怕麻烦,可以在setfenv前,将需要导入的外部变量或模块保存到本地变量环境中。
以上三种技术的访问速度是递增的。
5.4 module函数
基于5.3节中的模式,Lua5.1导入了module函数,可以完成上面代码的功能。只需要在模块代码前加一行:
- module('name'[,package.seeall]) --后续代码即可像写普通的lua代码一样
5.5 子模块与包
Lua支持层级模块名,可以使用.来分隔名称的层级。require在查询时,将.替换为系统目录的分隔符号。
6 面向对象
6.1 对象
Lua中的table实例就是一个对象。table中可定义属性和方法。示例:
- Account = { balance = 0;
- withdraw = function(self,v) self.balance -= v; end
- deposite = function(self,v) self.balance += v; end
- }
- Account:query = function(v) return self.balance;end
- a = Account;
- a.deposite(a,500);
- a.withdraw(a,100);
- a:query();
代码中的self可以替换为Account,这样函数就不需要传递第一个self参数,但这样将导致对象只能在Account变量名存在的时候进行使用。通过使用self,可以解决这个问题。
也可以通过::来声明及调用,其会向声明函数和调用函数中,增加一个隐藏的self参数。
6.2 类
为了表示一个类,只需要创建一个专用作类的实例的原型。原型是一组对象间共享行为的机制。如果一个对象的原表被设置为一个原型,那么可以称原型为对象的类。仍以Account为例:
- Account:new = function(obj)
- if not obj then
- obj = {}
- end
- self.__index = self
- setmetatable(obj,self)
- return obj;
- end
- a = Account:new()
- a.deposite(100)
6.3 继承
由于作为类的表也是对象,它们也可以从其他类获取方法。因此可以用来实现多层继承。
- SpecialAccount = Account:new()
- sa = SpecialAccount:new({limit=1000})
对象sa及相关类table的关系如下图:
6.4 多重继承
实现多重继承的关键在于__index将赋值为一个通用函数。通用函数负责在多个父table中查找相应的字段或操作。
- local function search (key,list) --search the first matched key
- for i=1,#list do
- local val = plist[i][key]
- if val then
- return val
- end
- end
- end
- function createClass(...)
- local c = {}; -- new class
- local parents = {...} -- parent class from args
- local finder = function(tbl,key)
- return search(key,parents)
- end
- setmetatable(c,{__index = finder });
- c.__index = c;
- c:new = functon(obj)
- if not obj
- obj = {}
- end
- setmetatable(obj,c)
- return obj
- end
- return c;
- end
- Named = {}
- Named.getname = function(self)
- return self.name
- end
- Name.setName = function(self,n)
- self.name = n
- end
- --现在以Named和Account为基类,创建新类
- NamedAccount = createClass(Account,Named);
- na = NamedAccount:new({name="Jobs"})
- print(na.getname())
6.5 封装
Lua本身定位于开发小型系统,并没有显式的提供可见性的控制。但可以通过以下的方法来模拟封装的特性。
一个对象由两个表组成,一个表记录对象的属性,一个表记录对象的方法。对象本身对过第二个表来访问。表示属性的表保存在方法的闭包中。这要求对象的创建使用特殊的函数。示例
- newAccount = function(initBalance)
- local self = {balance=initBalance} --private data
- local withdraw = function(v) --public function
- self.balance -= v;
- end
- local deposit = function(v) --public function
- self.balance -= v;
- end
- local query = function() --public function
- return self.balance;
- end
- local method = function() --private function
- ...
- end
- return {
- withdraw = withdraw,
- deposit = deposit,
- query = query
- }
- end
- --使用时
- account = newAccount(100)
- account.withdraw(40)
- print(account.query())
7 弱引用table
Lua具有内存回收的功能,但垃圾回收器也需要帮忙。例如对于只用一次的全局变量,需要设置为NIL才能被正常回收。
为了避免需要人为的去置为NIL,LUA提供了弱引用表的机制,用户能用它来告诉Lua一个引用不应该阻碍一个对象的回收。
所谓弱引用,就是一种会被收集器忽视的对象引用。如果一个对象所有引用都是弱引用,那么Lua就可以回收这个对象了,并且还可以删除这些弱引用本身。
弱引用表用来实现弱引用 ,其中的条目是弱引用条目。当一个对象只被一个弱引用表所持有时,Lua便可以回收这个对象。
常规的表,其key和value都是不能被回收的。而弱引用表中,可以设置key或 value或二者都是弱引用的。不管什么模式,只一个key或value被回收,那么整个kv对就被回收了。
需要注意的是,弱引用table中的对象才能被回收,数字和字符串这种值,是不可回收的。tbl[1]这种的将不会被回收,除非其对应的值被回收了。
table = {--mode="v" | --mode="k" | --mode="kv"} 来设置针对弱引用的模式。
7.1 memorize模式
此模式支持用空间换时间,可以将计算过程中的结果记录在表中,以便后续使用,以此为加速计算过程。
如果使用弱引用表,则可以自动回收不用的条目,避免表变的过大而影响计算过程。当然也会出现中间发生垃圾回收,导致需要重新计算的问题。