弱表(weak table)是一個很有意思的東西,像C++/Java等語言是沒有的。弱表的定義是:A weak table is a table whose elements are weak references,元素爲弱引用的表就叫弱表。有弱引用那麼也就有強引用,有引用那麼也就有非引用。我們先要釐這些基本概念:變量、值、類型、對象。
(1)變量與值:Lua是一個dynamically typed language,也就是說在Lua中,變量沒有類型,它可以是任何東西,而值有類型,所以Lua中沒有變量類型定義這種東西。另外,Lua中所有的值都是第一類值(first-class values)。
(2)Lua有8種基本類型:nil、boolean、number、string、function、userdata、thread、table。其中Nil就是nil變量的類型,nil的主要用途就是一個所有類型之外的類型,用於區別其他7中基本類型。
(3)對象objects:Tables、functins、threads、userdata。對於這幾種值類型,其變量皆爲引用類型(變量本身不存儲類型數據,而是指向它們)。賦值、參數傳遞、函數返回等都操作的是這些值的引用,並不產生任何copy行爲。
Lua的垃圾回收機制:gc是很多語言的常見機制,讓程序員拜託複雜易出錯的內存管理。
定義:Lua manages memory automatically by running a garbage collector to collect all dead objects (that is, objects that are no longer accessible from Lua).
三點理解:(1)gc自動運行,也可以手動調用;(2)自動收集的目標是引用計數爲0的對象;(3)dead objects:不能訪問到的對象,沒有引用指向它了,當然就是訪問不到的了,也就等同於垃圾內存了。
weak table的定義:
(1)weak表是一個表,它擁有metatable,並且metatable定義了__mode字段;
(2)weak表中的引用是弱引用(weak reference),弱引用不會導致對象的引用計數變化。換言之,如果一個對象只有弱引用指向它,那麼gc會自動回收該對象的內存。
(3)__mode字段可以取以下三個值:k、v、kv。k表示table.key是weak的,也就是table的keys能夠被自動gc;v表示table.value是weak的,也就是table的values能被自動gc;kv就是二者的組合。任何情況下,只要key和value中的一個被gc,那麼這個key-value pair就被從表中移除了( In any case, if either the key or the value is collected, the whole pair is removed from the table)。
對於普通的強引用表,當你把對象放進表中的時候,就產生了一個引用,那麼即使其他地方沒有對錶中元素的任何引用,gc也不會被回收這些對象。那麼你的選擇只有兩種:手動釋放表元素或者讓它們常駐內存。
strongTable = {} strongTable[1] = function() print("i am the first element") end strongTable[2] = function() print("i am the second element") end strongTable[3] = {10, 20, 30} print(table.getn(strongTable)) -- 3 collectgarbage() print(table.getn(strongTable)) -- 3
但是,在編程環境中,有時你並不確定手動給一個鍵值賦nil的時機,而是需要等所有使用者用完以後進行釋放,在釋放以前,是可以訪問這個鍵值對的。這種時候,weak表就派上用場了。關於weak table的理解,看下面這個小例子:
weakTable = {} weakTable[1] = function() print("i am the first element") end weakTable[2] = function() print("i am the second element") end weakTable[3] = {10, 20, 30} setmetatable(weakTable, {__mode = "v"}) -- 設置爲弱表 print(table.getn(weakTable)) -- 3 ele = weakTable[1] -- 給第一個元素增加一個引用 collectgarbage() print(table.getn(weakTable)) -- 1,第一個函數引用爲1,不能gc ele = nil -- 釋放引用 collectgarbage() print(table.getn(weakTable)) -- 0,沒有其他引用了,全部gc
當然在實際的代碼過程中,我們不一定需要手動collectgarbage,因爲該函數是在後臺自動運行的,它有自己的運行週期和規律,對編程者來說是透明的。
注意:只有擁有顯示構造的對象類型會被自動從weak表中移除,值類型boolean、number是不會自動從weak中移除的。而string類型雖然也由gc來負責清理,但是string沒有顯示的構造過程,因此也不會自動從weak表中移除,對於string的內存管理有單獨的策略。
基於weak表的簡單應用:
(1)記憶函數:一個相當普遍的編程技術是用空間來換取時間。你可以通過記憶函數結果來進行優化,當你用同樣的參數再次調用函數時,它可以自動返回記憶的結果。將函數的輸入和輸出分別作爲key和value放在一個weak table裏面,調用函數之前先查看有無現成的結果,有就返回,沒有就調用函數,然後將結果存入表中。由於是weak table,此表會定期自動清理掉不再有引用的鍵值對。
(2)關聯對象屬性:Lua本身使用這種技術來保存數組的大小。table庫提供了一個函數來設定數組的大小,另一個函數來讀取數組的大小。當你設定了一個數組的大小,Lua 將這個尺寸保存在一個私有的weak table,索引就是數組本身,而value就是它的尺寸。
同樣的,當我們需要給任一對象添加一個屬性的時候,可以在外部單獨做一弱key表,然後以對象爲key值,屬性值爲value。這樣即可以方便的訪問這個屬性,也不影響該對象的釋放。而且對象本身沒任何修改,能很好的保持對象本身的獨立性。
(3)帶有默認值的表:
有兩種實現方法,第一種方法,使用關聯對象屬性的方法,將表作爲key,默認值作爲value,存到一個弱key的weak表中:
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
第二種方法,針對不同的metatable來進行優化,對於每一個具體的默認值,生成一個與之對應的metatable,然後以默認值爲key,metatable爲value,存到一個弱value的weak表中:
metas = {} setmetatable(metas, {__mode = "v"}) setdefault = function (t, d) local mt = metas[d] if mt == nil then mt = {__index = function() return d end} metas[d] = mt end setmetatable(t, mt) end
兩種方式各有利弊,第一種方法對於每一個table都需要添加一個鍵值對,但是公用一個metatable。第二種方法需要許多個不同的metatable,但擁有相同默認值的table共用一個metatable,並且weak表要比第一種方法小。如果你的代碼環境中有很多個table,但常用默認值只有那麼幾種,建議選擇第二種方法,否則就選擇第一種方法。