文章目录
共16624字,阅读时长约50分钟。
控制结构 if
控制结构被用来在代码中做决定,它们基于布尔值(Boolean)控制代码的路径。Lua提供了if语句来实现。if后跟着一个布尔条件,然后跟着一个then/end块,这个块只有在布尔条件为真时才会执行。
elseif
你可能想要做一个复杂的决定,而不止是if语句。例如,你想要在一个字符串(string)的长度小于3的时候做一件事,大于3的时候做另一件事。这可以用elseif语句实现,它是由else和if组合的。
elseif后跟着一个布尔条件,然后跟着一个then/end块。elseif语句需要在if语句体后,end前。
name = "Jack"
if #name <= 3 then -- name的长度是4,所以这个then块内的代码不会执行
print("name length <= 3")
elseif #name > 3 then -- 这个elseif的then块会执行
print("name length > 3")
end
你可以添加很多elseif语句添加到一个if语句后。
一个if控制结构内,一旦某个if/elseif的布尔值为真,则执行其then块,其它的elseif不会再进行判断,then块也不会被执行。
print("input a number")
x = io.read()
-- 如果输入的不是数字,会执行下方的then块,否则继续向下判断
if type(x) ~= "number" then
print("not number")
-- 从控制台接收输入时会返回string,需要用内置函数tonumber转换为number
elseif tonumber(x) < 10 then
print("x < 10")
elseif tonumber(x) <100 then
print("x >= 10 and x < 100")
end
else
如果if/elseif 的参数没有一个为真,那就什么也不会执行。如果想要在if/elseif 没有参数为真时执行一个代码块,可以使用else语句。
x = io.read()
if x < 10 then
print("x < 10")
elseif x <100 then
print("x >= 10,and x < 100")
else
print("x >= 100")
end
只能有一个else语句,它必须在if/elseif 块的最后。else语句前可以没有elseif语句,直接跟在if语句后。
x = io.read()
if x < 10 then
print("x < 10")
else
print("x >= 10")
end
嵌套if语句
像块一样,if语句可以嵌套。它的范围规则和它被嵌套的块的范围一样。
print("input a number")
local x = tonumber(io.read())
if x < 10 then
print("more number")
local y = tonumber(io.read())
if x < y then
print("latter large")
else
print("previous large")
end
else
-- y为nil,因为y的范围是在上一个if块内
print("y: " .. tostring(y))
end
循环
一个代码块可以通过循环多次执行。Lua提供三种循环,包括while,repeat,for。
while循环
while循环以while关键字开始,后面跟着布尔条件和一个do/end块。只要布尔条件为真,代码块就会一直执行。
x = 10
-- 1.进入while语句,判断一次条件(这里是x>0)
-- 2.为真则运行一次do/end块内的代码,然后回到1
-- 3.为假则结束while循环
while x > 0 do
print("x: " .. x)
x = x - 1
end
无限循环
如果循环的条件永远不会为假,那这个循环不会结束,会一直执行。
while true do
print("forever loop")
end
真实可能发生的情况像这样:
x = 10
while x > 0 do
print("x: " .. x)
x = x + 1
end
这个循环会一直执行,因为x一直增加1,不会小于0。还有一个原因是,Lua的number溢出后不会回到0,而是用inf来替代(inf是大于0的)。
把上面的代码中x = x + 1换成 x = x * 2,x会很快达到inf。如下:
如果你遇到无限循环,在终端按下Ctrl + C,可以中断正在运行的进程。
中断循环
循环可能需要在执行时退出,在布尔条件被判定为假之前。或者是循环的一个分支,遇到一个异常而退出。在循环的任何节点,可以用break关键字马上退出循环。
x = 0
while x < 10 do
print("x: " .. x)
if x == 5 then -- x为5时,结束循环
break
end
x = x + 1
end
Lua没有实现其它语言有的continue关键字。
continue 作用:
当执行到continue时,跳过执行循环体内剩下的语句,直接进入布尔条件进行判断。
return语句也能中断一个循环。如果循环在函数内,中断函数;如果循环在文件范围内,中断文件执行。
repeat until循环
repeat until循环和while循环有一些不同。while循环的先判断一次布尔条件,再决定是否执行循环do/end块。
repeat until循环相反。repeat循环块先执行一次,然后进行布尔条件判断。这保证了循环块至少执行一次。
repeat until循环以repeat关键字开始,后面跟着循环块。这个块以until结束,后面跟着决定这个循环是否执行的布尔条件,条件为真时结束执行,为假时继续执行。
x = 10
repeat
print("repeat loop")
until x > 2
for循环
Lua有两种循环,数值型和通用型。通用型被用来迭代集合、表、对象。现在介绍数值型。
数值型for循环由for关键字,三个表达式,一个do/end块组成。三个表达式分别是初始化,最后,步骤表达式。每个表达式用逗号分隔:
for variable = initial_exp, final_exp, step_exp do
-- Chunk
end
初始化表达式的结果应该是数字的,它会被赋值给variable(一个变量,被称为计数器),可在for循环范围中使用。这个变量在步骤表达式中增加或减少。
例如,下面的循环从0到10,每次迭代计数器加1。
for i = 0, 10, 1 do
print( i )
end
修改步骤表达式的值可以改变每次迭代计数器增加的值。如下,每次迭代计数器加2:
for i = 0, 10, 2 do
print( i )
end
数值for循环不是只能增加,也可以减少。想要减少,需要初始化表达式小于最终表达式,并且步骤表达式为负数。
for i = 10, 0, -1 do
print( i )
end
加1是很常用的for循环,所以Lua提供了一个简便用法。如果你只给了初始化,最终表达式,Lua会假设你的步骤表达式是1。(不能是减1)
for i = 0, 10 do
print( i )
end
嵌套循环
循环是可以嵌套的,不同的循环可以相互嵌套。
如果有一个break语句,它会中断最靠近这条语句的内层循环。
for i = 0, 10 do
local j = 0
while j< 10 do
print("i: " .. i)
-- 每次j为2时,中断while循环
if j == 2 then
break
end
j = j + 1
end
end
表和对象
表是Lua唯一提供的数据结构。表可以实现其它数据结构,如列表、队列、栈。
Lua不是面向对象的,这个语言不支持对象。但是使用表、元表(meta-tables)可以实现对象。
介绍表
表可以用类来扩展Lua语言,或者甚至是一个基于类的混合系统。
表基本上是字典或数字。一个表是一个键值对集合。如果键是数字的,表就是数组。如果键是非数字的或混合的,表就是一个字典。除了nil,任何东西都可以作为键,值可以是任何东西,包括nil。
创建表
Lua创建表只需要用花括号({},或者说大括号)。创建表后需要赋值给一个变量,如果不赋值给变量,那就无法访问它。
tbl = {}
print(type(tbl))
存储值
表是一个关系型的数据结构,可以存储值。它很像其它语言的字典。
tbl = {}
tbl["x"] = 20
i = "x"
print(tbl[i])
print(tbl["x"])
表的键甚至可以是表。
如果你使用了字符串作为键,那可以用点号.语法,这个语法允许你比用中扩号更简单地访问值。
tbl = {}
tbl["x"] = 20
print(tbl.x)
最后一件事是:存在表中的值有默认值。如果没有给一个键赋值,那默认为nil。
tbl = {}
print(tbl.z) -- nil
表构造器
你可以在创建表时用表构造器赋值。只要在花括号内写键值对。键不需要引号(如果有引号则报错),它们默认为字符串。
colors = {
red = "#ff0000",
green = "00ff00"
}
print("red: " .. colors.red)
print("green: " .. colors["green"])
如果使用方括号计法,可以在构造器中使用非字符串键。
colors = { ["red"] = "#ff0000", [2] = "#00ff00"}
print("red: " .. colors.red)
print("red: " .. colors[2])
表是引用
整数和其它原始数据类型赋值是值,表赋值是引用。什么意思呢?
对原始数据类型,如果把变量赋值给其它变量,那每个变量都有自己的副本,可以独立修改。
x = 10
y = x -- 把x的值10赋值给了y
x = 15 -- 不会改变y的值
print(x) -- 15
print(y) -- 10
如果给变量赋值为引用,那多个变量有相同的引用,改变其中一个会改变所有这个引用变量的数据。
a = {}
b = a -- 把a引用的表赋值给了b,a和b指向同一个表
b.x = 10
a.x = 20
a.y = 30
print(b.x) -- 20
print(b.y) -- 30
a = nil
b = nil
最后两行是把变量引用设为nil,不再指向表。当表没有被任何变量引用时,可以被垃圾回收(farbage collection, gc)。垃圾回收是Lua的内存释放机制,可以重用内存。一旦一个表不需要了,所有引用都应该设为nil。
数组
Lua用表实现数组。
数组是内存上连续的块。如果满足以下要求,Lua可能会使用线性的内存块:
- 只有数字索引
- 索引从1开始
- 至少一半的索引不是nil
arr = {}
arr[1] = "x"
arr[2] = "y"
print(arr[1])
数组构造器
表有一个创建数组的构造器。要使用这个构造器,你需要输入值,不需要键在花括号中去创建一个表。第一个值将会有一个键为1,后面的键为前一个键加1。
arr = {"mon", "tues", "wednes"}
for i = 1, 3 do
print(arr[i])
end
数组是从1开始的
Lua的数组是从1开始的。这意味着语言假设第一个元素的索引为1,这和很多语言程0开始不同。
Lua是一个宽松的语言,你可以直接把值放在键为0的索引上。但是这可能会导致一些难以发现的问题,如在判断数组长度时。
vectot = {[0] = "x", "y"}
print(vector[0]) -- x
print(vector[1]) -- y
稀疏数组
数组可以是稀疏的,可以留一些洞在数组中。也就是一些索引可以不赋值,不赋值的默认为nil。
arr = {}
arr[1] = "x"
arr[4] = "y"
for i = 1, 4 do
print(arr[i])
end
这些洞有时会导致一些问题。
数组大小
就像用#操作符获取string的长度一样,可以用#获取数组长度。因为#会返回表的大小。可以用这个操作符遍历(访问其中的每一个元素)一个有动态大小的数组。
arr = { "a", "b", "c", "d" }
print(#arr) -- 4
for i = 1, #arr do
print(arr[i])
end
#只会从1开始计算数组长度,如果把元素放在0,则不会被计算在数组长度中。
尝试获取数组长度会有迷惑性。因为#在连续读到两个nil时判断数组结束。
arr = { "a" , [3] = "c", [6] = "f", [7] = "g"}
print(#arr) -- 3,索引2未赋值
-- 4,5连续两个nil值,不再计算后续元素
for i = 1, #arr do
print(arr[i]) -- a, nil, d
end
因为这很反直觉, 所以用#获取数组长度是不可靠的。更好的方式是迭代。
多维数组
Lua不是天然支持多维数组的。在Lua中创建一个多维数组需要创建一个数组的数组(表的表)。要对数组中的每一个元素声明一个新的数组。
rows = 4
cols = 4
matrix = {}
for i = 1, rows do
matrix[i] = {}
for j = 1, cols do
matrix[i][j] = i * j
end
end
迭代
在Lua中,可以用通用for循环迭代表或数组。
通用for循环由for关键字,跟着一个变量列表(两个变量),in关键字,表达式列表,最后是do/end块。
变量列表的第一个变量是控制变量。在每一个循环中,for循环会判断表达式列表,把结果赋值给变量列表。循环会保持执行直到控制变量为nil。
for循环的表达式列表通常由单个迭代器函数组成。Lua提供了很多内置迭代器处理不同任务。
理解键值对
键值对(pairs)迭代器函数被用来遍历一个表。这个函数返回两个变量,让我们称它为k和v。k包含了直到迭代结束的键,v包含了值。
vector = { x = 11, y = 22, z = 33}
for k, v in pairs(vector) do
print("key: " .. k .. ", value: " .. v)
end
理解索引键值对
索引键值对(ipairs)函数被用来迭代数组。它返回两个变量,让我们称之为i和v。i保存了数组迭代完的索引,v保存元素。
vector = { 1, 2, [4] = 4, [7] = 7 }
for i, v in ipairs(vector) do
print("index: " .. i .. ", value: " .. v)
end
得到如下输出:
ipairs遇到第一个索引为nil就结束迭代。
如果想迭代数组全部内容,需要用pairs函数。将上述代码for循环中的ipairs改为pairs,得到如下输出:
闭包
闭包(closures)会捕获块的封闭状态。一个很好的例子是一个函数返回一个匿名函数,这个匿名函数能看到这个封闭函数的本地变量。
返回一个匿名函数就创建了一个闭包。
function thisNum()
local curNum = 1
return function()
print(curNum)
end
end
this = thisNum()
this() -- 1
this() -- 1
这个thisNum函数通常被称为工厂。每次调用这个函数会创建一个新的闭包。然后你可以调用新的结果函数。
function nextNum()
local cur = 0
return function()
cur = cur + 1
print(cur)
end
end
next = nextNum()
next() -- 1
next() -- 2
new = nextNum()
new() -- 1
迭代器函数
闭包能创建一个自定义迭代器。这节你将创建一个自定义迭代器遍历数组,只返回存储在每个索引中的值,不返回索引。当这个迭代器第一次碰到nil变量就中断。这个自定义迭代器和通用型for循环配合。通用型for循环保存三个变量:
- 这个迭代器函数,也就是你的闭包
- 恒定的状态
- 一个控制变量,迭代器函数返回的第一个变量
通用for循环执行时,它判断in关键字后的表达式。这个表达式应该产生由for循环保存的三个值:迭代器函数,不变状态,控制变量的初始值。如果只返回一个值(这种情况下,是一个迭代器函数),另两个变量为nil:
days = {"mon", "tues", "wednes" }
function walk(array)
local index = 0
return function()
index = index + 1
return array[index]
end
end
for day in walk(days) do
print(day) -- mon, tues, wednes
end
这个代码中的walk函数就是迭代器工厂,它需要一个参数,返回一个闭包。这个闭包返回数组每个索引对应的值。当返回nil时,通用for循环就停止了。数组参数和索引在这个代码中都是闭包的本地变量。这意味着一旦你执行了初始的walk函数,这个函数的本地变量,只有被这个函数创建的闭包能获取。
元表
在Lua中,元表可以修改表的行为。任何表可以作为元表,任何表可以有元表。甚至元表也能有元表。元表用元方法修改表的行为。这些元方法是能影响表行为的,特定名字的函数。
下面以一个具体例子讲解。
首先,创建一个叫meta的表,这个表有一个名为__add(两个下划线_加上add)的函数,这是一个预留函数,接收两个参数。
左边的参数是:是有一个字段(成员变量)名为value的表,右边的是一个数字:
meta = {}
-- 添加一个__add方法到meta表中
meta.__add = function(left, right)
-- left被假定为一个表
return left.value + right
end
下一步,创建一个称为container的表。
container = {
value = 5
}
尝试把4加到container表中,会报错。因为不能把表和数字相加。如下:
```lua
container= { value = 5 }
print(container + 4)
通过添加一个有__add元方法的元表到container表,上述代码就能运行了。setmetatable函数用来赋予一个元表。
setmetatable(container, meta) -- set meata table
result = container + 4
print(result) -- 9
当Lua尝试加+任何东西到一个表,它会先判断这个表是否有一个元表,如果有,并且这个元表有一个__add元方法,这个方法会被用来执行这次加法。这意味着下面的代码:
result = container + 4
事实上是这样执行的:
result = meta.__add(container, 4)
元表可能是Lua最强大的特性,但是也容易让人困惑。
setmetatable
当创建一个新表,Lua不会给它一个元表,表的元表默认为nil。可以用setmetatable赋予表一个元表。这个函数接收两个参数,都是表,第一个是目标表,第二个是新的元表。
你可以设置任何表为任何表的元表,包括它自己。
container = {
value = 5,
__add = function (l, r)
return l.value + r.value
end
}
setmetatable(container, container)
result = container + container
print(result) -- 10
这个例子中,container表有一个__add函数,设置这个container表为它自己的元表。使下面的语句:
result = container + container
实际上是这样执行的:
result = container.__add(container.value, container.value)
getmetatable
作为setmetatable的互补,可以使用getmetatable方法获取一个表的元表,getmetatable接收一个参数,一个表。如果提供的表有一个元表,这个元表会被返回,否则返回nil:
x = {}
y = {}
setmetatable(x, y)
print(getmetatable(x)) -- x的元表是y,输出y表的引用地址
print(getmetatable(y)) -- nil,y无元表
__index
如果访问表不存在的字段,会返回nil。但是,如果被访问的表有一个,有__index元方法的元表,这个元方法会被调用。
x = {
foo = "bar"
}
y = { }
z = {
hello = "world z",
__index = function(table, key)
return z[key]
end
}
w = {
__index = function(table, key)
if key == "hello" then
return "inline hello"
end
return nil
end
}
print(x.hello) -- nil
print(y.hello) -- nil
setmetatable(x, z)
setmetatable(y, w)
print(x.foo) -- bar
print(x.hello) -- world z
print(y.foo) -- nil
print(y.hello) -- inline hello
上述代码中,当打印x.hello时,Lua看到x表没有字段hello,但是有一个有__idnex元方法的元表。Lua把x表和hello作为参数传递给元表z的__index元方法,它返回z的hello字段。
同理,打印y.foo, y.hello时,y没有这两个字段,会传递参数给元表w的元方法__index,对于foo,返回nil;对于hello,返回inline hello。
__newindex
__newindex元方法是__index元方法的互补。__index接收表缺失的键,__newindex赋值缺失键的值。__newindex接收三个参数:
- 被操作的表
- 缺失的键
- 赋予的值
x = {}
y = {}
z= {
__index = function(table, key)
return z[key]
end,
__newindex = function(table, key, value)
z[key] = value
end
}
setmetatable(x, z)
setmetatable(y, z)
x.foo = "bar"
print(x.foo) -- bar
print(y.foo) -- bar
print(z.foo) -- bar
在这个例子中,x,y都有元表z,z有__index元方法,当一个不存在的键被访问的时候会返回z表的键值。它还有一个__newindex元方法,当设置一个x,y没有的键的值时,会在z表内设置。
当执行x.foo = "bar"时,Lua看到x没有foo成员,但是它有一个有__newindex元方法的元表。Lua传递x表、foo变量、bar值给元表z的__newindex元方法,这个元方法在z表内设置一个键值对。
当打印x或y的foo字段值时,__index被调用返回z的foo字段值。
rawset, rawget
当一个表有__index和__newindex元方法时,也可以直接给表设值。可以通过rawset(原始设值)和rawget(原始赋值)函数,避开元表直接给表设值。
rawget接收两个参数,被访问的表,被访问的键。
rawget(table, key)
rawset接收三个函数,被访问的表,被设值的键,值。
rawset(table, key, value)
x = {}
y = {}
z= {
__index = function(table, key)
return z[key]
end,
__newindex = function(table, key, value)
z[key] = value
end
}
setmetatable(x, z)
setmetatable(y, z)
x.foo = "meta foo" -- x无foo键,调用元方法设值
rawset(x, "foo", "raw") -- 避开元方法,直接给x表设值
print(x.foo) -- raw,x表有foo键
print(y.foo)-- meta foo, y表没有foo,调用__index
__call
__call元方法让一个表可以被用作一个函数。在其它语言,这个构造被称为functor,Lua称之为functable。
__call元方法接收可变数量的参数。第一个参数是作为函数的表,接着是这个functable可接受的,任意数量的变量。
tbl = {
__call = function(table, val1, val2)
return "Hello " .. (val1 + val2)
end
}
setmetatable(tbl, tbl)
message = tbl(2, 3) -- 像调用函数一样调用表
print(message) -- Hello 5
你可以传递更少或更多的参数给functable,缺失的参数值为nil,多余的参数被抛弃。
操作符
Lua为元表提供了很多操作符,如加+,减-。通常,在两个表执行元方法时,会按照下面的规则执行:
- 表达式左边有,执行左边的
- 表达式左边没有,执行右边的
数学操作符
下面的都是二元操作符,元方法对这些操作符都接收两个参数,式子左边和右边的。每个函数都是下面的格式:
meta = {
__<method> = function(left, right)
-- Do stuff
end
}
把上方__<method>替换为下方对应写法。
- __add: 加法,写作 表 + 对象 或 对象 + 表。下同
- __sub: 减法
- __mul: 乘法
- __div: 除法
- __mod: 取模、取余
- __pow: 幂
等式操作符
有三个元方法比较相等性。这些元方法接收两个参数:
- __eq: 检查相等,当 表1 == 表2时调用。下同
- __lt: 检查小于,<
- __le: 检查小于等于, <=
__eq元方法用来检测相等性,例如,表达式 table1 == table2实际上执行 getmetatable(table1).__eq(table1, table2)。__eq元方法还能检查不等性,表达式 table1 ~= table2会被判断为 not (table1 == table2),实际上就会执行 not getmetatable(table1).__eq(table1,table2)。
__lt操作符用来检查参数是否小于另一个。也可以交换一下参数,用来检查是否大于。__le同理,交换参数可以检查是否大于等于。
其它操作符
还有一些操作符对应的元方法可能会有用。这些操作符主要是像字符串操作:
- __tostring: 被期待返回代表一个表的字符串。接收一个参数,要被转换成字符串的表。
- __len: 被期待返回表的长度当写 #table 时。接收想要获取长度的表。
- __concat: 被期待连接两个表,当写 table1 … table2 时。这个函数不必返回一个字符串,但是为了一致性应该返回。这个函数接收两个参数,要被连接的表。
对象
Lua不是面向对象(Object-Oriented Programming, OOP)语言,它提供了便利允许我们实现对象系统。
类(class) 是OOP的核心组成部分。类是创建多个对象的单一模板。经常把类比作房子的蓝图。蓝图定义了房子面积和布局,能通过单一蓝图生产大量房子。
同一蓝图创建出来的房子是独立的。房子主人对房子做的改变不会影响其它房子。蓝图的改变会影响这个蓝图创建的所有房子。
Lua中使用表创建类系统。元表能创建基于原型的对象系统,类似于JavaScript。
类
类(Class)定义了每个创建出来的对象(Object)有的变量和方法。对于每个对象,它们都有变量的副本,互相独立。
例如,如果我们要用类表示游戏里的敌人,假设每个敌人都有血量、攻击力、防御值:
Enemy = {}
Enemy.health = 200
Enemy.attack = 10
Enemy.defense = 20
敌人表会被当做所有敌人对象的蓝图。在OOP中,需要构造器(函数)。构造器就是实例化一个新对象的方法。在Lua中,构造器就是做特殊事情的函数。,为了方便,通常称之为new。对于Enemy来说,就是Enemy.new。
这个enemy对象的构造器将接受两个参数。第一个是Enemy表,构造器需要知道需要实例化对象的类。第二个是可选的表,代表了要创建的对象。如果不提供,会创建一个新表(对象)。
-- 为了方便,第一个参数叫self
Enemy.new = function(self, object)
-- 使用提供的object表,如果没有,新建一个空表
object = object or {}
setmetatable(object, self)
self.__index = self
return object
end
这段代码设置Enemy表为新对象的元表。之后,对象的__index元方法也是Enemy表。这样,无论何时访问新对象时,如果字段不存在,就会返回Enemy的字段副本。
这个构造器Enemy.new,可以像函数一样调用。第一个参数是强制的,第二个是可选的:
grunt = Enemy.new(Enemy)
miniBoss = Enemy.new(Enemy)
boss = Enemy.new(Enemy, {health = 500, defense = 100 })
miniBoss.health = 250
print("grunt.health: " .. grunt.health)
-- grunt.health: 200,grunt不存在health键,返回Enemy的
print("miniBoss.health: " .. miniBoss.health)
-- miniBoss.health: 250
print("boss.health: " .. boss.health)
-- boss.health: 500
下面给Enemy类增加一个受击hit函数。当玩家攻击敌人,造成伤害时调用。这个函数接收两个参数,第一个代表被攻击的敌人(如grunt、boss),第二个是收到多少伤害:
Enemy.hit = function(self, damage)
damage = damage - self.defense
if damage < 0 then
damage = 0
end
self.health = self.health - damage
end
尽管hit函数是Enemy表的一个字段,但我们永远不会直接引用Enemy.health。相反,我们使用函数的第一个参数,决定哪个敌人被攻击。
因为这个函数是属于Enemy的,所以调用Enemy.hit。不像构造器,这个函数的第一个参数是实例而不是Enemy表本身。
Enemy.hit(boss, 50)
print("boss.health: " .. boss.health)
enemy表只设置了它的__index元方法,而没有__newindex元方法。这意味着在hit函数执行前,grunt.health实际上返回的是Enemy的health字段。但是hit函数有这么一行代码:
self.health = sele.health - damage
当这个函数被调用,self指向grunt,一个新的叫health的字段被添加到grunt中。在这行代码执行后,grunt有自己的health字段,而不再返回Enemy.health。这都是因为设置了__index元方法,而没有设置__newindex元方法。
:操作符
前面已经实现了许多面向对象特性,这些代码技术上是正确的,但是每个enemy对象都不得不用存在Enemy类的函数。
Lua提供了一些语法糖(代码更简单的写法)在对象上调用函数,那就是冒号(:)操作符。
这个操作符把第一个参数提供给函数。第一个参数,通常称为self。你可以叫它任何东西,但是为了阅读和维护,还是遵循常规好。
Vector = {
x = 0,
y = 1,
z = 0
}
Vector.new = function(self, object)
object = object or {}
setmetatable(object, self)
self.__index = self
return object
end
Vector.print = function(self)
print("x: " .. self.x .. ", y: " .. self.y .. " z: " .. self.z)
end
velocity = Vector:new()
momentum = Vector:new({x = 20, z = 10})
Vector.print(velocity) -- x: 0 y: 1 z: 0
Vector.print(momentum) -- x: 20 y: 1 z:10
velocity:print() -- x: 0 y: 1 z: 0
momentum:print() -- x: 20 y: 1 z: 10
在使用冒号操作符之前,每个函数都需要调用Enemy表的函数,并且把Enemy作为第一个参数传递,像这样:
grunt = Enemy.new(Enemy)
boss = Enemy.new(Enemy, {health = 500, defense = 100})
使用冒号操作符之后,就简单多了。因为Enemy表在冒号左边,它被自动提供给第一个参数,在这个代码中叫self(实际上叫什么都行)。前面的代码可以写成这样:
grunt = Enemy:new()
boss = Enemy:new({health = 500, defense = 100})
使用冒号操作符让调用成员函数更简单。如hit函数,在使用冒号操作符之前:
Enemy.hit(boss, 50)
Enemy.hit(grunt, 55)
冒号操作符把左边的无论是什么,提供给函数的第一个参数。这就可以调用对象的方法而不是类的:
boss:hit(50)
grunt:hit(55)
这段代码能运行是因为语法糖。Lua知道boss表没有hit方法,但是它有__index元方法,这个元方法指向Enemy表,它尝试使用元方法调用函数。
对象内的表
上述对象系统只是说了类包含值的情况,它会在类包含引用(比如表)的时候崩溃。这是因为表是以引用传递的,而不是值传递。当__idnex返回一个包含表的类时,其中没有新的表副本,而是所有对象共享的表引用。
Character = {}
Character.position = {x = 10, y = 20, z = 30}
Character.new = function(self, object)
object = object or {}
setmetatable(object, self)
self.__index = self
return object
end
player1 = Character:new()
player2 = Character:new()
player1.position.x = 0
print(player2.position.x) -- 0
player1对position表的修改影响了player2。二者引用的是同一个表。
为了使Character的每个实例都有一个position的唯一副本。最好的方式是在构造器中添加这个表,在__index索引元方法被赋值前。new方法必须对表的每个实例的成员赋值,在设置元表之前。
Character.new = function(self, object)
object = object or {}
-- 先记录object.position表
tmp = object.position
-- 把obejct对position的引用去掉
object.position = {}
-- 如果tmp存在,则把旧的值赋值给新对象
if tmp then
object.position.x = tmp.x
object.position.y = tmp.y
object.position.z = tmp.z
-- 如果旧值不存在,从Character赋值
else
object.position.x = Character.position.x
object.position.y = Character.position.y
object.position.z = Character.position.z
end
setmetatable(object, self)
self.__index = self
return object
end
继承
OOP的一个关键概念是继承。继承允许一个类从另一个类继承函数。被继承的是父类,继承的是子类。换种说法是子类源自父类。
子类可以访问父类的所有函数和变量。子类除了可以访问,还可以提供父类任何函数自己的实现。子类重新实现父类函数被称为函数重写。
继承不必是线性的。单继承允许一个对象从一个祖先继承函数。多继承允许一个子类有多个父类。
多继承会导致很多继承问题。最大的问题是菱形问题:有两个基类,我们叫它A和B,这两个类都有函数foo,那子类访问foo时,应该访问A的还是B的呢?
单继承
使用单继承,任何类能从唯一父类继承函数。要实现单继承,要通过一个已经存在的构造器创建一个新类。
让我们从一个动物(Animal)类开始,以它为基类:
Animal = { sound = "" }
Animal.new = function(self, object)
object = object or {}
setmetatable(object, self)
self.__index = self
return object
end
Animal.MakeSound = function(self)
print(self.sound)
end
不是所有的动物都打算叫一样的声音。扩展Animal,创建两个新类,cat,dog:
Dog = Animal:new()
Dog.sound = "wang wang wang"
Cat = Animal:new()
Cat.sound = "miao miao miao"
Cat.angry = false
Cat.MakeSound = function(self)
if self.angry then
print("haqi haqi haqi")
else
print(self.sound)
end
end
上面的代码中,Dog是一个新类,而不是一个Animal实例。虽然听起来有点奇怪,因为语法上是一样的。Dog重写了sound变量。Cat提供了MakeSound自己的实现,这让猫能发出不同的声音。
不管猫还是狗,我们把它们都当做动物。我们知道不管是猫还是狗的对象都是动物。我们也知道所有动物都有一个发出声音的函数(MakeSound)。下面我们创建一堆动物,让它们叫:
animals = { Cat:new(), Dog:new(), Cat:new() }
animals[1].angry = true
-- 下面的代码输出
-- haqi haqi haqi
-- wang wang wang
-- miao miao miao
for i,v in ipairs(animals) do
v:MakeSound()
end
多继承
多继承会让代码变得复杂且难以调试(debug,找出bug)。很多语言如Java,C#都不支持它。
真的想实现,可以去 Lua官网学习。