lua實現面向對象

之前去面試被問到好多次這個lua面向對象的問題,正好看到這篇文章感覺寫的非常不錯,

元表概念

Lua中,面向對向是用元表這種機制來實現的。元表是個很“道家”的機制,很深遂,很強大,裏面有一些基本概念比較難理解透徹。不過,只有完全理解了元表,才能對Lua的面向對象使用自如,才能在寫Lua代碼的高級語法時遊刃有餘。

首先,一般來說,一個表和它的元表是不同的個體(不屬於同一個表),在創建新的table時,不會自動創建元表。
但是,任何表都可以有元表(這種能力是存在的)。

e.g.
t = {}
print(getmetatable(t))   --> nil
t1 = {}
setmetatable(t, t1)
assert(getmetatable(t) == t1)

setmetatable( 表1, 表2) 將表2掛接爲表1的元表,並且返回經過掛接後的表1。

元表中的__metatable字段,用於隱藏和保護元表。當一個表與一個賦值了__metatable的元表進行掛接時,用getmetatable操作這個表,就會返回__metatable這個字段的值,而不是元表!用setmetatable操作這個表(即給這個表賦予新的元表),那麼就會引發一個錯誤。

table: 0x9197200
Not your business
lua: metatest.lua:12: cannot change a protected metatable
stack traceback:
    [C]: in function 'setmetatable'
    metatest.lua:12: in main chunk
    [C]: ?

__index方法

元表中的__index元方法,是一個非常強力的元方法,它爲回溯查詢(讀取)提供了支持。而面向對象的實現基於回溯查找。
當訪問一個table中不存在的字段時,得到的結果爲nil。這是對的,但並非完全正確。實際上,如果這個表有元表的話,這種訪問會促使Lua去查找元表中的__index元方法。如果沒有這個元方法,那麼訪問結果就爲nil。否則,就由這個元方法來提供最終的結果。

__index可以被賦值爲一個函數,也可以是一個表。是函數的時候,就調用這個函數,傳入參數(參數是什麼後面再說),並返回若干值。是表的時候,就以相同的方式來重新訪問這個表。(是表的時候,__index就相當於元字段了,概念上還是分清楚比較好,雖然在Lua裏面一切都是值)

注意,這個時候,出現了三個表的個體了。這塊很容易犯暈,我們來理一下。
我們直接操作的表,稱爲表A,表A的元表,稱爲表B,表B的__index字段被賦予的表,稱爲表C。整個過程是這樣的,查找A中的一個字段,如果找不到的話,會去查看A有沒有元表B,如果有的話,就查找B中的__index字段是否有賦值,這個賦值是不是表C,如果是的話,就再去C中查找有沒有想訪問的那個字段,如果找到了,就返回那個字段值,如果沒找到,就返回nil。

對於沒有元表的表,訪問一個不存在的字段,就直接返回一個nil了。

__newindex是對應__index的方法,它的功能是“更新(寫)”,兩者是互補的。這裏不細講__newindex,但是過程很相似,靈活使用兩個元方法會產生很多強大的效果。

從繼承特性角度來講,初步的效果使用__index就可以實現了。

面向對象的實現

Lua應該說,是一種原型語言。原型是一種常規的對象,當其他對象(類的實例)遇到一個未知的操作時,原型會去查找這個原型。在這種語言中要表示一個類,只需創建一個專用作其他對象的原型。實際上,類和原型都是一種組織對象間共享行爲的方式。

Lua中實現原型很簡單,在上面分析的的那個三個表中,C就是A的原型。

原理講通後,來一點小技巧。其實,上面說的三個表嘛,不一定就是完全不同的。A和C可以是同一個。看下面的例子。

A = {}
setmetatable( A, { __index = A } )

這時,相當於A是A自身的原型了,自己是自己的原型,是個很有趣的字眼。就是說在查找的時候,在自己身上找不到就不會去其他地方找了。不過,自身是自身的原型本身並沒有多大用的。如果A能做爲一個類,然後生成的新對象以A做爲原型,這纔有用,後面談。

再看,自身也可以是自身的元表的。即A可以是A的元表。

A = {}
setmetatable( A, A )
這時就可以這樣寫了,
A.__index = 表或函數
自己是自己的元表有用處的,如果A.__index是賦予的一個表,至少能在內存中少產生一個表;而如果A.__index是一個函數,那麼就會產生很簡潔強大的效果。(__index爲其本身的一個字段了,不是很簡潔嗎)

然後,元表B與原型表C也可以是同一個。
A = {}
B = {}
B.__index = B
setmetatable( A, B )
這時,一個表的元表,就是這個表的原型,在面向對象的概念裏,就是這個表的類。

我們甚至可以,這樣來寫:

A = {}
setmetatable( A, A )
A.__index = A

從語法原理上,是行得通的。但Lua解釋器爲了避免出現不必要的麻煩(循環定義),把這種情況給Kick掉了,如果這樣寫,會報錯,並提示

loop in gettable

說真的,這樣定義也確實沒什麼用處。

下面開始正式進入面向對象的實現。

先引用一下Sputnik中的實現片斷,

local Sputnik = {}
local Sputnik_mt = {__metatable = {}, __index = Sputnik}

