Lua_isms
Posted 程序员不是码农
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Lua_isms相关的知识,希望对你有一定的参考价值。
《Programming in Lua》的第三部分,主要介绍Lua中的迭代器,for循环,元表和元方法,面向对象编程,环境,垃圾回收,协程以及反射,这部分内容有点多,并且大多以代码进行讲解,这些知识点也Lua编程中非常重要的。
Iterators and Closures(迭代器和闭包)
迭代器是一个允许我们遍历集合中所有元素的结构体,Lua中通常使用函数来表示迭代器,每次调用返回集合中的下一个元素,比如之前用到的io.read,每次调用都返回标准输入文件的下一行,如果没有数据可读了就返回nil。
任何迭代器都需要记住连续两次调用之间的状态,闭包提供了很好的机制来记录这些状态,实现这样一个机制需要两个函数:一个是闭包,另一个是工厂函数(用来创建闭包及其非静态变量)。以下为列表实现一个简单的迭代器:
接下来提供两种使用上述迭代器的方法:
迭代器的实现可能会很复杂,但是对于使用者来说是很简单的,而大多数情况,我们不需要自己去实现一个迭代器,而只是去使用。
The Semanitics of the Generic for
上述迭代器(while循环)有一个缺点:每次开启一个新的循环都需要创建一个新的闭包。
而for循环可自己保存迭代的状态来避免这种开销。其通常会保存三个值:迭代器函数,不变状态和一个控制变量。for的用法如下
ipairs
ipairs使用和实现如下:
当调用for循环时,其会获得3个值:迭代器函数iter,不变状态table t, 以及控制变量初始值0,接着Lua调用iter(t, 0), 其结果为1, t[1], 第二次迭代时,Lua调用iter(t, 1),其结果为2, t[2],直到遍历完所有元素。
pairs
pairs同ipairs类似,也是遍历table的所有元素,但是其用的迭代器是Lua原生的函数next,如下:
Traversing Tables in Order(有序遍历Table)
考虑下面这个table,按照字母顺序打印。
若使用pairs进行遍历,得到的结果是无序的,因为其使用next。因此需要用数组将table的key保存,对该table排序后再打印,如下:
Exercise
当遇到未知操作时,元表允许我们改变值的行为,比如,让两个table相加a + b
,会检查其中是否有元表,及其元表是否有__add
字段,有的话,就会调用相应的元方法。
table默认是没有元表的,可以通过setmetatable
来设置一个table的元表,如下:
字符串库给字符串默认设置了元表,其他类型的值是没有元表的
Arithmetic Metamethods(算数元方法)
用table来表示集合,并实现并集、交集等方法,如下:
接下来用元表来实现通过+号来求得其并集,在创建set的时候,需要设置其元表:
Relation Metamethods(关系元方法)
元表允许我们通过元方法(__eq, __lt, __le)赋予关系操作符意义。接下来通过元方法判断两个集合的关系:
The __index metamethod
之前提到,当我们访问table中不存在的字段时,其结果会是nil,但不全对,实际上,解释器会去寻找元表中是否有__index元方法,如果没有当然返回nil,如果有的话,相应的元方法会提供结果。
接下来以窗口为例来说明__index的使用:
The __newindex metamethod
当把一个值赋给table中不存在的key时,解释器会寻找__newindex对应的元方法,把赋值操作转换为调用__newindex对应的元方法。
上述这种设置默认值的方法中,如果d中没有对应字段,会继续触发d的元表,效率很低,可以把默认值直接保存在表的字段中,如下:
使用元表来实现跟踪table的访问:
使用元表实现只读table:
Class(类)
Lua中没有类的概念,但可以通过元表来模拟出类的实现。可通过__index
元方法和继承的概念来实现原型,以下为一个Account类:
上述做法可以改进,其实不必要创建mt,直接将self设置为其元表即可,如下:
Inheritance(继承)
因为类是对象,它能从其他类中获得方法,这种行为使得在Lua中实现继承变得非常简单。假设有一个基类Account如下:
创建一个Account的实例SpecialAccount:SpecialAccount = Account:new(), 接下来调用SpecialAccount的实例s:s = SpecialAccount:new{limit=1000.00},。
这三者关系可以理解为:s继承SpecialAccount,SpecialAccount继承Account, 如果调用s的某个方法,如果s中找不到就会去SpecialAccount中搜索,SpecialAccount中找不到就去Account中找。也可重定义Account中的方法:
Multiple Inheritance(多重继承)
一个多重继承的实现如下:
接下来介绍如何使用多重继承,先定义一个类Name如下
创建一个新的类NameAccount,继承Account和Named如下:
Exercise
Lua把自己的全局环境存储在全局变量_G
中,并且_G._G==_G
getfield
和setfield
实现方式如下:
Global_Variable Declarations
使用元表来检查访问的变量是否存在或被申明,如下:
Non-Global Environments and _ENV
Lua中全局变量没有必要真的是全局的,其实Lua压根就没有全局变量。把没有申明为local的变量定义为free变量,Lua把代码块中所有free变量放在_ENV表中,如下:
_ENV并不是一个全局变量,Lua中所有的代码块都可视为匿名函数,而这个_ENV就是这个匿名函数的上值(up-value),当我们load一个Lua代码块(chunk)时,会使用全局环境来初始化_ENV表,如下:
对Lua中全局变量的处理总结如下:
编译时会在代码块匿名函数外创建一个局部变量_ENV
编译器会把所有的
free
变量放入_ENV
中函数
load or loadfile
用全局环境来初始化代码块的第一个上值(up-value)
Lua交互模式下,每一行都有一个不同的_ENV
,可以使用do .. end
来运行一个代码块(沙盒环境),可以通过让_ENV = nil
来使得代码块中之后对全局变量的访问都失效,如下:
_G
和_ENV
虽然指向相同的table,但是他们是不同的实体,_ENV
是一个局部变量,代表的是当前的环境,代码块中所有对“全局”变量的访问都是访问_ENV
。_G
代表的是全局的环境。可以随时改变_ENV
,如下:
Environments and Modules
避免污染全局变量的模块实现:
Exercise
Lua中主要的垃圾回收机制:weak table, finalizers, 和函数collectgarbage。
Weak Tables(弱表)
垃圾回收器无法判断那些我们认为的垃圾,比如stack,通过一个数组和一个top索引值来实现,当pop元素时,只减少top的索引值,则栈顶元素还在数组中,垃圾回收器不会认为这是垃圾,而在我们看来它就是垃圾。因此,需要我们将其置为nil,才能被回收。
Weak tables其实就是告诉Lua这个引用的回收不受保护。因此如果所有对这个对象的引用都是弱引用,垃圾回收器会回收这个对象并删掉所有的弱引用。Lua通过weak table来实现弱引用。
有三种类型的弱表: 弱键(weak key), 弱值(weak value), 弱键和弱值(both keys and values are weak),通过元表的__mode字段来设置弱表的类型,三种情况分别对应k, v, kv。
不管什么类型,当key或者value被回收后,其对应的entry都会从table中删掉。
meorization机制:load是一个很耗的操作,因此可以把其结果记录下来,每次load前会先判断是否在记录中,如果在,则直接使用,不需要再load,如果不在,则调用load并把结果记录。
但是这种机制有一个缺点,一旦记录下load的结果,就会一直占用内存,哪怕之后不再用到,久而久之,必然耗尽内存。因此可以有弱表来优化这种机制,对于一些不再用的,直接交给垃圾回收机制处理,一个color的例子如下:
Finalizers
finalizer是一个跟对象相关的函数,当对象将要被回收时被调用。Lua通过元方法__gc来实现finalizers,如下:
注意__gc添加必须在setmetatable之前,如下:
The Garbage Collector
标记清除分为四步:mark
, cleaning
, sweep
, finalization
.
mark: 从根节点开始,标记Lua能够访问到的节点为alive。
cleaning: 处理finalizers和弱表,遍历所有finalization的对象中没有被标记为alive的对象,放入另一个列表中,用于finalization阶段。然后遍历弱表,将其中key或者value没有被标记为alive的entry从table中移除。
sweep: 遍历所有的Lua对象,回收没有被标记为alive的对象
finalization: 针对cleaning阶段放入列中的对象,调用其__gc元方法。
Lua 5.0在gc时需要暂停主程序,Lua5.1推出了一种增量gc的方法,不需要停止正在运行的程序,一步一步来来gc。
协程跟线程类似,它是一条执行的流程,有自己的栈、局部变量,指令指针,并且和其它线程共享全局变量。协程和线程的主要区别在于多线程程序是可以并行运行的,而协程同时只能有一个在运行,其它的挂起,因此协程之间是协作关系(collaborative)。
Coroutine Basics
Lua把所有协程相关的函数放在coroutines表中,create创建新协程,传入协程执行的方法,返回一个线程来代表新的协程,如下:
协程有四种状态:挂起(suspended),运行(running),正常(normal),死亡(dead),可以使用coroutine.status来查看但协程的状态。协程创建的初始状态为挂起,通过coroutine.resume来启动协程,将其状态改为运行状态。正在运行的协程可以通过coroutine.yield来交出执行权,并切换至挂起状态,等待下一次resume。
通常可以通过resume-yield对来交换数据,如下:
协程一个典型的例子就是生产者-消费者问题,如下:
Event-Driven Programming
Exercise
反射机制:程序具有检查和修改其执行流程的能力。动态语言天然支持一些反射特性:环境允许在运行时inspect全局变量,type
和pairs
函数允许在运行时inspect和遍历不了解的数据结构等。但是仍然有些没法做到:程序不能inspect其局部变量,不能跟踪函数的执行,函数没法知道它的调用者等等,这些在debug库得以实现。
debug库由两种类型函数组成:introspective functionsh
和hooks
,前者允许我们可以检查正在运行程序的一些状态:函数栈,当前执行哪一行,局部变量的值等等。后者允许我们跟踪一个程序的执行流程。
主要的introspective function
为getinfo
,其第一个参数可为一个函数或者stack level, 返回这个函数的一些数据。
通过debug.getlocal
访问局部变量:,第一个参数为stack level, 第二个参数为变量索引,返回改变量的当前名称和值。
以上通过大量实例,介绍了Lua中的迭代器,for循环,元表和元方法,面向对象编程,环境,垃圾回收,协程以及反射。
参考:
http://www.lua.org/pil/
http://www.lua.org/manual/5.3/manual.html#lua_call
以上是关于Lua_isms的主要内容,如果未能解决你的问题,请参考以下文章