前言

關於面向物件,從以前的火熱到現在逐漸冷卻,甚至網上還有很多人批評面向物件有問題。其實對面向物件應該理性的看待,它有不適用的場景,也有很多發揮作用的地方,比如像UI框架,遊戲框架,這些領域都可以用物件來建模。我們所要做的,就是根據不同的應用場景選擇合適的程式設計正規化。

Lua本身並不支援面向物件,但透過表和元表可以模擬實現差不多的效果,這一篇將實現一個可用的面向物件模組。網上也有很多類似的實現,可相互參考。

Lua的模組化

lua可以使用table來表示模組,我們在lua檔案中編寫一些函式,然後把這些函式儲存到一個表中,最後返回這個表,這就可以認為是一個模組:

local

M

=

{}

function

M

print

()

print

“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

print

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#也僅支援單繼承。前文說過,我們會謹慎的使用面向物件,僅用它來實現最上層的框架設計。所以從這點看,我認為完全足夠了。