function new(config, logger)

   -- 這裏生成obj對象之後,obj的原型就是Sputnik了,而後面會有很多的Sputnik的方法定義
   local obj = setmetatable({}, Sputnik_mt)
   -- 這裏的方法就是“繼承”的Sputnik的方法
   obj:init(config)
   返回這個對象的引用
   return obj
end

由上面可見,兩個表定義加上一個方法,實現了類,及由類產生對象的方案。因爲這是在模塊中,故new前面沒有表名稱。這種方式實現有個好處,就是在外界調用此模塊的時候,使用 

sputnik = require "sputnik"
然後,調用
s = sputnik.new()
就可以生成一個sputnik對象s了,這個對象會繼承原型Sputnik(就是上面定義的那個表)的所有方法和屬性。

但是,這種方法定義的,也有點問題,就是,類的繼承實現上不方便。它只是在類的定義上,和生成對象的方式上比較方便,但是在類之間的繼承上不方便。

下面,用另一種方式實現。

A = {
    x = 10,
    y = 20
}

function A:new( t )
    local t = t or {}
    self.__index = self
    setmetatable( t, self )
    return t
end

從A中產生一個對象AA

AA = A:new()

此時,AA就是一個新表了,它是一個對象,但也是一個類。它還可以繼續如下操作:

s = AA:new()

AA中本來是沒有new這個方法的,但它被賦予了一個元表(同時也是原型),這個時候是A,A中有new方法和x,y兩個字段。

AA通過__index回溯到A找到了new方法,並且執行new的代碼,同時還會傳入self參數。這就是奇妙所在,此時候傳入的self參數引用的是AA這個表,而不再是第一次調用時A這個表了。因此 AA:new() 執行後,同樣,是生成了一個新的對象s,同時這個對象以AA爲原型,並且繼承AA的所有內容。至此,我們不是已經實現了類的繼承了嗎?AA現在是A的子類,s是AA的一個對象實例。後面還可以以此類推,建立長長的繼承鏈。

由上也可見,類與原型概念上還是有區別的,Lua是一種原型語言,這點體現的得很明顯,類在這種語言中,就是原型,而原型僅僅是一個常規對象。

下面,如果在A中定義了函數:
function A:acc( v )
    self.x = self.x + v
end

function A:dec( v )
    if v > self.x then error "not more than zero" end
    self.x = self.x - v
end

然後,現在調用
s:acc(5)

那麼,是這樣調用的,先是查找s中有無acc這個方法,沒有找到,然後去找AA中有無acc這個方法,還是沒找到,就去A中找有無此方法,找到了。找到後,將指向s的self參數和5這個參數傳進acc函數中,並執行acc的代碼,執行裏面代碼的時候,這一句:
self.x = self.x + v
在表達式右端,self.x是一個空值,因爲self現在指向的是s,因此,根據__index往回回溯,一直找到A中有一個x,然後引用這個x值,10,因此,上面表達式就變成
self.x = 10 + 5
右邊計算得15,賦值給左邊,但這時self.x沒有定義,但是s(及s的元表)中也沒有定義__newindex元方法,於是,就在self(此時爲s)所指向的表裏面新建一個x字段,然後將15賦值給這個字段。

經過這個操作之後,實例s中,就有一個字段(成員變量)x了,它的值爲15。
下次,如果再調用
s:dec(10)
的話,就會做類似的回溯操作,不過這次只做方法的回溯,而不做成員變量x的回溯,因爲此時s中已經有x這個成員變量了,執行了這個函數後,s.x會等於5。

綜上,這就是整個類繼承,及對象實例方法引用的過程了。不過,話還沒說完。

AA作爲A的子類,本身是可以有一些作爲的,因爲AA之下的類及對象在查找時,都會先通過它這一關,纔會到它的父親A那裏去,因此,它這裏可以重載A的方法,比如,它可以定義如下函數:

function AA:acc(v)
    ...
end

function AA:dec(v)
    ...
end

函數裏面可以寫入一些新的不一樣的內容,以應對現實世界中複雜的差異性。這個特性用面向對象的話來說,就是子類可以覆蓋父類的方法及成員變量(字段),也就是重載。這個特性是必須的。

AA中還可以定義一些A中沒有的方法和字段,操作是一樣的,這裏提一下。

Lua中的對象還有一個很靈活強大的特性,就是無須爲指定一種新行爲而創建一個新類。如果只有一個對象需要某種特殊的行爲,那麼可以直接在該對象中實現這個行爲。也就是說,在對象被創建後,對象的方法和字段還可以被增加,重載,以應對實際多變的情況。而毋須去勞駕類定義的修改。這也是類是普通對象的好處。更加靈活。

可以看出,A:new()這個函數是一個很關鍵的函數,在類的繼承中起了關鍵性因素。不過爲了適應在模塊中使用的情況(很多),在function A:new(t)之外還定義一個 
function new(t)
    A:new(t)
end
將生成函數封裝起來,然後,只需使用 模塊名.new() 就可以在模塊外面生成一個A的實例對象了。

差不多了吧,可以看到,這種類實現的機制是多麼自洽,簡潔,靈活,強大!不過要折磨下你的大腦了。


本文轉載自:http://blog.csdn.net/xenyinzen/article/details/3536708

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章