整理一下Lua的基本语法和常识。
全局变量
Lua中,全局变量无需声明即可使用,使用未初始化的全局变量也不会导致错误,只是结果为nil
,nil
是一种和其他所有值进行区分的类型,表示无效的值,一个全局变量在第一次被赋值前的默认值就是nil
,而将nil
赋值给全局变量则相当于将其删除。
Boolean类型
- Lua中将除
false
和nil
外的所有其他值视为真。 not
只返回true
或false
。and
运算结果为:如果它的第一个操作数为false
,则返回第一个操作数,否则返回第二个操作数。or
运算结果为:如果它的第一个操作数不为false
,则返回第一个操作数,否则返回第二个操作数。
有一种常用的表达式:a and b or c(注意and的优先级比or高),如表达式x > y and x or y
会选出x和y中较大的一个。
独立解释器(standalone interpreter)
我们用lua命令lua [options] [script [args]]
来运行lua脚本
数值
以下公式是取模运算的定义:
a % b == a - ((a // b) * b)
,注意这里(a//b) * b
会根据整数和小数的不同,得到抹去最后几位数字为零的结果。
对整型操作数来说,取模运算结果的符号永远和第二个操作符一致。对于实数类型的操作数来说,x - x % 0.01
恰好是x保留两位小数的结果。
小于2^53的所有整形值的表示与双精度浮点型值的表示一样,对于绝对值超过了这个值的整型而言,将其转换为浮点型可能导致精度损失。顺带一提,将一个整型值加上0.0就能转换为浮点型,将浮点型与0按位或运算就能转化为整型。
解决八皇后问题
检查斜对角的方法比较巧妙,8x8的棋盘上如果从第一排摆棋子直到最后一排,那么每次检查只需要检查当前排上面的排,无非就是三种情况,同列、左对角、右对角,而检查左右对角线时可以利用列减行的套路,如果两个棋子在对角线上,那么其坐标中的纵坐标与横坐标的差必定相等。
N = 8;
function printBoard(a)
for i = 0, N - 1 do
for j = 0, N - 1 do
io.write(a[i] == j and "X" or "-", " ")
end
io.write("n")
end
io.write("n")
end
--check if the queen on row n, column col can be attacked
function check(a, n, col)
for i = 0, n - 1 do
if(a[i] == col) or --self column
(a[i] - i == col - n) or --left corner
(a[i] + i == col + n) then -- rigt corner
return false
end
end
return true
end
count = 0
--在第n排放皇后
function placeQueen(a, n)
if(n > N - 1) then
--printBoard(a)
count = count + 1
return
end
for col = 0, N - 1 do --go for columns
if check(a, n, col) then
a[n] = col --record column
placeQueen(a, n + 1)
end
end
end
placeQueen({}, 0)
print(count)
注意这个算法并不是最优。
表(table)
可以把表的索引当成成员名称使用,即a.name与a["name"]等价,注意是a["name"]不是a[name]。
表构造器{}
可以用来初始化表,如果初始化时没有定义键值,则默认从1开始,而不是从0开始,Lua很多机制都遵循这个惯例。
我们可以用pairs
遍历表中的键值对,遍历中元素出现的顺序是随机的,且每个元素只会出现一次,对于没有指定键(key)的列表,可以用ipairs
迭代器:
t = {10, print, 123, "hi"}
for k, v in ipairs(t) do
print(k, v)
end
--> 1 10
--> 2 function: 0x420610
--> 3 12
--> 4 hi
此时会确保遍历是按照顺序进行。
函数
Lua中函数是第一类值,所有函数都是匿名的,函数并没有名字,调用时实际上访问的是保存该函数的变量,通常该变量为全局变量,从而看起来函数有了一个名字,但在很多场合下依然会保留函数的匿名性。
举例来说,foo = function(x) return 2 * x end
就是将一个函数赋值给了变量foo
。
table.sort函数可以为我们排序表中的元素,
network = {
{name = "aaa", IP = "192.1.1.1"},
{name = "bbb", IP = "192.1.1.2"},
-- ...
}
table.sort(network, function (a, b) return (a.name > b.name) end)
再比如,导数的计算公式为f'(x) = (f(x + d) - f(x)) / d
,d趋于无穷小,我们可以写出计算导数的函数,
function derivative(f, delta)
delta = delta or 1e-4
return function (x)
return f(f(x + delta) - f(x)) / delta
end
end
myCos = derivative(math.sin)
非全局函数
由于函数是第一类值,因此函数还可以被储存在表字段和局部变量中
lib = {
foo = function (x, y) return x + y end,
goo = function (x, y) return x - y end
}
--或者
lib = {}
function lib.foo(x, y) return x + y end
function lib.goo(x, y) return x - y end
还可以用关键词local
来将一个函数声明为局部函数
local fact
fact = function(n)
if n == 0 then
return 1
else
return n * fact(n - 1)
end
end
注意上面我们不能直接对fact
函数初始化,否则在递归调用的时候会在函数体中尝试寻找全局的fact
函数,但是它并不存在。
词法定界(lexical scoping)和闭包(Closure)
这是一个很重要的概念,如果一个函数A包含函数B,即函数A中包含了函数B,函数B中的代码其实可以访问函数A中的局部变量,此时在函数B中该变量既不是局部变量也不是全局变量,而是所谓的非局部变量(non-local variable),也被称为上值(upvalue)。
function myCounter()
local count = 0
return function()
count = count + 1
return count
end
end
c1 = myCounter()
print(c1()) ---> 1
print(c1()) ---> 2
c2 = myCounter()
print(c2()) ---> 1
print(c2()) ---> 2
print(c2()) ---> 3
上述代码就是一个产生闭包的例子,函数myCounter
中的匿名函数访问了非局部变量count
,此时它与被赋值的函数名称绑定,在每次运行时加1,因此当再次将函数myCounter
赋值给c2
时,新的count
与新的闭包被创建,就会重新开始计数。此时c1
和c2
是不同的闭包,但建立在相同的函数上,拥有各自的非局部变量。
以下是wiki对闭包的解释:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。
闭包一词经常和匿名函数混淆。这可能是因为两者经常同时使用,但是它们是不同的概念。
闭包也被经常用来重新定义函数
do
local oldSin = math.sin
math.sin = function(x)
return oldSin(x * (math.pi / 180))
end
end
这段代码中do
被用来限制局部变量oldSin
的作用范围。
画个圆
function circle(cx, cy, cr)
return function(x, y)
return (x - cx) ^ 2 + (y - cy) ^ 2 <= cr ^ 2
end
end
function plot(area, width, height)
f = io.open("img.pbm", 'w')
f:write("P1n", width, " ", height, "n") -- 位图
for j = 1, height do
for i = 1, width do
f:write(area(i, j) and "1" or "0")
end
f:write("n")
end
f:close()
end
c = circle(400, 300, 100) -- 在图中心,半径100个像素
plot(c, 800, 600)
数据结构
Lua中的表并不是一种数据结构,而是其他数据结构的基础。
元表
元表定义的是实例的行为,但它只能给出预先定义的操作集合的行为,可以看作是oop中受限制的类,每一个表和用户数据类型都具有各自独立的元表,Lua中在创建新表时不带元表,可以用函数setmetatable
来设置或修改元表,通常我们也只能为表设置元表。
字符串标准库为所有的字符串都设置了同一个元表(函数getmetatable
用来获取元表),而其他类型在默认情况下都没有元表。
一个表可以成为任意值的元表,甚至成为自身的元表,用于描述自身特有的行为。
下面是一个集合的例子,来表现元表的用法,集合中有得到并集和交集的方法,现在为了让两个集合能直接用运算符+
来计算并集,x
来计算交集,我们在创建每个集合的函数中加入创建元表的代码:
local Set = {}
local mt = {} --meta table
function Set.new(l)
local set = {}
setmetatable(set, mt) --in this way, all sets created by this method share the same meta table
for _, v in ipairs(l) do
set[v] = true
end
return set
end
function Set.union(a, b)
local res = Set.new{}
print("a: ")
for k in pairs(a) do
--print(k)
res[k] = true
end
print("b: ")
for k in pairs(b) do
res[k] = true
--print(k)
end
return res
end
function Set.intersection(a, b)
local res = Set.new{}
for k in pairs(a) do
res[k] = b[k]
end
return res
end
function Set.printSet(set)
for k, v in pairs(set) do print(k, v) end
end
set1 = Set.new{10, 20, 30, 40}
set2 = Set.new{50, 70, 30}
mt.__add = Set.union -- 将方法union与加法运算符绑定
mt.__mul = Set.intersection -- 将方法intersection与乘法运算符绑定
Set.printSet(set1 + set2) --> 10, 20, 30, 40, 50, 70
print()
Set.printSet(set1 * set2) --> 30
还有诸如对运算符==
(eq)、<=
(le)、<
(__lt)的扩展,就不展开了。另外如果需要将元表对用户隐藏,只需要在原表中设置__metatable
字段,那么getmetatable
就会返回这个字段的值,而setmetatable
则会引发一个错误。
总的来说元表就是可以修改一个值在面对位置操作时的行为,通常是用户自定义的操作。其中若干定义具体操作的方法,比如上面例子的__add
被称为元方法。
__index元方法
当访问一个表中不存在的字段时会得到nil
,而事实上,系统会在这种情况发生时去查找一个名为__index
的元方法,如果它存在,那么就由它来提供最终的结果。
根据这个特性,我们可以为要定义的表事先定义一个原型表,如果后面在初始化时只初始化了部分字段,其余字段就会由原型中的默认值提供,而不是直接返回nil
了。
prototype = {x = 0, y = 0, width = 100, height = 100}
local mt = {}
function new(o)
setmetatable(o, mt)
return o
end
mt.__index = function (_, key)
return prototype[key]
end
w = new{x = 10, y = 20}
print(w.width) --> 100
我们也可以将__index
定义成一个函数,这样可以为不存在的索引提供一个统一的默认值。
local mt = {__index = function(t) return t.___ end}
function setDefault(t, d)
t.___ = d
setmetatable(t, mt)
end
如果我们不希望在访问一个表时调用__index
元方法,可以使用函数rawget(table, i)
,但这并不会加快代码的执行。
__newindex元方法
与index元方法类似,只是newindex用于表的更新而非查找,如果对一个表中不存在的索引赋值,解释器就会查找__newindex元方法,如果它存在,解释器就会调用它而不执行赋值,如果它是一个表,就会在这个表中进行赋值,而不是在原始的表中进行赋值。同样的,如果我们不希望在赋值时调用它,可以使用函数rawset(table, k, v)
。
跟踪对表的访问
我们用index和newindex跟踪对某个表的所有访问,但这两个元方法都是在表为空时才会被调用,因此如果需要监控一个表,要创建一个表代理(proxy),它是一个空表,用于追踪所有访问并将访问重新定向到原来的表,下面是实现代码:
function track(t)
local proxy = {}
local mt = {
__index = function(_, k)
print("_ : ", _)
print("*access to element" .. tostring(k))
return t[k]
end,
__newindex = function(_, k, v)
print("_ : ", _)
print("*update of element " .. tostring(k) .. " to " .. tostring(v))
t[k] = v
end,
__pairs = function()
return function(_, k)
local nextkey, nextvalue = next(t, k)
if nextkey ~= nil then
print("*traversing element " .. tostring(nextkey))
end
return nextkey, nextvalue
end
end,
__len = function()
return #t
end
}
setmetatable(proxy, mt)
return proxy
end
function test(t)
print("{")
for k, v in pairs(t) do
print(k, v)
end
print("}")
end
t = {}
t2 = track(t)
t2[2] = "hello"
print(t2[2])
print(t)
此时t
指向的是我们建立的空表proxy
,但赋值和取值操作都在它的元表中被重定向到之前的表t
了。需要遍历时,迭代器pairs
会找到代理表中的__paris
方法为我们输出表中的数据。
如果需要监控多个表,并不用为每个表创建不同的元表,只需要将每个代理与其原始表映射起来,然后让所有的代理共享一个公共的元表即可。
只读的表
根据上面学到的,要创建一个只读的表并不难,一样用代理的方法,将原始表赋值给__index
,然后将__newindex
设置为抛出异常:
function readOnlyTable(t)
local proxy = {}
local mt = {
__index = t,
__newindex = function(t, k, v)
error("permission denied", 2)
end
}
setmetatable(proxy, mt)
return proxy
end
OOP
创建一个账户Account
,其中有存钱、取钱的功能:
Account = {balance = 0}
function Account.withdraw(self, amount)
self.balance = self.balance - amount
end
--use ":" to hide "self" parameter
function Account:deposit(amount)
self.balance = self.balance + amount
end
Account.deposit(Account, 200)
print(Account.balance) ---> 200
Account:withdraw(100)
print(Account.balance) ---> 100
如果需要创建多个银行账户,那么可以把Account
作为一个原型类,后面的账户都是它生成的实例,那么可以将实例中元表的__index = Account
:
local mt = {__index = Account}
function Account.new(o)
o = o or {}
setmetatable(o, mt)
return o
end
思路很简单,在Account
“类”中新建一个“成员函数”new
用来生成实例,它只做一件事,就是将新的表的元表设置成我们定义好的mt
,那么这个用new
方法新建的表就会含有Account
中的“成员”。
而事实上,我们并不需要mt
做转换,可以直接将Account
本身作为元表,在它的成员中加入元方法__index
,由上面的思路,这个元方法就是它自己。这里有一点绕,需要理清思路想清楚逻辑,优化后的代码如下:
function Account:new(o)
o = o or {} --initiate
self.__index = self
setmetatable(o, self)
return o
end
这种写法下Account
直接作为实例的元表,其中元方法__index
就是它自己,如果生成的实例中某种行为或变量没有定义,则会到元表也就是Account
中去查看是否存在元方法__index
,然后会发现存在并且就是Account
,那么就会在Account
中查找有没有前面需要调用的成员。注意这里并不会像递归那样无限重复这个过程,因为Lua只会对定义了元表的值去检查,而Account
自己是没有定义任何元表的。
继承
有了类就会有继承,如果我们需要某些基类没有提供的新特性,比如某个VIP账号可以透支,那么就需要创建新的取钱方法,此时相当于基类派生出了子类,并且子类复写了基类提供的取钱方法,基类提供了默认的取钱方法,但子类可以有自己的取钱实现,如果需要就自己去实现,即多态。下面是对Account
基类派生子类VIP_Account
的代码:
function Account:deposit(amount)
self.balance = self.balance + amount
end
function Account:withdraw(amount)
if(amount > self.balance) then
error("insufficient funds")
end
self.balance = self.balance - amount
end
VIP_Account = Account:new()
function VIP_Account:withdraw(amount)
if(amount - self.balance > self:getLimit()) then
error("insufficient funds")
end
self.balance = self.balance - amount
end
function VIP_Account:getLimit()
return self.limit or 0
end
vip = VIP_Account:new{ limit = 1000 }
account = Account:new()
vip:deposit(2000)
vip:withdraw(1000)
vip:withdraw(1500)
print()
account:deposit(2000)
account:withdraw(1000)
account:withdraw(1500)
--deposit done, balance: 2000
--withdraw done, balance: 1000
--withdraw done, balance: -500
--deposit done, balance: 2000
--withdraw done, balance: 1000
--insufficient funds
子类VIP_Account
和基类的取钱方法名称相同,因此解释器在调用时会直接用子类的版本而不会去访问Account
中的取钱方法。
有意思的是,我们创建VIP_Account
这个子类时其实调用的是Account
中生成实例的方法new
,它本质上还是一个表,我们在这个表中加入了新的方法withdraw
和getLimit
,然后再用它去调用new
,Lua会发现VIP_Account
这个表中没有这个方法,于是在原表中的__index
(也就是Account
)中去找,从而调用了Account
的new
方法,这个过程就可以看作是VIP_Account
这个类从基类Account
中继承了方法new
。 可见在Lua中实例和类的概念是模糊的,一个表既可以是一个实例也可以是一个类,生成新的实例,这和传统的语言如Java、C++都很不一样。上面的例子中VIP_Account
和Account
中其实都是一个表,只要弄清楚它们之间的关系就很好理解了。
多继承
根据上面的思路,如果一个类需要继承自多个父类,我们需要将__index
变成一个可以在多个父类中搜索成员变量或函数的函数,同时需要一个专门的函数createClass
来创建这个有多个父类的子类:
--find member k in parents
function search(parents, k)
for i = 1, #parents do
local v = parents[i][k]
if v then return v end
end
end
--create multi inherit class
function createClass(...)
local c = {}
local parents = {...}
setmetatable(c, { __index = function(t, k)
return search(parents, k)
end})
c.__index = c
function c:new(o)
o = o or {}
setmetatable(o, c)
return o
end
return c
end
Named = {}
function Named:getName()
return self.name
end
function Named:setName(_name)
self.name = _name
end
NamedAccount = createClass(Account, Named)
namedAccount = NamedAccount:new{name = "Tizeng"}
print(namedAccount:getName())
namedAccount:setName("Tizeng Yan")
print(namedAccount:getName())
namedAccount:deposit(1000)
namedAccount:withdraw(1000)
namedAccount:withdraw(10)
--Tizeng
--Tizeng Yan
--deposit done, balance: 1000
--withdraw done, balance: 0
--insufficient funds
在创建新类时,在createClass
函数的参数中输入要继承的父类,新类元表的__index
就不再是一个表,而是一个在父类中搜索成员的函数,然后就是比较让人混淆的一点,createClass
用来创建新派生的子类,而其中的new
函数用来生成新类的实例,思路和之前定义Account
类一样,将自己作为新类的元表,并将元方法__index
定义成自己。将new
的定义放在createClass
中是因为只有在创建了新类后才能创建它包含的函数,这样一来根据闭包的特性,生成的每个实例的元表都会是c
。
一个类不能同时成为它实例和子类的元表。
私有性
在Lua中,我们通过把需要隐藏的成员与函数绑定成闭包来实现成员的私有化,直接用代码举例:
function newAccount(_balance)
local self = {balance = _balance}
local withdraw = function(amount)
self.balance = self.balance - amount
end
local deposit = function(amount)
self.balance = self.balance + amount
end
local getBalance = function() return self.balance end
return {
withdraw = withdraw,
deposit = deposit,
getBalance = getBalance
}
end
acc = newAccount(100)
acc.withdraw(40)
print(acc.getBalance()) --> 60
上面的代码中,返回的表只有三个“公有”的方法,对“私有”成员self
进行操作,self
对外部不可见,调用这些方法时,我们也不需要之前的:
操作符。