元表的元方法
函數 | 描述 |
---|---|
__add | 運算符 + |
__sub | 運算符 - |
__mul | 運算符 * |
__ div | 運算符 / |
__mod | 運算符 % |
__unm | 運算符 -(取反) |
__concat | 運算符 .. |
__eq | 運算符 == |
__lt | 運算符 < |
__le | 運算符 <= |
__call | 當函數調用 |
__tostring | 轉化爲字符串 |
__index | 調用一個索引 |
__newindex | 給一個索引賦值 |
由於那幾個運算符使用類似,所以就不單獨說明了,接下來說 __call, __tostring, __index, __newindex四個元方法。
__call
__call可以讓table當做一個函數來使用。
local mt = {}
--__call的第一參數是表自己
mt.__call = function(mytable,...)
--輸出所有參數
for _,v in ipairs{...} do
print(v)
end
end
t = {}
setmetatable(t,mt)
--將t當作一個函數調用
t(1,2,3)
結果:
1
2
3
__tostring
__tostring可以修改table轉化爲字符串的行爲
local mt = {}
--參數是表自己
mt.__tostring = function(t)
local s = "{"
for i,v in ipairs(t) do
if i > 1 then
s = s..", "
end
s = s..v
end
s = s .."}"
return s
end
t = {1,2,3}
--直接輸出t
print(t)
--將t的元表設爲mt
setmetatable(t,mt)
--輸出t
print(t)
結果:
table: 0x14e2050
{1, 2, 3}
__index
調用table的一個不存在的索引時,會使用到元表的__index元方法,和前幾個元方法不同,__index可以是一個函數也可是一個table。
作爲函數:
將表和索引作爲參數傳入__index元方法,return一個返回值
local mt = {}
--第一個參數是表自己,第二個參數是調用的索引
mt.__index = function(t,key)
return "it is "..key
end
t = {1,2,3}
--輸出未定義的key索引,輸出爲nil
print(t.key)
setmetatable(t,mt)
--設置元表後輸出未定義的key索引,調用元表的__index函數,返回"it is key"輸出
print(t.key)
結果:
nil
it is key
作爲table:
查找__index元方法表,若有該索引,則返回該索引對應的值,否則返回nil
local mt = {}
mt.__index = {key = "it is key"}
t = {1,2,3}
--輸出未定義的key索引,輸出爲nil
print(t.key)
setmetatable(t,mt)
--輸出表中未定義,但元表的__index中定義的key索引時,輸出__index中的key索引值"it is key"
print(t.key)
--輸出表中未定義,但元表的__index中也未定義的值時,輸出爲nil
print(t.key2)
結果:
nil
it is key
nil
__newindex
當爲table中一個不存在的索引賦值時,會去調用元表中的__newindex元方法
作爲函數
__newindex是一個函數時會將賦值語句中的表、索引、賦的值當作參數去調用。不對錶進行改變
local mt = {}
--第一個參數時表自己,第二個參數是索引,第三個參數是賦的值
mt.__newindex = function(t,index,value)
print("index is "..index)
print("value is "..value)
end
t = {key = "it is key"}
setmetatable(t,mt)
--輸出表中已有索引key的值
print(t.key)
--爲表中不存在的newKey索引賦值,調用了元表的__newIndex元方法,輸出了參數信息
t.newKey = 10
--表中的newKey索引值還是空,上面看着是一個賦值操作,其實只是調用了__newIndex元方法,並沒有對t中的元素進行改動
print(t.newKey)
結果:
it is key
index is newKey
value is 10
nil
作爲table
__newindex是一個table時,爲t中不存在的索引賦值會將該索引和值賦到__newindex所指向的表中,不對原來的表進行改變。
local mt = {}
--將__newindex元方法設置爲一個空表newTable
local newTable = {}
mt.__newindex = newTable
t = {}
setmetatable(t,mt)
print(t.newKey,newTable.newKey)
--對t中不存在的索引進行負值時,由於t的元表中的__newindex元方法指向了一個表,所以並沒有對t中的索引進行賦值操作將,而是將__newindex所指向的newTable的newKey索引賦值爲了"it is newKey"
t.newKey = "it is newKey"
print(t.newKey,newTable.newKey)
結果:
nil nil
nil it is newKey
rawget 和 rawset
有時候我們希望直接改動或獲取表中的值時,就需要rawget和rawset方法了。
rawget可以讓你直接獲取到表中索引的實際值,而不通過元表的__index元方法。
local mt = {}
mt.__index = {key = "it is key"}
t = {}
setmetatable(t,mt)
print(t.key)
--通過rawget直接獲取t中的key索引
print(rawget(t,"key"))
結果:
it is key
nil
rawset可以讓你直接爲表中索引的賦值,而不通過元表的__newindex元方法。
local mt = {}
local newTable = {}
mt.__newindex = newTable
t = {}
setmetatable(t,mt)
print(t.newKey,newTable.newKey)
--通過rawset直接向t的newKey索引賦值
rawset(t,"newKey","it is newKey")
print(t.newKey,newTable.newKey)
結果:
nil nil
it is newKey nil
元表的使用場景
作爲table的元表
通過爲table設置元表可以在lua中實現面向對象編程。
作爲userdata的元表
通過對userdata和元表可以實現在lua中對c中的結構進行面向對象式的訪問。
弱引用table
與python等腳本語言類似地,Lua也採用了自動內存管理(Garbage Collection),一個程序只需創建對象,而無需刪除對象。通過使用垃圾收集機制,Lua會自動刪除過期對象。垃圾回收機制可以將程序員從C語言中常出現的內存泄漏、引用無效指針等底層bug中解放出來。
我們知道Python的垃圾回收機制使用了引用計數算法,當指向一個對象的所有名字都失效(超出生存期或程序員顯式del了)了,會將該對象佔用的內存回收。但對於循環引用是一個特例,垃圾收集器通常無法識別,這樣會導致存在循環引用的對象上的引用計數器永遠不會變爲零,也就沒有機會被回收。
一個在python中使用循環引用的例子:
class main1:
def __init__(self):
print('The main1 constructor is calling...')
def __del__(self):
print('The main1 destructor is calling....')
class main2:
def __init__(self, m3, m1):
self.m1 = m1
self.m3 = m3
print('The main2 constructor is calling...')
def __del__(self):
print('The main2 destructor is calling....')
class main3:
def __init__(self):
self.m1 = main1()
self.m2 = main2(self, self.m1)
print('The main3 constructor is calling...')
def __del__(self):
print('The main3 destructor is calling....')
# test
main3()
輸出內容爲:
The main1 constructor is calling...
The main2 constructor is calling...
The main3 constructor is calling...
可以看出,析構函數(__del__函數)沒有被調用,循環引用導致了內存泄漏。
垃圾收集器只能回收那些它認爲是垃圾的東西,不會回收那些用戶認爲是垃圾的東西。比如那些存儲在全局變量中的對象,即使程序不會再用到它們,但對於Lua來說它們也不是垃圾,除非用戶將這些對象賦值爲nil,這樣它們才能被釋放。但有時候,簡單地清除引用還不夠,比如將一個對象放在一個數組中時,它就無法被回收,這是因爲即使當前沒有其他地方在使用它,但數組仍引用着它,除非用戶告訴Lua這項引用不應該阻礙此對象的回收,否則Lua是無從得知的。
table中有key和value,這兩者都可以包含任意類型的對象。通常,垃圾收集器不會回收一個可訪問table中作爲key或value的對象。也就是說,這些key和value都是強引用,它們會阻止對其所引用對象的回收。在一個弱引用table中,key和value是可以回收的。
弱引用table(weak table)是用戶用來告訴Lua一個引用不應該阻礙對該對象的回收。所謂弱引用,就是一種會被垃圾收集器忽視的對象引用。如果一個對象的引用都是弱引用,該對象也會被回收,並且還可以以某種形式來刪除這些弱引用本身。
弱引用table有3種類型:
1、具有弱引用key的table;
2、具有弱引用value的table;
3、同時具有弱引用key和value的table;
table的弱引用類型是通過其元表中的__mode字段來決定的。這個字段的值應爲一個字符串:
如果包含'k',那麼這個table的key是弱引用的;
如果包含'v',那麼這個table的value是弱引用的;
弱引用table的一個例子,這裏使用了collectgarbage函數強制進行一次垃圾收集:
a = {1,4, name='cq'}
setmetatable(a, {__mode='k'})
key = {}
a[key] = 'key1'
key = {}
a[key] = 'key2'
print("before GC")
for k, v in pairs(a) do
print(k, '\t', v)
end
collectgarbage()
print("\nafter GC")
for k, v in pairs(a) do
print(k, '\t', v)
end
輸出:
before GC
1 1
2 4
table: 0x167ba70 key1
name cq
table: 0x167bac0 key2
after GC
1 1
2 4
name cq
table: 0x167bac0 key2
在本例中,第二句賦值key={}會覆蓋第一個key,當收集器運行時,由於沒有地方在引用第一個key,因此第一個key就被回收了,並且table中的相應條目也被刪除了。至於第二個key,變量key仍引用着它,因此它沒有被回收。
注意,弱引用table中只有對象可以被回收,而像數字、字符串和布爾這樣的“值”是不可回收的。
備忘錄(memoize)函數是一種用空間換時間的做法,比如有一個普通的服務器,每當它收到一個請求,就要對代碼字符串調用loadstring,然後再調用編譯好的函數。不過,loadstring是一個昂貴的函數,有些發給服務器的命令有很高的頻率,例如"close()",如果每次收到一個這樣的命令都要調用loadstring,那還不如讓服務器用一個輔助的table記錄下所有調用loadstring的結果。
備忘錄函數的例子:
local results = {}
setmetatable(results, {__mode='v'})
function mem_loadstring(s)
local res = results[s]
if res == nil then
res=assert(loadstring(s))
results[s]=res
end
return res
end
local a = mem_loadstring("print 'hello'")
local b = mem_loadstring("print 'world'")
a = nil
collectgarbage()
for k,v in pairs(results) do
print(k, '\t', v)
end
例子中,table results會逐漸地積累服務器收到的所有命令及其編譯結果。經過一定時間後,會耗費大量的內存。弱引用table正好可以解決這個問題,如果results table具有弱引用的value,那麼每次垃圾收集都會刪除所有在執行時未使用的編譯結果。
在lua元表一文中,提到過如何實現具有默認值的table。如果要爲每一個table都設置一個默認值,又不想讓這些默認值持續存在下去,也可以使用弱引用table,如下面的例子:
local defaults = {}
setmetatable(defaults, {__mode='k'})
local mt = {__index=function(t) return defaults[t] end}
function setDefault(t, d)
defaults[t] = d
setmetatable(t, mt)
end
local a = {}
local b = {}
setDefault(a, "hello")
setDefault(b, "world")
print(a.key1)
print(b.key2)
b = nil
collectgarbage()
for k,v in pairs(defaults) do
print(k,'\t',v)
end
弱引用的應用:lua中檢測內存泄露
在lua中檢測內存泄露,也分兩個層次,
層次1:同時檢測lua層中的lua對象的泄露和因爲lua層而導致的c對象的泄露。
層次2:僅檢測lua層中的lua對象的泄露。
做到滿足層次1的內存泄露檢測相對略微複雜,不過原理還是比較簡單的,即掃描兩個時間點時的lua state並獲得快照,之後比較兩張快照,後一張中多出來的內存引用就是後一張那個時間點相對前一張那個時間點多分配的內存。那如果這兩個時間點在功能邏輯上是一致的(比如前一個時間點是進入某副本前,後一個時間點是退出某副本後),那麼多出來的內存引用就是泄露出來的內存。
這裏來重點討論層次2的實現,僅檢測lua層中lua對象的泄露這一點也是很重要的。代碼規模一大,想通過敏銳的視力和嚴謹的頭腦分析能力來檢測出內存泄露是很困難的,因此需要一定的工具來爲自己提供幫助。這裏可以利用lua提供的“弱引用”。“弱引用”是這樣的一種引用,其對某個對象的引用並不會對lua gc機制對該對象的垃圾回收造成影響,也就是說某對象若其只存在弱引用,那麼該對象會被gc回收——簡單來說,gc會無視弱引用。
因此我們可以建立一個全局的內存泄露監控弱引用表,其對鍵值和內容值的引用爲“弱引用”(即其metatable中的__mode元屬性值爲"kv"),把我們關心的對象放置到該表中。然後等到某一個我們認爲此對象應該已經被回收的時刻查看一下該表中是否還存在此對象即可:若不存在,說明該對象被正確的回收了;若存在說明該對象未被正確的回收,簡言之,該對象泄露了。