迭代器和泛型for
迭代器和闭包
迭代器(iterator)是一种可以让我们遍历一个集合中所有元素的代码结构。在Lua语言中,通常使用函数表示迭代器:每一次调用函数时,函数会返回集合中的"下一个"元素。
一个闭包就是一个可以访问其自身的环境中一个或多个局部变量的函数。这些变量将连续调用过程中的值并将其保存在闭包中,从而使得闭包能够记住迭代所处的位置
一个简单的迭代器:
在这个例子中,values就是工厂。每当调用这个工厂时,它就会创建一个新的闭包(即迭代器本身)。这个闭包将它的状态保存在其外部的变量t和i中,这两个变量也是由values创建的。每次调用这个迭代器时,它就从列表t中返回下一个值。在遍历完最后一个元素后,迭代器返回nil,表示迭代结束。
泛型for的语法
泛型for在循环过程中在其内部保存了迭代函数。实际上,泛型for保存了三个值:一个迭代函数、一个不可变状态(invariant state)和一个控制变量(control variable)
无状态迭代器
无状态迭代器(stateless iterator)就是一种自身不保存任何状态的迭代器。因此,可以在多个循环中使用同一个无状态迭代器,从而避免创建新闭包的开销。
迭代的状态由正在被遍历的表(一个不可变状态,它不会在循环中改变)及当前的索引值(控制变量)组成。
还有一种创建迭代器的方式可以**让迭代器进行实际的迭代操作。**当使用这种迭代器时,就不再需要编写循环了。相反,只需要调用这个迭代器,并传入一个描述了在每次迭代时迭代器需要做什么的参数即可。更确切地说,迭代器接收一个函数作为参数,这个函数在循环的内部被调用,这种迭代器就被称为真正的迭代器(true iterator)。举一个更具体的例子,让我们使用这种风格再次重写allwords迭代器:
马尔可夫链算法
Lua
print("please Input N (size for the sequence of previous words):")
local N = tonumber(io.read())
while math.type(N) ~= "integer" do
print("Invalid type, ReInput N:")
N = tonumber(io.read())
end
function allwords ()
local line = io.read() -- current line
local pos = 1 -- current position in the line
return function()
-- iterator function
while line and line ~= "" do
-- repeat while there are lines
local w, e = string.match(line, "(%w+[,;.:]?)()", pos)
if w then
-- found a word?
pos = e -- update next position
return w -- return the word
else
line = io.read() -- word not found; try next line
pos = 1 -- restart from first position
end
end
return nil -- no more lines: end of traversal
end
end
function prefix (words)
return table.concat(words," ")
end
local statetab = {}
function insert (prefix, value)
local list = statetab[prefix]
if list == nil then
statetab[prefix] = { value }
else
list[#list + 1] = value
end
end
local MAXGEN = 200
local NOWORD = "\n"
-- build table
local wordTable = {}
for i = 1,N do
wordTable[N] = NOWORD
end
print("\nPlease Input the words:")
for nextword in allwords() do
insert(prefix(wordTable), nextword)
for i = 1,#wordTable do
wordTable[i] = wordTable[i + 1]
end
wordTable[N] = nextword
end
insert(prefix(wordTable), NOWORD)
-- generate text
for i = 1,N do
wordTable[N] = NOWORD
end
for i = 1, MAXGEN do
local list = statetab[prefix(wordTable)]
-- choose a random item from list
local r = math.random(#list)
local nextword = list[r]
if nextword == NOWORD then
return
end
io.write(nextword, " ")
for i = 1,#wordTable do
wordTable[i] = wordTable[i + 1]
end
wordTable[N] = nextword
end
pil4/chapter19/chapter19.lua at master · 0kk470/pil4 (github.com)
元表和元方法
元表可以修改一个值在面对一个未知操作时的行为
例如,假设a和b都是表,那么可以通过元表定义Lua语言如何计算表达式a+b。当Lua语言试图将两个表相加时,它会先检查两者之一是否有**元表(metatable)**且该元表中是否有__add字段。如果Lua语言找到了该字段,就调用该字段对应的值,即所谓的元方法(metamethod)(是一个函数)
可以认为,元表是面向对象领域中的受限制类。像类一样,元表定义的是实例的行为 。不过,由于元表只能给出预先定义的操作集合的行为,所以元表比类更受限;同时,元表也不支持继承
Lua语言中的每一个值都可以有元表。每一个表和用户数据类型都具有各自独立的元表,而其他类型的值则共享对应类型所属的同一个元表。Lua语言在创建新表时不带元表:
可以使用函数setmetatable来设置或修改任意表的元表:
在Lua语言中,我们只能为表设置元表;如果要为其他类型的值设置元表,则必须通过C代码或调试库完成
字符串标准库为所有的字符串都设罝了同一个元表,而其他类型在默认情况中都没有元表
一个表可以成为任意值的元表;一组相关的表也可以共享一个描述了它们共同行为的通用元表;一个表还可以成为它自己的元表,用于描述其自身特有的行为。总之,任何配置都是合法的。
算术运算相关的元方法
现在,假设想使用加法操作符来计算两个集合的并集,那么可以让所有表示集合的表共享一个元表。这个元表中定义了这些表应该如何执行加法操作。首先,我们创建一个普通的表,这个表被用作集合的元表:
然后,修改用于创建集合的函数Set.new。在新版本中只多了一行,即将mt设置为函数Set.new所创建的表的元表:
在此之后,所有由Set.new创建的集合都具有了一个相同的元表:
最后,向元表中加入元方法(metamethod)__add,也就是用于描述如何完成加法的字段:
此后,只要Lua语言试图将两个集合相加,它就会调用函数Set.union,并将两个操作数作为参数传入。
关系运算相关的元方法
元表还允许我们指定关系运算符的含义,其中的元方法包括等于(__eq)、小于(__lt)和小于等于(__le)。其他三个关系运算符没有单独的元方法,Lua语言会将a~=b转换为not(a==b),a>b转换为b<a,a>=b转换为b<=a。
部分有序是指,并非所有类型的元素都能够被正确地排序。例如,由于Not a Number(NaN)的存在,大多数计算机中的浮点数就不是完全可以排序的。
标准规定任何涉及NaN的比较都应返回假,这就意味着NaN<=x永远为假,x<NaN也为假。因此,在这种情况下,a<=b到not(b<a)的转化也就不合法了。
在集合的示例中,我们也面临类似的问题。<=显而易见且有用的含义是集合包含:a<=b通常意味着a是b的一个子集。然而,根据部分有序的定义,a<=b和b<a可能同时为假。因此,我们就必须实现__le(小于等于,子集关系)和__lt(小于,真子集关系):
表相关的元方法
算术运算符、位运算符和关系运算符的元方法都定义了各种错误情况的行为,但它们都没有改变语言的正常行为。Lua语言还提供了一种改变表在两种正常情况下的行为的方式,即访问和修改表中不存在的字段。
__index元方法
对表的访问会引发解释器查找一个名为__index的元方法。如果没有这个元方法,那么像一般情况下元素不存在的情况一样,结果就是nil;否则,则由这个元方法来提供最终结果。
Lua
---[[
--创建具有默认值的原型
prototype = {x = 0,y = 0,width = 100,height = 100}
local mt = {} --创建一个元表
--声明构造函数
function new(o)
setmetatable(o,mt)
return o
end
mt.__index = function (_,key )
return prototype[key]
end
w = new{x = 10,y = 20}
print(w.width)
--]]
输出100
Lua语言会发现w中没有对应的字段"width",但却有一个带有__index元方法的元表。因此,Lua语言会以w(表)和"width"(不存在的键)为参数来调用这个元方法。元方法随后会用这个键来检索原型并返回结果。
在Lua语言中,使用元方法__index来实现继承 是很普遍的方法。虽然被叫作方法,但元方法__index不一定必须是一个函数,它还可以是一个表 。当元方法是一个函数时,Lua语言会以表和不存在的键为参数调用该函数 ,正如我们刚刚所看到的。当元方法是一个表时,Lua语言就访问这个表。因此,在我们此前的示例中,可以把__index简单地声明为如下样式:
mt.__index = prototype
如果我们希望在访问一个表时不调用__index元方法,那么可以使用函数rawget 。调用rawget(t,i)会对表t进行原始(raw)的访问,即在不考虑元表的情况下对表进行简单的访问
__newindex元方法
元方法__newindex与__index类似,不同之处在于前者用于表的更新而后者用于表的查询。当对一个表中不存在的索引赋值时,解释器就会查找__newindex元方法:如果这个元方法存在,那么解释器就调用它而不执行赋值。像元方法__index一样,如果这个元方法是一个表,解释器就在此表中执行赋值,而不是在原始的表中进行赋值。此外,还有一个原始函数允许我们绕过元方法:调用rawset(t,k,v)来等价于t[k]=v,但不涉及任何元方法
具有默认值的表
一个普通表中所有字段的默认值都是nil。通过元表,可以很容易地修改这个默认值:
跟踪对表的访问
假设我们要跟踪对某个表的所有访问。由于__index和__newindex元方法都是在表中的索引不存在时才有用,因此,捕获对一个表所有访问的唯一方式是保持表是空的。如果要监控对一个表的所有访问,那么需要为真正的表创建一个代理(proxy)。这个代理是一个空的表,具有用于跟踪所有访问并将访问重定向到原来的表的合理元方法
Lua
---[[
function track(t)
local proxy = {} --'t'的代理类
--为代理创建元表
local mt = {
__index == function(_,k)
print("*access to element" .. tostring(k))
return t[k] --访问原来的表
end,
__newindex = function ( _,k,v )
print("*update of element" .. tostring(k) .. " to " .. tostring(v))
t[k] = v --更新原来的表
end,
__pairs = function( )
return function(_,k) --迭代函数
local nextkey,nextvalue = next(t,k)
if nextkey ~= nil then --避免最后一个值
print("*traversing element" .. tostring(nextkey))
end
return nextkey,nextvalue
end
end,
__len = function() return #t end
}
setmetable(proxy,mt)
return proxy
end
t = {}
t = track(t)
t[2] = "hello"
print(t[2])
--]]
只读的表
面向对象(Object-Oriented)编程
从很多意义上讲,Lua语言中的一张表就是一个对象。首先,表与对象一样,可以拥有状态。其次,表与对象一样,拥有一个与其值无关的的标识(self -- 类似于 this 指针);特别地,两个具有相同值的对象(表)是两个不同的对象,而一个对象可以具有多个不同的值;最后,表与对象一样,具有与创建者和被创建位置无关的生命周期。
对象有其自己的操作。表也可以有自己的操作,例如:
上面的代码创建了一个新函数,并将该函数存入Account对象的withdraw字段。
不过,在函数中使用全局名称Account是一个非常糟糕的编程习惯。首先,这个函数只能针对特定对象工作。其次,即使针对特定的对象,这个函数也只有在对象保存在特定的全局变量中时才能工作。如果我们改变了对象的名称,withdraw就不能工作了:
这种行为违反对象拥有独立生命周期的原则。
另一种更加有原则的方法是对操作的接受者(receiver)进行操作。因此,我们的方法需要一个额外的参数来表示该接受者,这个参数通常被称为self或this
此时,当我们调用该方法时,必须指定要操作的对象:
通过使用参数self,可以对多个对象调用相同的方法:
Lua语言可以使用冒号操作符(colon operator)隐藏self参数。使用冒号操作符,我们可以将上例重写为a2:withdraw(260.00):
我们可以使用点分语法来定义一个函数,然后用冒号语法调用它,反之亦然,只要能够正确地处理好额外的参数即可:
类(Class)
我们可以参考基于原型的语言(prototype-based language) 中的一些做法来在Lua语言中模拟类,例如Self语言(JavaScript采用的也是这种方式)。在这些语言中,对象不属于类。相反,每个对象可以有一个原型(prototype)。原型也是一种普通的对象,当对象(类的实例)遇到一个未知操作时会首先在原型中查找。要在这种语言中表示一个类,我们只需要创建一个专门被用作其他对象(类的实例)的原型对象即可。类和原型都是一种组织多个对象间共享行为的方式。
如果有两个对象A和B,要让B成为A的一个原型,只需要:
即,Lua语言调用了原来的deposit函数,传入了a作为self参数。因此,新账户a从Account继承了函数deposit。同样,它还从Account继承了所有的字段。
继承(Inheritance)
多重继承(Multiple Inheritance)
这种实现的关键在于把一个函数用作__index元方法。请注意,当一个表的元表中的__index字段为一个函数时,当Lua不能在原来的表中找到一个键时就会调用这个函数。基于这一点,就可以让__index元方法在其他期望的任意数量的父类中查找缺失的键。
多重继承意味着一个类可以具有多个超类。因此,我们不应该使用一个(超)类中的方法来创建子类,而是应该定义一个独立的函数createClass来创建子类。函数createClass的参数为新类的所有超类
Lua
--在表'plist'的列表中查找'k'
local function search( k,plist )
for i = 1,#plist do
local v = plist[i][k] --尝试第'i'个超类
if v then return v end
end
end
function ccreateClass( ... )
local c = {} --新类
local parents = {...} --父类列表
--在父类列表中查找类缺失的方法
setmetatable(c,{__index = function(t,k)
return search(k,parents)
end })
--将'c'作为其实例的元素
c.__index = c
--为新类定义一个新的构造函数
function c:new(o)
o = o or {}
setmetatable(o,c)
return o
end
return c -- 返回新类
end
私有性(Privacy)
**一个表用来保存对象的状态,另一个表用于保存对象的操作(或接口)。**我们通过第二个表来访问对象本身,即通过组成其接口的操作来访问。为了避免未授权的访问,表示对象状态的表不保存在其他表的字段中,而只保存在方法的闭包中。例如,如果要用这种设计来表示银行账户,那么可以通过下面的工厂函数创建新的对象:
首先,这个函数创建了一个用于保存对象内部状态的表,并将其存储在局部变量self中。然后,这个函数创建了对象的方法。最后,这个函数会创建并返回一个外部对象,该对象将方法名与真正的方法实现映射起来。这里的关键在于,这些方法不需要额外的self参数,而是直接访问self变量。由于没有了额外的参数,我们也就无须使用冒号语法来操作这些对象,而是可以像普通函数那样来调用这些方法:
这种设计给予了存储在表self中所有内容完全的私有性。当newAccount返回后,就无法直接访问这个表了,我们只能通过在newAccount中创建的函数来访问它
单方法对象(Single-method Object)
上述面向对象编程实现的一个特例是对象只有一个方法的情况。在这种情况下,可以不用创建接口表,只要将这个单独的方法以对象的表示形式返回即可。诸如io.lines或string.gmatch这样的内部保存了状态的迭代器就是一个单方法对象。
虽然使用这种方式不能实现继承,但我们却可以拥有完全的私有性:访问单方法对象中某个成员只能通过该对象所具有的唯一方法进行。
对偶表示(Dual Representation)
实现私有性的另一种有趣方式是使用对偶表示(dual representation)