最近學習了Lua語言,記錄一下本身以爲對幾個重要概念的學習過程。html
table是Lua語言的一個重要的數據結構。它很像一個Map,咱們能夠經過給出一個key來得到對應的value。而且,table的key能夠是除nil之外的任意類型。看代碼:編程
local tab = {}
tab.a = 1
tab['b'] = '233'
tab[f] = function()
print('call a function')
end
for k, v in pairs(tab) do
print(string.format('tab.%s = %s', tostring(k), tostring(v)))
end
-- Output:
-- tab.a = 1
-- tab.b = 233
-- tab.f = function
複製代碼
Lua的table不止於此,還有不少騷操做。api
MetaTable是Lua中元表。我的認爲,元表是對table操做時觸發的行爲的集合。「觸發的行爲」是什麼?它能夠是一個function,定義這個行爲作什麼;也能夠是一個table,定義這個行爲的備選table。元表能夠有不少屬性,具體參照官網,我以__index爲例。bash
__index定義了在table中經過給定的key找到的value爲nil時怎麼辦的行爲。話很少說看代碼:數據結構
local aTable = {}
local aMetatable = {}
print(aTable.y)
setmetatable(aTable, aMetatable)
print(aTable.y)
aMetatable.__index = function(t,k)
-- t就是aTable
local tempTable = { y = 666 }
return tempTable[k]
end
print(aTable.y)
-- Output:
-- nil
-- nil
-- 666
複製代碼
首先先聲明和定義兩個table,aMetatable後面用做aTable的元表。元表一樣也是一個表,因此這麼聲明沒毛病。而後獲取aTable的y屬性的值,不用想,確定是得到的是一個空值。接着,把aTable的元表設爲aMetatable,而後再獲取一次aTable的y屬性的值。一樣的,得到的是一個空值。爲何?由於aTable的元表沒有任何能夠觸發的行爲。那就爲aTable的元表增長一個行爲__index,在打印一個aTable的y屬性的值,這會就打印出666了。總結一下這個過程:當咱們訪問aTable的y屬性時,Lua虛擬機發現它是空值,因此他就會在aTable的元表中找到__index這個屬性,若是這個屬性是一個function,那就執行它,並把它的執行結果,返回做aTable的y屬性的值。閉包
固然上面的代碼在設置元表時能夠更加簡化:函數式編程
aMetatable.__index = { y = 666 }
複製代碼
執行完這段語句,元表中__index這個行爲就是一個table了。這個當咱們訪問aTable的y屬性時,Lua虛擬機發現aTable.y是空的,就會去aMetatable.__index這個「表」裏面把y做爲key去取一個值並返回。這與上面的代碼是等價的。函數
然而我總感受還少了點什麼,上面的代碼,我只是根據輸出來猜想它的行爲,而不能肯定它是怎麼作到的。因而我在Lua的源代碼裏,全局搜索關鍵詞「__index」,成功定位到__index的實現:oop
/*
** Finish the table access 'val = t[key]'.
** if 'slot' is NULL, 't' is not a table; otherwise, 'slot' points to
** t[k] entry (which must be nil).
*/
void luaV_finishget (lua_State *L, const TValue *t, TValue *key, StkId val,
const TValue *slot) {
int loop; /* counter to avoid infinite loops */
const TValue *tm; /* metamethod */
for (loop = 0; loop < MAXTAGLOOP; loop++) {
if (slot == NULL) { /* 't' is not a table? */
lua_assert(!ttistable(t));
tm = luaT_gettmbyobj(L, t, TM_INDEX);
if (ttisnil(tm))
luaG_typeerror(L, t, "index"); /* no metamethod */
/* else will try the metamethod */
}
else { /* 't' is a table */
lua_assert(ttisnil(slot));
tm = fasttm(L, hvalue(t)->metatable, TM_INDEX); /* table's metamethod */ if (tm == NULL) { /* no metamethod? */ setnilvalue(val); /* result is nil */ return; } /* else will try the metamethod */ } if (ttisfunction(tm)) { /* is metamethod a function? */ luaT_callTM(L, tm, t, key, val, 1); /* call it */ return; } t = tm; /* else try to access 'tm[key]' */ if (luaV_fastget(L,t,key,slot,luaH_get)) { /* fast track? */ setobj2s(L, val, slot); /* done */ return; } /* else repeat (tail call 'luaV_finishget') */ } luaG_runerror(L, "'__index' chain too long; possible loop"); } 複製代碼
解釋一下,首先定義聲明一個loop防止死循環,tm存儲在元表中查找__index的結果。至於爲何要防止死循環能夠無論,由於不是咱們讀源碼的目的。接着定位到for循環內的第一個if-else分支,if分支內,註釋說這是t不是一個table的狀況。咱們能夠跳過,看看else分支,else分支是t是table的狀況。else分支會去找table: t的元表,若是找到的元表爲空,或者是元表中找不到__index屬性,那就把結果設置爲空,提早返回。若是找到了__index那就繼續。接着看第二個if分支,若是__index是一個函數,那就用luaT_callTM調用它,luaT_callTM的代碼以下:學習
void luaT_callTM (lua_State *L, const TValue *f, const TValue *p1,
const TValue *p2, TValue *p3, int hasres) {
ptrdiff_t result = savestack(L, p3);
StkId func = L->top;
setobj2s(L, func, f); /* push function (assume EXTRA_STACK) */
setobj2s(L, func + 1, p1); /* 1st argument */
setobj2s(L, func + 2, p2); /* 2nd argument */
L->top += 3;
if (!hasres) /* no result? 'p3' is third argument */
setobj2s(L, L->top++, p3); /* 3rd argument */
/* metamethod may yield only when called from Lua code */
if (isLua(L->ci))
luaD_call(L, func, hasres);
else
luaD_callnoyield(L, func, hasres);
if (hasres) { /* if has result, move it to its place */
p3 = restorestack(L, result);
setobjs2s(L, p3, --L->top);
}
}
複製代碼
能夠看到,luaT_callTM先把棧的狀態保存起來,再把__index這個函數,及其第一個參數,第二個參數推入,由於hasres爲1,因此第一個if分支不執行。接着,第二個if-else就調用__index方法。到了第三個if分支,由於hasres爲1,因此會執行這個分支。這個if分支會還原棧的狀態,並把結果賦值給p3,也就是上游傳過來的val,而後把結果推入棧中。結束。 再回到luaV_finishget,到了最後一個if分支,看代碼的意思,就是直接把__index當作一個table,在這個table中以給定的key查找value,並把查找結果返回。至此__index的實現原理就結束了。 結論是,若是__index是一個function,那就會把原table以及key傳入給這個function,這個function處理後把結果返回,Lua虛擬機會把這個結果當作是查詢結果;若是__index是一個table,那就用給定的key在__index中查詢,並把結果返回。這和上面的猜想是相符的。
咱們初始化一個對象,這個對象裏面可能有些屬性不是必填的。好比一個person,它的屬性name、age、sex都是必填的,而height、weight是選填的。咱們很天然的就會這麼定義一個函數來初始化person:
function initPerson(name, age, sex, height, weight)
-- 初始化..
local person = getDefault()
person.name = name
person.age = age
person.sex = sex
person.height = height or 0
person.weight = weight or 0
return person
end
function printPerson( person )
print(string.format(
'name = %s, age = %d, sex = %s, height = %d, weight = %d',
person.name,
person.age,
person.sex,
person.height,
person.weight
))
end
-- 僅傳入必填屬性
local p1 = initPerson('Q1', 23, 'female')
printPerson(p1)
-- 傳入必填屬性+身高?
local p2 = initPerson('Q2', 23, 'female', 169)
printPerson(p1)
-- 傳入必填屬性+體重?
local p3 = initPerson('Q3', 23, 'female', 55)
printPerson(p1)
-- Output:
-- name = Q1, age = 23, sex = female, height = 0, weight = 0
-- name = Q2, age = 23, sex = female, height = 169, weight = 0
-- name = Q3, age = 23, sex = female, height = 55, weight = 0
複製代碼
輸出不符合咱們的預期,由於Lua在傳遞參數是會把實參順序推入到棧中,再按順序對號入座到形參。如何解決默認參數的問題,咱們能夠傳入一個table,這個table中以key爲參數,value爲參數的值。在初始化person的函數中,咱們用key來在傳來的table中取出對應參數的值,若是取出來的value爲空,那就或一下,給它設置一個默認值就行了。代碼以下:
function initPerson( tPerson )
-- 初始化..
local person = getDefault()
person.name = tPerson.name
person.age = tPerson.age
person.sex = tPerson.sex
person.height = tPerson.height or 0
person.weight = tPerson.weight or 0
return person
end
-- 僅傳入必填屬性
local p1 = initPerson({name = 'Q1', age = 23, sex = 'female'})
printPerson(p1)
-- 傳入必填屬性+身高?
local p2 = initPerson({name = 'Q1', age = 23, sex = 'female', height = 169})
printPerson(p1)
-- 傳入必填屬性+體重?
local p3 = initPerson({name = 'Q1', age = 23, sex = 'female', weight = 55})
printPerson(p1)
-- Output:
-- name = Q1, age = 23, sex = female, height = 0, weight = 0
-- name = Q2, age = 23, sex = female, height = 169, weight = 0
-- name = Q3, age = 23, sex = female, height = 0, weight = 55
複製代碼
結果符合預期。不過,上面的代碼,嚴格意義上來講,person的五個屬性都成了可選參數,由於開發者是可能會忘了填name、age或sex屬性。解決方法是:要麼在開發的時候,開發者要知道name,age和sex必定要填值;要麼就直接把name,age和sex單獨抽出來,在加上一個table做爲initPerson的參數列表,像這樣
function initPerson(name, age, sex, tOptArgs )
-- 初始化..
local person = getDefault()
person.name = name
person.age = age
person.sex = sex
tOptArgs = tOptArgs or {}
person.height = tOptArgs.height or 0
person.weight = tOptArgs.weight or 0
return person
end
複製代碼
才能作到完美的必選參數+可選參數的初始化。
Lua支持必定的OOP。Lua自己沒有提供面向對象編程的支持,當時咱們能夠用Lua的一個重要數據結構「table」來模擬OOP的過程。很少說,上代碼。
MyObject = {
name = "MyObject",
doWhat = "something"
}
function MyObject:newInstance( obj )
obj = obj or {}
setmetatable(obj, self)
self.__index = function(t,k)
return self[k]
end
obj.name = "Q"
obj.fieldB = "eat"
return obj
end
function MyObject:doSomething()
print(string.format('%s do %s.', self.name, self.doWhat))
end
local oneObj = MyObject:newInstance()
oneObj:doSomething()
-- Output:
-- Q do eat.
複製代碼
MyObject這個表,有兩個屬性,name和doWhat,咱們能夠把它看作一個「類」;而且還定義了兩個方法newInstance和doSomething。形如「XXX.xxx()」和「XXX:xxx()」的形式是Lua語言的語法糖,一樣都是在「類」中聲明一個函數:
// 1
Person.say = function(self)
end
// 2
function Person.say(self)
end
// 3
function Person:say()
end
複製代碼
上面的代碼中,三者是等價的,一樣爲Person中的say屬性賦值一個函數。對於1和2,2是Lua的語法糖,2等價於1。對於2和3,3是Lua的語法糖,「.」號和「:」號的區別在於,「:」號會在調用函數時,首先推入一個self,再推入函數的參數。
而後看看newInstance函數。它首先對obj進行或操做,確保傳進來的obj不爲空,保證其至少是一個空表。而後,就是爲obj設置元表,設置爲self,而self就是MyObject。接着就是爲self設置一個屬性__index,這個屬性的值是一個function。和上面的setmetatable聯合來看,這兩句語句的意思是: 若是在obj中,根據一個key找到的結果是nil,那就去執行__index這個function。在這個function中,會去查找self這個表並返回,self就是MyObject。因此,若是咱們訪問obj的doSomething屬性,由於obj沒有,那就執行__index,在MyObject中查找,找到了,那就返回做查詢結果。因此newInstance還有另外一個版本:
function MyObject:newInstance( obj )
obj = obj or {}
setmetatable(obj, self)
self.__index = self
obj.name = "Q"
obj.fieldB = "eat"
return obj
end
複製代碼
更加的簡化,意思是若是在obj中,根據的key到的結果是空,那就用這個key去self中查找,並做爲查詢結果。(這個版本我一開始沒法理解,看了Lua的源碼才知道是什麼意思,仍是function版的好理解..)
回到newInstance中,接下來就是爲obj設置一些屬性,而後返回。在doSomething中,由於咱們執行的是
oneObj:doSomething()
複製代碼
因此在doSomething中,self就是oneObj。oneObj的name屬性和doWhat屬性是'Q'和'eat',因此輸出符合預期。
Lua支持函數式編程。由於我以前更熟悉Java,轉到Lua一時半會理解不了函數式編程。因此新的概念,我喜歡和Java比較。Lua中的函數式編程,就是把function當作是一個「值」,你能夠在任意一個地方聲明它,也能夠把它賦值到某一個變量中。因此,只要把Lua中的函數當成一個值就行了,只不過這個值不能加減乘除和邏輯變換罷了。因此,下面的代碼在Lua中是合法的:
local f = function()
return '2333'
end
function test()
print(f())
f = function()
return '666'
end
print(f())
end
-- Output:
-- 2333
-- 666
{% endcodeblock %}
能夠看到上面的代碼,test中有嵌套了一個function。我在想,若是這個function訪問了test的局部變量,那會是什麼情形?作個實驗:
{% codeblock lang:Lua %}
function getIncreaser()
local level = 0
return function()
level = level + 1
return level
end
end
local increaser = getIncreaser()
for i = 1, 5 do
print(increaser())
end
-- Output:
-- 1
-- 2
-- 3
-- 4
-- 5
複製代碼
講道理,getIncreaser的level僅在getIncreaser的生命週期內有效。而後,getIncreaser返回的function中持有了level,因此在getIncreaser退出後,level並無釋放,由於increaser持有了它。因此每調用一次increaser,level就會自增一次,就是一個簡單的自增器。這種現象,有一個很厲害的名字,叫作「閉包(Closure)」
簡單的瞭解了函數式編程後,我繼續和Java比較。Java中,回調函數怎麼作?傳一個函數?不行,由於Java不能把function做爲參數。那就把這個function包裝成一個類,再把這個類的實例做爲參數就行了:
public interface Callback {
void callback();
}
public class MyProcessor {
private Callback mCallback;
public void setCallback(Callback callback) {
mCallback = callback
}
public void notifyCallback() {
if (mCallback != null) {
mCallback.callback();
}
}
}
複製代碼
好囉嗦啊,我只是要回調而已,若是是觀察者模式,那我還要維護一個List。Lua支持函數式編程,那就只需這樣:
function setCallback(callback)
myProcessor.callback = callback
end
function notifyCallback()
if myProcessor.callback then
myProcessor.callback()
end
end
複製代碼
很簡潔。若是是觀察者模式,那就把callback插入到一個table就能夠了,須要notify的時候遍歷一下,挨個調用就行了。
剛開始學Lua的時候,感受它就是一個動態類型的語言。學完以後,以爲table很重要,只要精通table,我以爲就能精通Lua的七八成。另外,學了Lua以後,有了比較,才以爲Java有點囉嗦(非貶義,Java有他的道理),才能理解Kotlin中一些api爲何要這麼設計,以及設計的理由是什麼。雖說技多不壓身,可是學完以後必定要比較,我以爲才能理解做者設計某一門語言的理由,它適用於什麼狀況,不適用於什麼狀況。有了比較,才能更好地使用一門語言,寫出更好的代碼,由於編程是一門藝術。沒有比較,我以爲學再多也沒用。