Lua中類的實現理探討(Lua中實現類的方法)

NO IMAGE

Lua中沒有類的概念,但我們可以利用Lua本身的語言特性來實現類。

下文將詳細的解釋在Lua中實現類的原理,涉及到的細節點將拆分出來講,相信對Lua中實現類的理解有困難的同學將會釋疑。

類是什麼?

想要實現類,就要知道類到底是什麼。

在我看來,類,就是一個自己定義的變數型別。它約定了一些它的屬性和方法,是屬性和方法的一個集合。

所有的方法都需要一個名字,即使是匿名函式實際上也有個名字。這就形成了方法名和方法函式的鍵值對映關係,即方法名為鍵,對映的值為方法函式。

比如說有一個類是人,人有一個說話的方法,那就相當於,人(Person)是一個類,說話(talk)是它的一個方法名,說話函式是它的實際說話所執行到的內容。

人也有一個屬性,比如性別,性別就是一個鍵(sex),性別的實際值就是這個鍵所對應的內容。

理解了類實際上是一個鍵值對的集合,我們不難想到用Lua中自帶的表來實現類。

例項是什麼?

如果理解了類實際就是一個鍵值對映的表,那麼我們再來理解例項是什麼。

例項就是具有類的屬性和方法的集合,也是一個表了。聽起來好像和類差不多?

類全域性只有一個集合,相當於上帝,全域性只有一塊記憶體;而例項就普通了,普天之下有那麼多人,你可以叫A說一句話,A便執行了他的說話方法,但是不會影響B的說話。因為他們是例項,彼此分配著不同的記憶體。

說了那麼多廢話,其實例項就是由類建立出來的值,試著把類想象成型別而不是類。

兩個語法糖

試著建立一個人類 Person

複製程式碼 程式碼如下:
Person = {name=”這個人很懶”}

以上程式碼將Person初始化為一個表,這個表擁有一個為name的鍵,其預設值是”這個人很懶”。

說成白話就是人類擁有一個叫名字的屬性。

那就再賦予人類一個說話的功能吧。

複製程式碼 程式碼如下:
Person.talk = function(self, words)
    print(self.name..”說:”..words)
end

以上程式碼在Person表中加入一個鍵值對,鍵為talk,值為一個函式。

好了,只要呼叫,Person.talk(Person, “你好”),將會列印出:這個人很懶說:你好。

不過在寫程式時,大家都習慣把function放在前面,這就是函式的語法糖:

複製程式碼 程式碼如下:
function Person.talk(self, words)
    print(self.name..”說:”..words)
end

這與上面的函式定義是等價的,但是這麼寫你就很難看出來talk其實是Person表中的一個鍵,其對應的值為一個函式。

當然嘴巴都是長在自己身上的,說話只能自己說,不可能自己張嘴別人說話,所以每次都傳個self引數實在是有點不美觀,於是冒號語法糖上場。

我們還可以這麼定義人類的說話功能:

複製程式碼 程式碼如下:
function Person:talk(words)
    print(self.name..”說:”..words)
end

這與上面兩段程式碼都是等價的,它的變化是少了self的引數,將點Person.talk改為了冒號Person:talk。

但是函式體內,卻依然可以使用self,在使用:代替.時,函式的引數列表的第一個引數不再是words,Lua會自動將self做為第一個引數。這個self引數代表的意思就是這個函式的實際呼叫者。

所以我們呼叫Person:talk(“你好”)與Person.talk(Person, “你好”)是等價的,這就是冒號語法糖帶來的便利。

如何查詢表中的元素?

下面我們需要理解在Lua的表中是怎麼查詢一個鍵所對應的值的。

假設我們要在表p中查詢talk這個鍵所對應的值,請看下面的流程圖:
複製程式碼 程式碼如下:
p中有沒有talk這個鍵? 有 –> 返回talk對應的值
        |
       沒有
        |
p中是否設定過metatable? 否 –>  返回nil
        |
        有
        |
在p的metatable中有沒有__index這個鍵? 沒有 –>  返回nil
        |
        有
        |     
在p的metatable中的__index這個鍵對應的表中有沒有talk這個鍵? 沒有 –> 返回nil
        |
        有,返回getmetatable(p).__index.talk

理解以上內容是本文的重點,反覆閱讀直至你記住了。

可以看到,由於metatable和__index這兩個神奇的東西,Lua能在當前表中不存在這個鍵的時候找到其返回值。

下面將會講一講metatable這個語言特性。

對metatable的理解

metatable是什麼?

metatable的中文名叫做元表。它不是一個單獨的型別,元表其實就是一個表。

我們知道在Lua中表的操作是有限的,例如表不能直接相加,不能進行比較操作等等。

