Lua本身是沒有面向對象支持的,但面向對象編程在邏輯復雜的大型工程卻很有用。於是很多人用Lua本身的數據結構table來模擬面向對象。最簡單的一種方法是把對象的方法、成員都放到table中。如:
-- file:test.lua local test = {} function test:get_x() return self.x or 0 end function test:set_x( _x ) self.x = _x end local test_module = {} function test_module.new() local t = {} for k,v in pairs( test ) do t[k] = v end return t end return test_module
調用也比較簡單:
-- file:main.lua local test = require "test" local _t = test.new() _t:set_x( 999 ) print( _t:get_x() )
這已經很像面向對象編程。但我們可以看到這樣寫有些缺點:
1.數據和方法混在一起(當然這不是什么大問題,C++也是這樣)
2.每創建一個對象,都要將方法復制一遍
3.沒法繼承
Lua有強大的元表(metatable),利用它我們可以更優雅地封裝一下:
1.先統一封裝一個面向對象函數:
-- file:oo.lua local oo = {} local cls = {} local function new( clz ) local t = {} setmetatable(t, clz) return t end function oo.class( parent,name ) local t = {} cls[name] = t parent = parent or {} rawset( t,"__index",t ) setmetatable( t,{ __index = parent,__call = new } ) return t end return oo
2.然后重新寫類的實現:
-- file:test.lua local oo = require "oo" local test = oo.class( nil,... ) function test:get_x() return self.x or 0 end function test:set_x( _x ) self.x = _x end return test
3.調用也更加簡單了:
-- file:main.lua local Test = require "test" local _t = Test() _t:set_x( 999 ) print( _t:get_x() )
可以看到,利用元表,我們可以把方法全部放到元表中,與對象成員數據分開。元表本身是一個表,它也有元表,可以把父類作為元表的元表實現繼承。我們如果再擴展一下,還可以實現對象方法的熱更,對象統計...
雖然元表很巧妙,但它的實現是有代價的。Lua得先在table中查找是否有相同的值,如果沒有,再去元表找。如果是多重繼承,那么還得一層層元表找下去。下面我們來測試一下元表的效率。
-- lua metatable performance test -- 2016-04-01 -- xzc local test = function( a,b ) return a+b end local empty_mt1 = {} local empty_mt2 = {} local empty_mt3 = {} local empty_mt4 = {} local empty_mt5 = {} local empty_mt6 = {} local empty_mt7 = {} local empty_mt8 = {} local mt = {} mt.test = test local mt_tb = {} setmetatable( empty_mt8,{__index = mt} ) setmetatable( empty_mt7,{__index = empty_mt8} ) setmetatable( empty_mt6,{__index = empty_mt7} ) setmetatable( empty_mt5,{__index = empty_mt6} ) setmetatable( empty_mt4,{__index = empty_mt5} ) setmetatable( empty_mt3,{__index = empty_mt4} ) setmetatable( empty_mt2,{__index = empty_mt3} ) setmetatable( empty_mt1,{__index = empty_mt2} ) setmetatable( mt_tb,{__index = empty_mt1} ) local tb = {} tb.test = test local ts = 10000000 f_tm_start() local cnt = 0 for i = 1,ts do cnt = test( cnt,1 ) end f_tm_stop( "call function native" ) f_tm_start() local cnt = 0 for i = 1,ts do cnt = tb.test( cnt,1 ) end f_tm_stop( "call function as table value" ) f_tm_start() for i = 1,ts do cnt = empty_mt6.test( cnt,1 ) end f_tm_stop( "call function with 3 level metatable" ) f_tm_start() for i = 1,ts do cnt = mt_tb.test( cnt,1 ) end f_tm_stop( "call function with 10 level metatable" )
在我的筆記本上測試,結果為:
local ts = 10000000 call function native 1091772 microsecond call function as table value 1287172 microsecond call function with 3 level metatable 2014431 microsecond call function with 10 level metatable 3707181 microsecond
可以看到,采用第一種方法封閉的面向對象比原生函數調用慢不了多少,但用第二種方法實現3重繼承的話,幾乎慢了一倍。
在實際項目中,我們用的是第二種封裝方式,最主要是可以繼承和熱更代碼。雖然效率有一定影響,但實際應用中邏輯消耗的時間比函數調用的時間仍大得多,這點損耗可以接受。這個世界上沒有最快,只有更快,不必盯着程序的效率看。在滿足項目要求的情況下,開發效率也是很值得考慮的。