魔兽世界编程宝典读书笔记(4-2)

来源:互联网 发布:windows快捷方式的说法 编辑:程序博客网 时间:2024/05/17 02:23

 

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()的使用了,它有两个参数:
tbl----等待添加metatable的表。
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定义自己的加减乘除
我记得有一句话是这么说的:在编程世界,程序员就是上帝,而这里所提供的四个函数就是最明显的写照。加减乘除不一定非要按照我们平时认为的那种方式去运算,只要我们能正确的表达我们期待的计算方式,那么计算机就会按照我们的要求在进行加减乘除的运算。当然,还是有一些限制需要我们注意。
每个算术方法都带有两个参数,返回一个值,返回值的类型可以是任意类型。
在重新定义算术方法的时候,应该考虑多重运算的情况,换句话:一个表达式的运算结果很可能是另一个更大的表达式的一部分。
还是来看个例子吧。下面的函数重新定义了两个表的加运算(默然说话:默认情况下,两个表是不能进行加运算的),意思就是把第二个表的数据和第一个表的数据进行合并,形成一个新表。
> 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,
>> height=100,
>> x=25,
>> y=10
>> }
> form3={
>> width=100,
>> height=100,
>> x=35,
>> y=10
>> }
这样也能完成我们希望的工作,但是我们会发现一个很大的问题,由于我们希望这些窗口都具备相同的宽和高,那么一旦程序要进调整所有这些窗口的宽和高,那就要把每一个窗口对象的宽和高都调整一遍,这样很麻烦,而且容易出错。要是这些宽和高都独立放在一张表中,然后使用类似继承的方式,让我们所有的窗口都继承这张只包括宽和高的表,这样,如果要修改,我们就只需要修改这张只包括宽和高的表就可以了。这时,我们就可以使用_ _index来完成我们的希望:
首先创建这张独立的表,它只包括宽和高:
> form={
>> width=100,
>> height=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,key,"@#$!")
>> else
>>   rawset(a,key,value)
>> end
>> end
这是一段很简单的,用来屏蔽某些关键字的代码,如果你插入一个新的键,并且给这个新键的值是“**”或者”操”,那么它将被取代为"@#$!"。rawset()函数是一个让你完成默认的键—值赋值操作的函数:
> form1.name="**"
> print(form1.name)
@#$!
4.6        小结
本章介绍了表的方方面面,下一章我们将继续学习关于Lua函数的高级特征。
原创粉丝点击