元表的作用就是增加和改變表的既定操作。只有設定過元表的表,才會受到元表的影響而改變自身的行為。

通過全域性方法setmetatable(t, m),會將表t的元表設定為表m。通過另一個全域性方法getmetatable(t)則會返回它的元表m。

注意:所有的表都可以設定元表,然而新建立的空表如果不設定,是沒有元表的。

元方法

元表作為一個表,可以擁有任意型別的鍵值對,其真正對被設定的表的影響是Lua規定的元方法鍵值對。

這些鍵值對就是Lua所規定的鍵,比如前面說到的__index,__add,__concat等等。這些鍵名都是以雙斜槓__為字首。其對應的值則為一個函式,被稱為元方法(metamethod),這些元方法定義了你想對錶自定義的操作。

例如:前面所說的__index鍵,在Lua中它所對應的元方法執行的時機是當查詢不存在於表中的鍵時應該做的操作。考慮以下程式碼:
複製程式碼 程式碼如下:
–定義元表m
m = {}
–定義元表的__index的元方法
–對任何找不到的鍵,都會返回”undefined”
m.__index = function ( table, key )
  return “undefined”
end  
 
–表pos
pos = {x=1, y=2}
–初始沒有元表,所以沒有定義找不到的行為
–因為z不在pos中,所以直接返回nil
print(pos.z) — nil
–將pos的元表設為m
setmetatable(pos, m)
–這是雖然pos裡仍然找不到z,但是因為pos有元表,
–而且元表有__index屬性,所以執行其對應的元方法,返回“undefined”
print(pos.z) — undefined

pos表中本沒有z這個鍵,通過設定pos的元表為m,並設定m的__index對應的方法,這樣所有取不到的鍵都會返回“undefined”了。

以上我們瞭解到,元表的__index屬性實際上是給表配備了找不到鍵時的行為。

注意:元表的__index屬性對應的也可以為一個表。

再舉個栗子,希望能夠加深對元表和元方法的理解,__add鍵,考慮以下程式碼:
複製程式碼 程式碼如下:
–建立元表m,其中有__add鍵和其定義的方法
local m = {
  __add = function(t1, t2)
    local sum = {}
    for key, value in pairs(t1) do
      sum[key] = value
    end
 
    for key, value in pairs(t2) do
      if sum[key] then
        sum[key] = sum[key] value
      else
        sum[key] = value
      end
    end
    return sum
  end
}
 
–將table1和table2都設定為m
local table1 = setmetatable({10, 11, 12}, m)
local table2 = setmetatable({13, 14, 15}, m)
 
–表本來是不能執行 操作的,但是通過元表,我們做到了!
for k, v in pairs(table1 table2) do
  print(k, v)
end
–print
–1 23
–2 25
–3 27

表本身是不能用 連起來計算的,但是通過定義元表的__add的方法,並setmetatable到希望有此操作的表上去,那些表便能進行加法操作了。

因為元表的__add屬性是給表定義了使用 號時的行為。

類的實現手段

好,假設前面的內容你都沒有疑問的閱讀完畢話,我們開始進入正題。

請先獨立思考一會,我們該怎麼去實現一個Lua的類?

思考ing…

種種鋪墊後,我們的類是一個表,它定義了各種屬性和方法。我們的例項也是一個表,然後我們類作為一個元表設定到例項上,並設定類的__index值為自身。

例如人類:
複製程式碼 程式碼如下:
–設定Person的__index為自身
Person.__index = Person  
 
–p是一個例項
local p = {}
 
–p的元表設定為Person
setmetatable(p, Person)
 
p.name = “路人甲”
 
–p本來是一個空表,沒有talk這個鍵
–但是p有元表,並且元表的__index屬性為一個表Person
–而Person裡面有talk這個鍵,於是便執行了Person的talk函式
–預設引數self是呼叫者p,p的name屬性為“路人甲”
p:talk(“我是路人甲”)
 
–於是得到輸出
–路人甲說:我是路人甲

為了方便,我們給人類一個建立函式create:

複製程式碼 程式碼如下:
function Person:create(name)
    local p = {}
    setmetatable(p, Person)
    p.name = name
    return p
end
 
local pa = Person:create(“路人甲”)
local pb = Person:create(“路人乙”)
pa:talk(“我是路人甲”) –路人甲說:我是路人甲
pb:talk(“我是路人乙”) –路人乙說:我是路人乙

這樣我們可以很方便用Person類建立出pa和pb兩個例項,這兩個例項都具備Person的屬性和方法。

以上便是Lua實現一個類的方法,至於類的繼承,當成一次練習吧,請大家思考~

您可能感興趣的文章:

Lua實現類繼承Lua中類的實現