Lua语言学习笔记

0. 前言

最近学习了Lua语言,记录一下本身以为对几个重要概念的学习过程。html

1. Table

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

1.1. MetaTable

MetaTable是Lua中元表。我的认为,元表是对table操做时触发的行为的集合。「触发的行为」是什么?它能够是一个function,定义这个行为作什么;也能够是一个table,定义这个行为的备选table。元表能够有不少属性,具体参照官网,我以__index为例。bash

1.1.1. __index

__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中查询,并把结果返回。这和上面的猜想是相符的。

1.2. Function的默认参数

咱们初始化一个对象,这个对象里面可能有些属性不是必填的。好比一个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
复制代码

才能作到完美的必选参数+可选参数的初始化。

2. Lua中的面向对象

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',因此输出符合预期。

3. 函数式编程

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的时候遍历一下,挨个调用就行了。

4. 总结

  1. table是Lua的一个数据结果,其行为相似于一个map。
  2. metatable是对table操做时触发的行为的一个集合。
  3. 能够用table来实现function的默认参数。
  4. 运用table + metatable能够实现简单的OOP。
  5. Lua支持函数式编程与闭包。

5. 感想

刚开始学Lua的时候,感受它就是一个动态类型的语言。学完以后,以为table很重要,只要精通table,我以为就能精通Lua的七八成。另外,学了Lua以后,有了比较,才以为Java有点啰嗦(非贬义,Java有他的道理),才能理解Kotlin中一些api为何要这么设计,以及设计的理由是什么。虽说技多不压身,可是学完以后必定要比较,我以为才能理解做者设计某一门语言的理由,它适用于什么状况,不适用于什么状况。有了比较,才能更好地使用一门语言,写出更好的代码,由于编程是一门艺术。没有比较,我以为学再多也没用。

相关文章
相关标签/搜索