概述 4.4 表的面向对象
编程 表也可以用于另外一种“面向对象编程”的编程方式,这种方式是基于对象的概念进行的一种编程方式。对象既包括了数据,也包括了对这些数据的 *** 作(专业术语把 *** 作叫做“方法”)。对应于Lua,数据就是指各种变量,而方法就是指特定的函数。而通过前面的讲述,大家已经看到,表既可以赋值为变量,也可以赋值为函数。 4.4.1 创建非面向对象计数器 为了显示面向对象的威力,我
4.4 表的面向对象编程 表也可以用于另外一种“面向对象编程”的编程方式,这种方式是基于对象的概念进行的一种编程方式。对象既包括了数据,也包括了对这些数据的 *** 作(专业术语把 *** 作叫做“方法”)。对应于Lua,数据就是指各种变量,而方法就是指特定的函数。而通过前面的讲述,大家已经看到,表既可以赋值为变量,也可以赋值为函数。
4.4.1 创建非面向对象计数器 为了显示面向对象的威力,我们先来看一段非面向对象的代码。我们写一个计数器: > --创建一个区域 > do >>
--私有变量counter >>
counter=0 >> >>
--公有的函数,完成获得counter的值 >>
counter_get=function() >>
return counter >>
end >> >>
counter_inc=function() >>
counter=counter+1 >>
end >> end 上面这个计数器是可以正常工作的: > counter_inc() > print(counter_get()) 1 > counter_inc() > counter_inc() > counter_inc() > print(counter_get()) 4 这段代码创建了一个简单的单向计数器,它不能后退,但可以通过函数counter_get()和counter_inc()分别获取值和递增值。但是,这里只有一个计数器,而很多情况下,我们需要大量的计数器,于是,我们可能需要重复这些代码许多许多遍。声明很多很多的变量,这些变量名不能相同,由于变量名的不同,所有的获取值和递增值的函数也要全部重复很多遍。所以大家看出这样写法在功能上是很有局限的,而且代码上也做了不必要的重复。
4.4.2 把表作为简单的对象 下面是计数器的另一种实现方式,我们使用一个表来定义变量和函数(
默然说话:对的,表既可以装变量,也可以装函数,所以,我们可以在一个表里既装变量,也装函数) > counter={ >>
count=0 >> } > counter.get=function(self) >> return self.count >> end > counter.inc=function(self) >> self.count=self.count+1 >> end 该程序允许我们执行以下语句: > print(counter.get(counter)) 0 > counter.inc(counter) > print(counter.get(counter)) 1 在这个实现中,实际的计数器变量存储在了一个表中。与值交互的每一个函数都有一个名为self的参数,我们可以通过以下的代码很容易就创建第二个计数器: > counter2={ >> count=15, >> get=counter.get, >> inc=counter.inc, >> } > print(counter2.get(counter2)) 15
4.4.3 用冒号调用对象方法 上面我们在调用get()和inc()方法的时候,我们都是使用了对应的对象作为参数,这样的写法感觉有点麻烦(同一个对象名打了两遍),Lua提供了一个简单的方式让我们能够少打一遍对象名,就是把counter.get(counter)写为counter:get()。这样,counter对象就会自动作为第一个参数被传给了get()方法。
4.4.4 创建更佳的计数器 这个计数器程序看上去仍然很笨拙。我们可以定义一个更健壮的计数器系统: > --创建一个新的区域来定义计数器 > do >>
local get=function(self) >>
return self.count >>
end >>
local inc=function(self) >>
self.count=self.count+1 >>
end >>
new_counter=function(value) >>
if type(value)~="number" then >>
value=0 >>
end >>
local obj={ >>
count=value, >>
get=get, >>
inc=inc, >>
} >>
return obj >>
end >> end 这个样例提供了一个名为new_counter的全局函数,它只有一个参数,即计数器的初始值。它返回一个对象,该对象包含两个方法和计数器的初始值。这个函数就是典型的工厂函数,它负责生产计数器对象,只要你传递给它一个计数器的初始值,它就返回一个计数器对象给你,每个对象都包括两个方法,一个可以获得计数器当前值,另一个进行计数。下面运行一些测试代码以验证系统是否正常工作: > counter=new_counter() > print(counter:get()) 0 > counter2=new_counter(15) > print(counter2:get()) 15 > counter:inc() > print(counter:get()) 1 > counter2:inc() > print(counter2:get()) 16 这就是面向对象带来的好处,代码比较简单,却完成了强大的功能,当然由于代码被大量的重用了,在理解上也带来了一定的难度。但与在功能的强大和代码简单的优点相比,这点难度又算得了什么呢?
4.5 利用Metatables对表进行扩展 Lua中,我们可以对表的key-value对执行 *** 作,访问key对应的value,遍历所有的key-value。但是我们不可以对两个table执行加 *** 作,也不可以比较两个表的大小,除非,你使用了Metatables对它们如何进行相关 *** 作 Metatables允许我们改变表的行为,例如,使用Metatables我们可以定义Lua如何计算两个table的相加 *** 作a+b(
默然说话:这部分内容非常象C++里的运算符重载。)。 Metatable是一个简单的表,它可以存储一些关于表可以执行的 *** 作的信息。Metatable类似于一个父类,它的 *** 作可以被更改,而所有设置了Metatable的表的行为都会与Metatable一致。
4.5.1 添加Metatable 在Lua中,任何一个表在开始的时候都没有Metatable,你可以使用setMetatable()来将任何的表定义为别的表的Metatable,换句话:你可以将你定义的任何表提升为一个父亲,让别的表做它的儿子: > tbl1={"a","b","c"} > tbl2={"d","e","f"} > tbl3={} > mt={} > setMetatable(tbl1,mt) > setMetatable(tbl2,mt) > setMetatable(tbl3,mt) 大家都看到了setMetatable()的使用了,它有两个参数:
l tbl----等待添加Metatable的表。
l mt----Metatable 你可以使用getMetatable()来查看一个表是否具有了Metatable,表默认是没有Metatable的,所以getMetatable()会返回一个nil,如果它已经通过setMetatable()获得了一个Metatable,那么getMetatable()就会返回这个Metatable对象。 > print(getMetatable(mt)) nil > print(getMetatable(tbl1)==mt) true 从上面的代码我们可以看出getMetatable()的用法,它传一个参数,就是你想知道有没有Metatable的那个表,它有一个返回值,就是Metatable对象。
4.5.2 定义Metatable方法 定义Metatable方法也就是重写Metatable中已经存在的方法,让它们具备我们所期望的功能。Metatable方法很多,参数各不相同,但每个方法都是两个下划线开头(
默然说话:下面的列表及代码中,为了能让大家看到两个下划线,我在两个下划线之间多加了一个空格,你们在写代码的时候是不应该加这个空格的,因为变量名已经规定,空格是不能作为变量或函数名的一部分的,这里加空格仅仅是为了显示上的需要)。这里只介绍WoW中比较常用的方法。如果你想了解得更详细可以参考《Lua编程(Programming in Lua)》(
默然说话:这本书只有 200多页,它比较详细介绍了Lua,如果以前没有任何编程经验的同学,可以先看一下这本书,这会获得一些有益的帮助,有编程经验的同学也可以看一下以便更详细的了解Lua)。 表4-1 常用的元方法
Metatable 方法 | 参数个数 | 描述 |
_ _add | 2 | 定义 + 运算符的行为 |
_ _mul | 2 | 定义 * 运算符的行为 |
_ _div | 2 | 定义 / 运算符的行为 |
_ _sub | 2 | 定义 - 运算符的行为 |
_ _unm | 1 | 定义负号的行为 |
_ _tostring | 1 | 定义一个表应该以何种字符串格式来表示自己的行为 |
_ _concat | 2 | 定义 .. 运算符的行为 |
_ _index | 2 | 定义表如何检索下标的行为 |
_ _newindex | 3 | 定义表如何创建下标的行为 |
1. 使用_ _add ,_ _sub ,_ _mul ,_ _div 定义自己的加减乘除 我记得有一句话是这么说的:在编程世界,程序员就是上帝,而这里所提供的四个函数就是最明显的写照。加减乘除不一定非要按照我们平时认为的那种方式去运算,只要我们能正确的表达我们期待的计算方式,那么计算机就会按照我们的要求在进行加减乘除的运算。当然,还是有一些限制需要我们注意。
l 每个算术方法都带有两个参数,返回一个值,返回值的类型可以是任意类型。
l 在重新定义算术方法的时候,应该考虑多重运算的情况,换句话:一个表达式的运算结果很可能是另一个更大的表达式的一部分。 还是来看个例子吧。下面的函数重新定义了两个表的加运算(
默然说话:默认情况下,两个表是不能进行加运算的),意思就是把第二个表的数据和第一个表的数据进行合并,形成一个新表。 > mt._ _add=function(a,b) >> local result=setmetable({},mt) >> --复制a表的内容到result表 >> for i=1,#a do >>
table.insert(result,a[i]) >> end >> --复制b表的内容到result表 >> for i=1,#b do >>
table.insert(result,b[i]) >> end >> --返回result表 >> return result >> end 这个函数的参数a和b就是两个表对象。第一行代码新建了一个空的表,并设置Metatable为mt。之后就是将a表和b表的全部内容都复制到新的表中,也就是完成我们期待的将a表和b表的内容合并的功能,下面是对上面的代码进行的测试,这里我们使用了运算符+: > newtbl=tbl1+tbl2 > print(#newtbl) 6 > for i=1,#newtbl do >> print(newtbl[i]) >> end 张三 李四 王五 苹果 香蕉 梨 Metatable方法正确的执行了我们所定义的 *** 作。 2. 使用_ _unm 定义负运算 在数学里,一个数的负运算就是取这个数据的相反数,而这里不仅仅要 *** 作数字,我们要 *** 作的是表,所以我们希望出现的是:当在一个表前面加一个负号的时候,会将这个表里的所有元素进行倒序排序,下面的代码将实现这个想法: > mt._ _unm=function(a) >> local result=setMetatable({},mt) --将a表倒序输出,并将每一个元素都装到result表中 >> for i=#a,1,-1 do >>
table.insert(result,a[i]) >> end >> return result >> end 上面的代码思路和重写加法运算的思路大同小异,也是创建一个新的表result,然后倒着循环a表,将a表的中每个元素取出来装到result中,然后再返回result。下面是测试代码: > newtbl2=-newtbl > for i=1,#newtbl2 do >>
print(newtbl2[i]) >> end 梨 香蕉 苹果 王五 李四 张三 3.使用_ _tostring建立有意义的输出 在前面,每次我们都要写一个循环才能看到表中的每个元素,这显得很麻烦,有没有什么简单办法来完成这个过程呢?在面向对象的语言中,我们都知道有一个叫tostring的方法是用来完成这个作用的:产生一个可用的简单字符串输出。 在写代码之前,我们不妨先试试下面的代码,看看,如果我直接print()一个表对象会发生什么: > print(tbl1) table: 003CB7C8 > print(tbl2) table: 003CBA18 > print(newtbl) table: 003C6850 > print(newtbl2) table: 004626E8 > print(t) nil 从上面的代码我们可以看出:直接打印一个表对象,得到的是一个table:开头,后面跟着八位十六进制数字的字符串输出形式,如果这个表不存在,它将输出一个nil(最后的那个print(t)就是这样的情况),这就是默认的_ _tostring()定义的情况。我们来把它改为我们希望的情况:以“{”开头,以“}”结束,中间是所有的表元素,每个表元素之间以逗号分开,开头第一个元素之前不能有逗号,最后一个元素之后也不能有逗号,代码类似这样: > mt.__tostring=function(a) >>
local result="{" >>
for i=1,#a do >>
if i>1 then >>
result=result .. "," >>
end >>
result=result .. tostring(a[i]) >>
end >>
result=result .. "}" >>
return result >> end tostring函数同样有一个参数:a表,里面也写了一个循环,但与前面两个函数不同的地方,它的返回值是一个字符串,而不是一个表对象,所以这里的result是一个字符串。我们使用循环取出a表中的每一个元素,并按我们希望的格式进行了字符串连接,最后返回了result,下面是测试代码: > print(tbl1) {张三,李四,王五} > print(tbl2) {苹果,香蕉,梨} > print(newtbl) {张三,王五,苹果,梨} > print(newtbl2) {梨,张三} > print(t) nil 现在我们不再看到那个table:的格式输出了,而是按照我们预想的格式将表中的所有元素进行了输出。在比较复杂的对象出,能这样输出一个字符串将会非常的有用处。 5. 使用_ _index 在后备表中浏览 在前面我们提到过,如果在一张表中使用了一个没有值键来索引这张表的元素,那将会看到一个nil的值,下面的代码可以说明这一点: > print(tbl1[4]) nil > print(tbl1.some) nil 其实我们可以改变这一现象,因为Lua中做了如下规定:在使键去寻找值的过程中,如果可以在表中找到值,那么就直接返回值,如果找不到值,就去调用_ _index这个Metatable方法,由_ _index决定输出什么。 这个在实际中非常有用,比如我们有很多的窗口,它们具体不同的x,y坐标(在屏幕上的显示位置不一样),但我们希望这些窗口都具有相同的大小,如果我们直接来写,可能是下面这样: > form1={ >> wIDth=100, >> height=100, >> x=23, >> y=10 >> } > form2={ >> wIDth=100, >> x=25, >> y=10 >> } > form3={ >> wIDth=100, >> x=35, >> y=10 >> } 这样也能完成我们希望的工作,但是我们会发现一个很大的问题,由于我们希望这些窗口都具备相同的宽和高,那么一旦程序要进调整所有这些窗口的宽和高,那就要把每一个窗口对象的宽和高都调整一遍,这样很麻烦,而且容易出错。要是这些宽和高都独立放在一张表中,然后使用类似继承的方式,让我们所有的窗口都继承这张只包括宽和高的表,这样,如果要修改,我们就只需要修改这张只包括宽和高的表就可以了。这时,我们就可以使用_ _index来完成我们的希望: 首先创建这张独立的表,它只包括宽和高: > form={ >> wIDth=100, >> } 然后创建两个三个代表窗口的表,它们只包括x,y坐标,并为它们都指定Metatable: > form1={ >> x=1, >> y=10 >> } > form2={ >> x=101, >> y=10 >> } > form3={ >> x=201, >> y=10 >> } > setMetatable(form1,mt) > setMetatable(form2,mt) > setMetatable(form3,mt) 之后就是定义Metatable的_ _index方法: 这个方法比较特别,它有两种定义的方式,它可以指定为一张表(这里所有的Metatable方法中唯一个可以指定为表的方法,其它的都只能指定为函数),也可以直接指定为一个函数。如果你指定为一张表,那么,当你所索引的键在窗口中找不到时,它就会到_ _index所指定的那张表中去找,并返回找到的值。如果指定的是一个函数,那就直接返回这个函数的返回值。 指定为表: 要将_ _index指定为一张表,代码很简单: > mt._ _index=form 之后我们来试试下面的代码: > print(form1.wIDth) 100 > print(form2.wIDth) 100 > print(form3.wIDth) 100 这个功能很类似于面向对象语言中的继承。这样一来,我们要进行修改宽和高也会变得非常容易了: > form.wIDth=50 > print(form1.wIDth) 50 > print(form2.wIDth) 50 > print(form3.wIDth) 50 只要修改了form的wIDth,form1,form2还有form3的宽都跟着变化了。 指定为函数: 我们也可以使用函数的形式来实现与上面一样的功能,这个函数返回一个字符串,有两个参数,第一个是表,第二个是键: > mt.__index=function(a,key) >>
return form[key] >> end 将上面的代码敲入解释器中,并再次运行前面的测试代码,输出form1、form2、form3的宽,你会发现与前面所使用的代码得到的效果是完全一致的。 大家应该明显可以看出,指定一个表绝对比指定一个函数在代码方面要简单的得多。不过使用函数还能做更有趣的事情,我们试试下面的代码: > mt.__index=function(a,key) >>
if form[key] ~=nil then >>
return form[key] >>
else >>
return "我不知道你要寻找什么" >>
end >> end 上面这段代码加入了一个判断语句,如果这个键能在form中找到对应的值,那么就返回这个值,如果找不到,那就会返回一个出错信息:“我不知道你要寻找什么”: > print(form3.wIDth) 50 > print(form3.wIDth2) 我不知道你要寻找什么 上面的测试代码已经表明,当第二个打印语句意外的将wIDth打错的时候,输出的不再是一个让人迷惑的nil,而一句很人性化的出错信息。 6. 使用_ _newindex 增强对表赋值的处理 _ _index方法是在表中查找一个键的值却找不到的时候被调用,而_ _newindex则是在一个表中插入新的键—值对之前被调用。也就是说,如果你重写了_ _newindex方法,那么它就执行这个方法所做的规定,而不再是简单的完成键—值的赋值 *** 作了。_ _newindex有三个参数:第一个参数是需要索引的表,第二个参数是需要添加的键,第三个参数是需要赋值的值。看个例子: > mt.__newindex=function(a,key,value) >> if value=="**" or value==" *** " then >>
rawset(a,"@#$!") >> else >>
rawset(a,value) >> end >> end 这是一段很简单的,用来屏蔽某些关键字的代码,如果你插入一个新的键,并且给这个新键的值是“**”或者” *** ”,那么它将被取代为"@#$!"。rawset()函数是一个让你完成默认的键—值赋值 *** 作的函数: > form1.name="**" > print(form1.name) @#$!
4.6 小结 本章介绍了表的方方面面,下一章我们将继续学习关于Lua函数的高级特征。 总结
以上是内存溢出为你收集整理的魔兽世界编程宝典读书笔记(4-2)全部内容,希望文章能够帮你解决魔兽世界编程宝典读书笔记(4-2)所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
评论列表(0条)