魔獸世界編程寶典讀書筆記(4-2)

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函數的高級特徵。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章