Lua語言:使用元表實現面向物件
前言
關於面向物件,從以前的火熱到現在逐漸冷卻,甚至網上還有很多人批評面向物件有問題。其實對面向物件應該理性的看待,它有不適用的場景,也有很多發揮作用的地方,比如像UI框架,遊戲框架,這些領域都可以用物件來建模。我們所要做的,就是根據不同的應用場景選擇合適的程式設計正規化。
Lua本身並不支援面向物件,但透過表和元表可以模擬實現差不多的效果,這一篇將實現一個可用的面向物件模組。網上也有很多類似的實現,可相互參考。
Lua的模組化
lua可以使用table來表示模組,我們在lua檔案中編寫一些函式,然後把這些函式儲存到一個表中,最後返回這個表,這就可以認為是一個模組:
local
M
=
{}
function
M
。
()
(
“test”
)
end
return
M
要使用模組,需要用到require函式,用法很簡單:
local
M
=
require
(
“test2”
)
M。print
()
lua的標準庫大多數用表把功能歸類起來,像io, string, table等等。但我們並不需要require就可以直接使用,這是因為虛擬機器在載入的時候,就把這些標準庫都加進來了。require使用的時候是很簡單的,但它背後做了很多事情,深入理解它背後的機制,能讓我們更好的理解lua模組的載入過程:
首先require檢查package。loaded表,看看模組是否已經載入,如果載入就直接返回模組,這就避免require時一直重複載入。如果我們想強制重新載入模組應該怎麼做呢?只要把package。loaded的該模組設為nil即可,看下面程式碼:
package。loaded
[
“test2”
]
=
nil
local
M
=
require
(
“test2”
)
如果在package。loaded找不到模組,那麼將透過package。searchers來查詢,package。searchers是由查詢函式組成的列表,這意味著你可以增加查詢函式來修改預設的行為。而查詢函式返回的是一個loader,呼叫loader才能得到最終的模組。
package。searchers預設由4個查詢函式組成。第一個嘗試查詢package。preload[modname],看是否存在loader函式。
第二個透過package。path檢查是否存在對應的lua檔案,如果存在就呼叫loadfile載入lua檔案,載入的結果就是一個loader。所以,我們可以將lua檔案看成一個函式,檔案中的返回語句就是這個函式的返回值,從這裡也瞭解到,返回的不一定是表,也可以是函式,或是其他任意型別的物件。
第三個查詢package。cpath看是否存在對應名字的C庫,如果找到C庫,就呼叫package。loadlib載入之,然後查詢C庫中的luaopen_modname函式(modname就是我們指定的模組名),此時luaopen_modname就是loader,呼叫它得到最終的結果。
第四個查詢函式是為了處理“子模組”的情況,比如我們這樣寫:require(“a。b。c”),第2,3個查詢函式會轉換成:a/b/c,這樣。就變成路徑分隔符;第4個查詢函式不是這樣處理:它先取出a,然後透過package。cpath找到a的動態庫(如a。so, a。dll),然後載入這個動態庫,再去找luaopen_a_b_c這個函式,找到後呼叫得到返回物件。這個特性允許我們將多個模組寫在一個動態庫中,以減少動態庫的數量。
透過上面的查詢函式找到loader後,呼叫loader返回物件,將這個物件儲存到package。loaded[modname]中,下次再require直接從package。loaded[modname]返回就行了。如果loader返回的是nil,package。loaded[modname]會設定為以true,防止一直重複載入。
如果找不到loader,就會引發一個錯誤。
面向物件的實現
在前面的章節我們介紹了元表和元方法,其中__inddex函式在取不到表的key時觸發,我們利用這個特性來實現lua的面向物件機制,首先了解幾個重要的概念:
類
:這是物件的元表,在訪問不到物件的欄位時,會觸發類的
index,而
index可以設為類自己,這樣就變成訪問類的欄位。通常我們把物件的功能函式放在類中,讓該類的所有物件都共享一套函式。
類的元表
:這個表是為了實現繼承,類也找不到欄位時,會觸發類的元表的__index,這裡會取出父類並繼續訪問,如此迴圈,一直到父類為空為止。
物件
:這就是一個普通的table,把它的元表設為指定的類,那麼它就被稱為該類的物件。
具體實現的程式碼很少,先來看看:
—— rtl。lua
local
rtl
=
{}
local
ksuper
=
{}
—— 作為查詢父類的key
—— 定義一個類, super是父類,為nil表示沒有父類
function
rtl
。
class
(
super
)
local
klass
=
{}
—— 這就是代表類的table
klass
[
ksuper
]
=
super
—— 把父類table儲存起來
klass。__index
=
klass
—— __index設為自己
local
_class_metatable
=
rtl。_class_metatable
—— 這是類的元表,所有類都設定同一個元表
if
not
_class_metatable
then
_class_metatable
=
{
__index
=
function
(
t
,
k
)
—— 取得類的父類
local
super
=
rawget
(
t
,
ksuper
)
if
super
then
—— 然後繼續訪問k,如果找不到還會觸發這個__index,一直往上追溯
return
super
[
k
]
else
return
nil
end
end
,
__call
=
function
(
cls
,
。。。)
—— 建立類的物件,cls就是類
local
obj
=
{}
—— 將cls設為obj的元表,找不到obj的欄位時,就會觸發klass。__index
—— 而__index指向kclass自己,所以相當於在kclass上訪問欄位。
setmetatable
(
obj
,
cls
)
—— 約定_init為類的建構函式,所以這裡會呼叫建構函式
local
_init
=
cls。_init
if
_init
then
_init
(
obj
,
。。。)
end
—— 最後返回這個物件
return
obj
end
}
rtl。_class_metatable
=
_class_metatable
end
—— 設定類的元表
setmetatable
(
klass
,
_class_metatable
)
—— 最後返回類
return
klass
end
return
rtl
我們把這份程式碼寫在rtl。lua中,接下來看看怎麼使用它
先來實現一個類:
—— animal。lua
local
rtl
=
require
(
“rtl”
)
local
Animal
=
rtl。class
()
function
Animal
。
say
(
self
)
error
(
“I don‘t known who i am ”
)
end
return
Animal
我們有了一個Animal類,接著實現兩個子類:
—— test。lua
local
rtl
=
require
(
“rtl”
)
local
Animal
=
require
(
“animal”
)
local
Cat
=
rtl。class
(
Animal
)
—— 建構函式
function
Cat
。
_init
(
self
,
name
)
self。name
=
name
end
—— 覆蓋父類的方法
function
Cat
。
say
(
self
)
(
string。format
(
“I am a cat, my name is %s”
,
self。name
))
end
local
Dog
=
rtl。class
(
Animal
)
現在可以使用這些類來建立物件了:
—— 建立一個Cat物件,並呼叫say,注意呼叫時用:,讓cat物件作為第1個引數傳入函式
local
cat
=
Cat
(
“tom”
)
cat
:
say
()
——> I am a cat, my name is tom
—— 建立一個Dog物件,呼叫say,因為Dog並沒有實現say函式,因此呼叫到Animal的say函式。最終丟擲一個錯誤
local
dog
=
Dog
()
dog
:
say
()
——> 。\animal。lua:6: I don’t known who i am
從例子看是不是和其他語言類似呢,不過這套程式碼僅實現了單繼承,並不支援多重繼承,其實像Java和C#也僅支援單繼承。前文說過,我們會謹慎的使用面向物件,僅用它來實現最上層的框架設計。所以從這點看,我認為完全足夠了。