lua面向對象封裝及元表(metatable)性能測試


  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重繼承的話,幾乎慢了一倍。

  在實際項目中,我們用的是第二種封裝方式,最主要是可以繼承和熱更代碼。雖然效率有一定影響,但實際應用中邏輯消耗的時間比函數調用的時間仍大得多,這點損耗可以接受。這個世界上沒有最快,只有更快,不必盯着程序的效率看。在滿足項目要求的情況下,開發效率也是很值得考慮的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM