0. 前言
最近学习了Lua语言,记录一下自己觉得对几个重要概念的学习过程。
1. Table
table是Lua语言的一个重要的数据结构。它很像一个Map,我们可以通过给出一个key来获得对应的value。并且,table的key可以是除nil以外的任意类型。看代码:
1 | local tab = {} |
Lua的table不止于此,还有很多骚操作。
1.1. MetaTable
MetaTable是Lua中元表。个人认为,元表是对table操作时触发的行为的集合。「触发的行为」是什么?它可以是一个function,定义这个行为做什么;也可以是一个table,定义这个行为的备选table。元表可以有很多属性,具体参照官网,我以__index为例。
1.1.1. __index
__index定义了在table中通过给定的key找到的value为nil时怎么办的行为。话不多说看代码:
1 | local aTable = {} |
首先先声明和定义两个table,aMetatable后面用作aTable的元表。元表同样也是一个表,所以这么声明没毛病。然后获取aTable的y属性的值,不用想,肯定是获得的是一个空值。接着,把aTable的元表设为aMetatable,然后再获取一次aTable的y属性的值。同样的,获得的是一个空值。为什么?因为aTable的元表没有任何可以触发的行为。那就为aTable的元表增加一个行为__index,在打印一个aTable的y属性的值,这会就打印出666了。总结一下这个过程:当我们访问aTable的y属性时,Lua虚拟机发现它是空值,所以他就会在aTable的元表中找到__index这个属性,如果这个属性是一个function,那就执行它,并把它的执行结果,返回作aTable的y属性的值。
当然上面的代码在设置元表时可以更加简化:
1 | aMetatable.__index = { y = 666 } |
执行完这段语句,元表中__index这个行为就是一个table了。这个当我们访问aTable的y属性时,Lua虚拟机发现aTable.y是空的,就会去aMetatable.__index这个「表」里面把y作为key去取一个值并返回。这与上面的代码是等价的。
然而我总感觉还少了点什么,上面的代码,我只是根据输出来猜测它的行为,而不能确定它是怎么做到的。于是我在Lua的源代码里,全局搜索关键词「__index」,成功定位到__index的实现:
1 | /* |
解释一下,首先定义声明一个loop防止死循环,tm存储在元表中查找__index的结果。至于为什么要防止死循环可以不管,因为不是我们读源码的目的。接着定位到for循环内的第一个if-else分支,if分支内,注释说这是t不是一个table的情况。我们可以跳过,看看else分支,else分支是t是table的情况。else分支会去找table: t的元表,如果找到的元表为空,或者是元表中找不到__index属性,那就把结果设置为空,提前返回。如果找到了__index那就继续。接着看第二个if分支,如果__index是一个函数,那就用luaT_callTM调用它,luaT_callTM的代码如下:
1 | void luaT_callTM (lua_State *L, const TValue *f, const TValue *p1, |
可以看到,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:
1 | function initPerson(name, age, sex, height, weight) |
输出不符合我们的预期,因为Lua在传递参数是会把实参顺序推入到栈中,再按顺序对号入座到形参。如何解决默认参数的问题,我们可以传入一个table,这个table中以key为参数,value为参数的值。在初始化person的函数中,我们用key来在传来的table中取出对应参数的值,如果取出来的value为空,那就或一下,给它设置一个默认值就好了。代码如下:
1 | function initPerson( tPerson ) |
结果符合预期。不过,上面的代码,严格意义上来说,person的五个属性都成了可选参数,因为开发者是可能会忘了填name、age或sex属性。解决方法是:要么在开发的时候,开发者要知道name,age和sex一定要填值;要么就直接把name,age和sex单独抽出来,在加上一个table作为initPerson的参数列表,像这样
1 | function initPerson(name, age, sex, tOptArgs ) |
才能做到完美的必选参数+可选参数的初始化。
2. Lua中的面向对象
Lua支持一定的OOP。Lua本身没有提供面向对象编程的支持,当时我们可以用Lua的一个重要数据结构「table」来模拟OOP的过程。不多说,上代码。
1 | MyObject = { |
MyObject这个表,有两个属性,name和doWhat,我们可以把它看做一个“类”;并且还定义了两个方法newInstance和doSomething。形如「XXX.xxx()」和「XXX:xxx()」的形式是Lua语言的语法糖,同样都是在“类”中声明一个函数:
1 | // 1 |
上面的代码中,三者是等价的,同样为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还有另一个版本:
1 | function MyObject:newInstance( obj ) |
更加的简化,意思是如果在obj中,根据的key到的结果是空,那就用这个key去self中查找,并作为查询结果。(这个版本我一开始无法理解,看了Lua的源码才知道是什么意思,还是function版的好理解..)
回到newInstance中,接下来就是为obj设置一些属性,然后返回。在doSomething中,因为我们执行的是
1 | oneObj:doSomething() |
所以在doSomething中,self就是oneObj。oneObj的name属性和doWhat属性是’Q’和’eat’,所以输出符合预期。
3. 函数式编程
Lua支持函数式编程。因为我之前更熟悉Java,转到Lua一时半会理解不了函数式编程。所以新的概念,我喜欢和Java比较。Lua中的函数式编程,就是把function看成是一个「值」,你可以在任意一个地方声明它,也可以把它赋值到某一个变量中。所以,只要把Lua中的函数当成一个值就好了,只不过这个值不能加减乘除和逻辑变换罢了。所以,下面的代码在Lua中是合法的:
1 | local f = function() |
可以看到上面的代码,test中有嵌套了一个function。我在想,如果这个function访问了test的局部变量,那会是什么情形?做个实验:
1 | function getIncreaser() |
讲道理,getIncreaser的level仅在getIncreaser的生命周期内有效。然后,getIncreaser返回的function中持有了level,所以在getIncreaser退出后,level并没有释放,因为increaser持有了它。所以每调用一次increaser,level就会自增一次,就是一个简单的自增器。这种现象,有一个很厉害的名字,叫做「闭包(Closure)」
简单的了解了函数式编程后,我继续和Java比较。Java中,回调函数怎么做?传一个函数?不行,因为Java不能把function作为参数。那就把这个function包装成一个类,再把这个类的实例作为参数就好了:
1 | public interface Callback { |
好啰嗦啊,我只是要回调而已,如果是观察者模式,那我还要维护一个List。Lua支持函数式编程,那就只需这样:
1 | function setCallback(callback) |
很简洁。如果是观察者模式,那就把callback插入到一个table就可以了,需要notify的时候遍历一下,挨个调用就好了。
4. 总结
- table是Lua的一个数据结果,其行为类似于一个map。
- metatable是对table操作时触发的行为的一个集合。
- 可以用table来实现function的默认参数。
- 运用table + metatable可以实现简单的OOP。
- Lua支持函数式编程与闭包。
5. 感想
刚开始学Lua的时候,感觉它就是一个动态类型的语言。学完之后,觉得table很重要,只要精通table,我觉得就能精通Lua的七八成。另外,学了Lua之后,有了比较,才觉得Java有点啰嗦(非贬义,Java有他的道理),才能理解Kotlin中一些api为什么要这么设计,以及设计的理由是什么。虽然说技多不压身,但是学完之后一定要比较,我觉得才能理解作者设计某一门语言的理由,它适用于什么情况,不适用于什么情况。有了比较,才能更好地使用一门语言,写出更好的代码,因为编程是一门艺术。没有比较,我觉得学再多也没用。