【Lua】详解if,while,for,表和对象,数组,元表


共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官网学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值