Lua,让人惊叹的艺术!
2008 - 09 - 30 12:09
Lua 对于国内开发者来说可能还比较陌生,然而随着它在电子娱乐、网络娱乐领域得到大量应用,许多人也开始关注起它来。
Lua 是一门脚本语言,虽小巧但功能完备。它主要是面向过程的语言,与大多数脚本语言类似。不过,Lua 围绕“栈”的语言构造以及强大的表格基本类型,不仅使其执行速度在所有优秀脚本语言中最快,而且语法优美、灵活多变。一些语言所标榜的诸多特性,例如面向对象,对于 Lua 来说,只需使用一些“语法糖”就能实现,这听起来是不是有些夸张?
事实上,Lua 中类似的夸张之处还有很多,关键在于,Lua 实现这些特性不费吹灰之力,也无需艰深的语法,这才是最令人惊叹的。如果你也开始觉察到 Lua 的独特之处,那么就请随我一同走进 Lua 的世界。
新手提示
可以下载最新的 LuaEdit 程序来编辑和运行 Lua 脚本。你可以很容易地从网上找到并下载它,3.0 或以上版本就足够运行下面的示例了。打开 LuaEdit,选择 File - New Unit,输入或粘贴代码,按“播放”键即可。
Lua 之简
Lua 需要我们记住的内容只有变量、关键字和基本语法。关键字和语法与常见语言几乎一样,例如 if…then…elseif…else…end、while…do、for…in、function、repeat(循环次数控制)、return;像 +、-、*、/、^ 之类的标准数学符号,对于有编程基础的人来说,几乎无需学习。
所谓“变量”,除了能存储单个的数、字符串、表(table)、函数(function)以外,还有自定义数据(userdata)。理论上,userdata 可以存放任何东西,但如果 Lua 没有宿主,它似乎不会被用到,所以可以先忽略它。
总之,Lua 的核心内容用一页纸几乎可以全部写出来(参见 Lua 5.1 白皮书索引前一页),而标准库更是简洁到了“精致”的地步(可以参见本 Blog 的 Lua 基础篇转载)。超出这一页内容,就需要发挥自己的想象力了!
函数语法糖
在 Lua 编程中,函数是一种特殊的变量。可以用 function 声明函数,使用时在变量名后紧跟 ()。示例代码如下:
f = function (x, y)
return x + y
end
print(f(1, 1)) -- 注释:print 全局函数用于在控制台打印文字,此处输出 “2”
对于习惯了 C、Java 等语言的编程者来说,上述语法可能有些怪异,就像北方人听四川人讲话,能明白但感觉拗口。Lua 为不同编程习惯的人准备了语法糖,以下是 VB/Delphi 风格的写法:
function f(x, y)
return x + y
end
print(f(1, 1)) -- 输出“2”
嵌套函数
由于 Lua 的函数本质上是普通变量,因此可以在任何地方使用,甚至可以在一个函数内部使用另一个函数。示例代码如下:
function f(x, y)
function f1(x)
return x + 1
end
return f1(x) + y
end
print(f(1, 1)) -- 显示” 3 ”
这种方式被称为“内嵌函数”,并非 Lua 独有,Javascript 也具备这种能力。
多返回值函数和无限形参
如果前面的内容让你不太适应,那么函数的以下功能或许会让你感到满意。首先是多返回值函数:
function f(x)
return x, x + 1, x + 2
end
y, y1, y2 = f(1)
print(y, y1, y2) -- 打印“ 1 2 3”
如果只想接收部分返回值,也很简单。例如,只接收第一个参数:
function f(x)
return x, x + 1, x + 2
end
y = f(1)
print(y) -- “1”
只接收第二个参数:
function f(x)
return x, x + 1, x + 2
end
_, y1 = f(1)
print(y1) -- “2”
如果有难以计数的参数需要传入函数,或者忘记传入某些参数,Lua 也能轻松应对。使用 ... 可以实现无限形参:
function f(...)
for _, x in ipairs(arg) do
print(x)
end
end
f(4, 5, 6)
-- 打印出:
-- 4
-- 5
-- 6
Lua 之所以能实现这些功能,得益于其栈式的内部存储结构和典型容器:表。
放在函数里的 OOP
下面,我们来挑战函数的极限,尝试用函数模拟面向对象编程(OOP)。以下是一个用 Lua 函数实现的“狼吃兔子”游戏示例:
function CreateAnimal(species, food, hungry, speed, living) -- 初始化动物类函数,参数依次为 种类、食物、饥饿度、速度、存活标记
local function getSpecies() return species end
local function getHungry() return hungry end
local function getSpeed() return speed end
local function getFood() return food end
local function getLiving() return living end
local function setLiving(bLive) living = bLive end -- 设置为 false ,表示该动物已死亡
local function eat(otherAnimal) -- 如果觅食主体的食物就是客体的种类,并且主体饥饿度大于 0 ,并且主体的奔跑速度大于客体,则吃下客体,客体死亡,主体饥饿度下降 1
if (food == otherAnimal.getSpecies() and hungry > 0 and speed > otherAnimal.getSpeed()) then
hungry = hungry - 1
otherAnimal.setLiving(false)
end
end
return {getSpecies = getSpecies, getLiving = getLiving, setLiving = setLiving, getSpeed = getSpeed, getFood = getFood, getHungry = getHungry, eat = eat}
end
rabbit = CreateAnimal("rabbit", "grass", 1, 10, true) -- 新建一个动物:兔子
wolf = CreateAnimal("wolf", "rabbit", 2, 20, true) -- 新建一个动物:狼
print(rabbit.getLiving(), wolf.getHungry()) -- true , 2
wolf.eat(rabbit) -- 狼吃兔子
print(rabbit.getLiving(), wolf.getHungry()) -- false , 1
在这个示例中,函数几乎实现了与类相媲美的功能。要解释上述代码的工作原理,需要用到“闭包”等比较深奥的概念。简单来说,如果假设函数传入的形参是普通的本地变量(local variables),由于使用这些变量的函数仍然存在(被 rabbit、wolf 所引用),所以这些变量不会被垃圾回收,从而发挥了私有数据(从某种意义上来说还是这几个函数的共享数据)的作用。
匿名函数
“闭包”也有实际的用途,下面的函数返回一个匿名函数,可作为计数器使用:
function createCounter()
local counter = 0
return function()
counter = counter + 1
return counter
end
end
f = createCounter()
print(f())
print(f())
-- 输出
-- 1
-- 2
table - 类的原型!
有人说 table (表类型,在 Lua 里,v = {} 就建立了一张空表)相当于一个数组、一个 C 的结构体变量、一个 Python 的 List + Dictionary,或者一个 Javascript 的 Object。其实这种说法不完全准确,数组不能有负数作为下标;结构体一旦生成,就不能往里面添加数据或函数成员;Object 也不是默认就支持 [] 的寻址操作。
以下是一个比较夸张的 table 示例:
Mary = {
name = 'Mary',
title = 'Miss.',
age = 28,
friends = {'Mike', 'John'},
getAge = function (self, asker)
r = table.foreach(self.friends, function (i, v) if (asker.name == v) then return v end end)
if (r ~= nil) then
return self.age
else
return nil
end
end
}
Mike = {name = 'Mike'}
print(Mary:getAge(Mike)) -- 输出 “28”
Tom = {name = 'Tom'}
print(Mary:getAge(Tom)) -- 输出 “nil”
这个例子展示了 Mary 小姐的一个忌讳,Mary 有两个朋友:Mike 和 John,她对别人询问她的年龄特别敏感,除非是她的朋友,否则会闭口不答。因此,当传入 Mike 给 getAge 时,能获得“28”岁的回答,而 Tom 的询问则会被无视。从这个例子可以看出,表不仅可以存储数据,还可以存储子表和函数,已经具备了完整对象容器的两大基本功能:存放数据和成员函数。
新语法糖:Mary:getAge(Mike) 与 Mary.getAge(Mary, Mike) 完全等价,让习惯 OOP 的人能找到 OOP 的感觉。
操作符重载
操作符重载一直是 C++ 引以为豪的特性,C# 继承了这一特性也增色不少。尽管它不是非常重要的特性,但能让类更像一个数据类型,从而使 C++ 部分保持了 C 简洁的风格。
Lua 本身是函数式语言,但借助 metatable (元表)这个强大的工具,实现操作符重载易如反掌。table 和 userdata 两种数据类型支持 metatable 功能。以下示例用表类型模拟复数,x 表示实部,y 表示虚部;然后把复数相加的逻辑(实部 + 实部,虚部 + 虚部)写进元表内 __add 方法中:
meta = {
__add = function(op1, op2)
op = {}
op.x = op1.x + op2.x
op.y = op1.y + op2.y
return op
end
}
a = {x = 1, y = 1}
setmetatable(a, meta)
b = {x = 3, y = 4}
c = a + b
print(c.x, c.y) -- 输出 4,5
元表是普通表变量中的一张隐藏的表,用 setmetatable 函数设置它,用 getmetatable 函数读取它。
建立自己的类
我们已经看到 table 的强大之处,但每次创建 table 都要从 {} 开始,也就是从零开始。在 OOP 时代,我们需要自己的类型,即自己的类。在建立自己的类之前,让我们深入了解一下 metatable 这个 Lua 的“百宝箱”。
metatable 虽然是普通的表,但却是 Lua 内部系统直接认识的表。例如,Lua 解析 + 运算符时,会从相关操作数 a (或者 b)的元表中查找 __add 字段(术语叫“事件”)来寻求处理方法(因为 Lua 自己不知道如何把两张表相加)。这类方法被称为“元方法”(metamethod)。
除了与操作符重载有关的元方法外,要建立自己的类,还需要认识另一个元方法 __index。当表格搜寻成员未果时,Lua 会触发它,__index 所指向的元方法返回的结果将成为搜寻结果。__index 元方法还可以直接指向一张表。
下面通过实例来看一下。假设我们正在编制一个复数计算程序,需要定义许多复数,实现一个“永久”的复数类型 (complex)会很方便。可以这样写 Lua 代码:
complex = {
__add = function(op1, op2)
op = {}
op.x = op1.x + op2.x
op.y = op1.y + op2.y
setmetatable(op, complex)
return op
end,
create = function(o)
o = o or {}
setmetatable(o, complex)
return o
end
}
a = complex.create{x = 1, y = 1}
b = complex.create{x = 3, y = 4}
c = a + b
print(c.x, c.y) -- 输出 4,5
在该代码中,利用 complex.create 创建复数,只需输入复数的实部和虚部的值,然后可以对复数使用加法运算,加法运算的结果(这里指 c)也是复数。我们说一个表是复数(表),不仅要求该表具有 x、y 两项数值,还要求具有复数的行为,如“相加”、“相减”、“相乘”等。
新语法糖:在给函数传递参数时,func({1, 2, 3}) 与 func{1, 2, 3} 等价。
实现类的单继承
一些人可能羡慕具有完整 OOP 语法的脚本语言,如 Ruby。Lua 尽管被设计用于函数式编程,但要支持 OOP 语法并不难,仅用前面提到的关于 metatable (元表)的相关知识就能实现。
我们建立了如下的 Lua 类继承语法:
- 一切都继承自
object。 - 要直接从
object产生新类,使用语句:NewClass = object(),相当于 Ruby 的NewClass < object。 - 要从
NewClass产生派生类,使用语句:SubClass = NewClass()。 - 要为某个类,如
SubClass书写构造函数,使用语法:function SubClass:constructor(p1, p2, ...) functionbody end。 - 要为某个类新建一个函数,使用语法:
function SubClass:func(p1, p2, ...) functionbody end。 - 要按照 Java 的样式定义一个新类,按照如下格式书写:
SubClass = NewClass{ data1 = 1, data2 = 2, function constructor(self, p1, p2) ... end, function func(self, p1, p2) ... end } - 要新建一个类的实例,相当于 Java 中的
Class1 c = new Class1(),可以这样书写:c = new(Class1)。 - 要调用带参数的构造函数,相当于 Java 中的
Class1 c = new Class1(p1),可以这样书写:c = new(Class1, p1)。 - 也可以利用表直接初始化新的类实例:
c = new(Class1, {newvalue = 9})。
为了实现这样简洁的语法,只需要实现根类 object 和具有语法糖作用的 new 函数,代码如下:
object = {}
setmetatable(object, {
__call = function (super, init)
local function createClass(super, init)
local class = init or {}
class.super = super
class.constructor = false
class.instance = function(self, o, ...)
local obj = {}
-- copy from self( class prototype)
for k, v in pairs(self) do
obj[k] = v
end
if (type(o) == "table") then
-- copy from o
for k, v in pairs(o) do
obj[k] = v
end
else
table.insert(arg, 1, o)
end
local function call_constructor(c, ...)
if c.super then
call_constructor(c.super, ...)
end
if c.constructor then
c.constructor(obj, ...)
end
end
call_constructor(class, unpack(arg))
setmetatable(obj, {__index = self.super})
return obj
end
setmetatable(class, {
__call = createClass,
__index = class.super
})
return class
end
return createClass(super, init)
end
})
function new (class, init, ...)
return class:instance(init, ...)
end
这里用到了 __call 元方法,用于自定义“调用”操作时表的行为。
以下是测试我们建立的继承语法的代码及输出(假设定义 object 的代码放在 inheritance.lua 文件中):
dofile("inheritance.lua") -- 定义继承树的根类 object
classA = object() -- classA 从 object 派生
classB = classA() -- classB 从 classA 派生
function classA:constructor(x) -- 定义 classA 的构造函数
print("classA constructor")
self.x = x
end
function classA:print_x() -- 定义一个成员函数 classA:print_x
print(self.x)
end
function classA:hello() -- 定义另一个成员函数 classA:hello
print("hello classA")
end
function classB:constructor() -- 定义 classB 的构造函数
print("classB constructor")
end
function classB:hello() -- 重载 classA:hello 为 classB:hello
print("hello classB")
self.super.hello() -- 调用基类的 hello 方法
end
x = new(classB, 1) -- 新建一个 classB 的实例
x:print_x() -- 执行 print_x 方法,该方法实际是在基类 classA 中定义
x:hello() -- 调用 classB 的重载方法 hello
输出结果:
classA constructor
classB constructor
1
hello classB
hello classA
实现类的多重继承
在类的单继承基础上,只需对 object 稍加修改,Lua 就能支持多重继承,从而超越 Ruby 等大多数脚本语言,获得与 C++ 相似的能力。
除单继承的语法外,我们定义 Lua 多重继承的几个新语法:
- 如果
Class1从Class2和Class3继承,使用语句:Class1 = Class2() + Class3()。 - 也可以同时定义成员数据及函数,如下所示:
Class1 = Class2() + Class3{ data1 = 1, data2 = 2, function constructor(self, p1, p2) ... end, function func(self, p1, p2) ... end }要实现上述语法,只需为
object重新定义__index和__add两个元方法,完整的object定义如下:object = {}
setmetatable(object, { __call = function (super, init) local function createClass(super, init) local class = init or {} class.super = {super} class.constructor = false class.instance = function(self, o, ...) local obj = {} -- copy from self( class prototype) for k, v in pairs(self) do obj[k] = v end if (type(o) == "table") then -- copy from o for k, v in pairs(o) do obj[k] = v end else table.insert(arg, 1, o) end
local function callconstructor(c, ...) if c.super then for , v in ipairs(c.super) do call_constructor(v, ...) end end if c.constructor then c.constructor(obj, ...) end end call_constructor(self, unpack(arg))
setmetatable(obj, { __index = function (t, k) for _, v in ipairs(t.super) do local nv = v[k] if (nv ~= nil) then return nv end end end }) return obj end
setmetatable(class, { call = createClass, index = function (t, k) for _, v in ipairs(t.super) do local nv = v[k] if (nv ~= nil) then return nv end end end, __add = function (class1, class2) -- merge super classes local sum = {} for k, v in pairs(class1) do sum[k] = v end for k, v in pairs(class2) do sum[k] = v end local supersum = {} for , v in ipairs(class1.super) do table.insert(supersum, v) end for , v in ipairs(class2.super) do table.insert(super_sum, v) end sum.super = super_sum return sum end }) return class end return createClass(super, init) end })
function new (class, init, ...) return class:instance(init, ...) end
同样,测试结果令人满意(假设上述 `object` 的代码放置在 `m_inheritance.lua` 文件内):
dofile("m_inheritance.lua")
classA = object{x = 1, y = 1} -- classA 由 object 直接派生 classB = classA{x = 2, y = 2} -- classB 由 classA 直接派生 classC = object{z = 0.4} -- classC 由 object 直接派生 usefulClass = classB() + classC{x = 3, y = 3} -- usefulClass 多重继承于 classB 和 classC
function classA:constructor() -- 定义 classA 的构造函数 print("classA constructor") end
function classA:print_x() -- 定义一个成员函数 classA:print_x print("x:" .. self.x) end
function classA:hello() -- 定义另一个成员函数 classA:hello print("classA_hello") print("y:" .. self.y) end
function classB:constructor() -- 定义 classB 的构造函数 print("classB constructor") end
function classB:hello() -- classB:hello 重载 classA:hello print("classB_hello") print("y:" .. self.y) classA.hello(self) end
function classC:constructor() -- 定义 classC 的构造函数 print("classC constructor") end
function classC:print_z() -- 定义 classC 的 print_z 函数 print("z:" .. self.z) end
function usefulClass:constructor() -- 定义 usefulClass 的构造函数 print("usefulClass_constructor") end
x = new(usefulClass) -- 新建 usefulClass 的实例 x:print_x() -- 调用 print_x,实际是调用 classA:print_x x:hello() -- 调用 hello,实际是调用 classB:hello x:print_z() -- 调用 print_z,实际是调用 classC:print_z
结果输出为:
classA constructor classB constructor classC constructor usefulClass_constructor x:3 classB_hello y:3 classA_hello y:3 z:0.4
看到这些,你是否也同意 Lua 算得上一门艺术呢?相信我,奇迹仅仅就是你,再加上 Lua!