轉載出處:http://blog.csdn.net/xenyinzen/article/details/3536708 web
元表概念
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的實例對象了。
函數