《Lua程序设计》--学习4

闭包

在Lua语言中,函数是严格遵循词法定界(lexicalscoping)的第一类值(first-classvalue)。

“第一类值”意味着Lua语言中的函数与其他常见类型的值(例如数值和字符串)具有同等权限:一个程序可以将某个函数保存到变量中(全局变量和局部变量均可)或表中,也可以将某个函数作为参数传递给其他函数,还可以将某个函数作为其他函数的返回值返回。

“词法定界”意味着Lua语言中的函数可以访问包含其自身的外部函数中的变量(也意味着Lua语言完全支持Lambda演算)。

函数是第一类值

 

 赋值语句右边的表达式(function(x)body end)就是函数构造器,与表构造器{}相似。因此,函数定义实际上就是创建类型为"function"的值并把它赋值给一个变量的语句。

在Lua语言中,所有的函数都是匿名的(anonymous)

像其他所有的值一样,函数并没有名字。当讨论函数名时,比如print,实际上指的是保存该函数的变量。虽然我们通常会把函数赋值给全局变量,从而看似给函数起了一个名字,但在很多场景下仍然会保留函数的匿名性

匿名函数在这条语句中显示出了很好的便利性

像函数sort这样以另一个函数为参数的函数,我们称之为高阶函数(higher-order function)。高阶函数是一种强大的编程机制,而利用匿名函数作为参数正是其灵活性的主要来源

非全局函数

函数不仅可以被存储在全局变量中,还可以被存储在表字段局部变量中。

将函数存储在表字段中,大部分Lua语言的库就采用了这种机制,下列的示例创建了这种函数

 在表字段中存储函数是Lua语言中实现面向对象编程的关键要素。

当把一个函数存储到局部变量时,就得到了一个局部函数(local function),即一个被限定在指定作用域中使用的函数。局部函数对于(package)而言尤其有用:由于Lua语言将每个程序段(chunk)作为一个函数处理,所以在一段程序中声明的函数就是局部函数,这些局部函数只在该程序段中可见。词法定界保证了程序段中的其他函数可以使用这些局部函数。

在定义局部递归函数(recursive local function)时,由于原来的方法不适用,所以有一点是极易出错的

 当Lua语言编译函数体中的fact(n-1)调用时,局部的fact尚未定义。因此,这个表达式会尝试调用全局的fact而非局部的fact。我们可以通过先定义局部变量再定义函数的方式来解决这个问题:

所以使用这种定义方式更合适

在间接递归的情况下,必须使用与明确的前向声明(explicit forward declaration)等价的形式:

请注意,不能在最后一个函数定义前加上local。否则,Lua语言会创建一个全新的局部变量f,从而使得先前声明的f(函数g中使用的那个)变为未定义状态。

词法定界

当编写一个被其他函数B包含的函数A时,被包含的函数A可以访问包含其的函数B的所有局部变量,我们将这种特性称为词法定界

---[[
function newCounter(  )
	local count = 0
    return function (  )
	  count = count + 1
      return count
    end
end

c1 = newCounter()
print(c1())
print(c1())
print(c1())
c2 = newCounter()
print(c2())
--]]

 

好怪啊,重复调用的时候那个count还在里面,c1的count是c1的,c2的是c2的,互不干涉

 c1和c2是不同的闭包。它们建立在相同的函数之上,但是各自拥有局部变量count的独立实例。

从技术上讲,Lua语言中只有闭包而没有函数。函数本身只是闭包的一种原型

由于函数可以被保存在普通变量中,因此在Lua语言中可以轻松地重新定义函数,甚至是预定义函数。

当重新定义一个函数的时候,我们需要在新的实现中调用原来的那个函数

 上述代码使用了do代码段来限制局部变量oldSin的作用范围;根据可见性规则,局部变量oldSin只在这部分代码段中有效。因此,只有新版本的函数sin才能访问原来的sin函数,其他部分的代码则访问不了。

我们可以使用同样的技巧来创建安全的运行时环境(secure environment),即所谓的沙盒(sandbox)。

我们可以通过使用闭包重定义函数io.open来限制一个程序能够访问的文件:

 

模式匹配

模式匹配的相关函数

函数string.find

函数string.find用于在指定的目标字符串中搜索指定的模式

函数string.find找到一个模式后,会返回两个值:匹配到模式开始位置的索引和结束位置的索引。如果没有找到任何匹配,则返回nil:

匹配成功后,可以以函数find返回的结果为参数调用函数string.sub来获取目标字符串中匹配相应模式的子串。对于简单的模式来说,这一般就是模式本身。

函数string.find具有两个可选参数。第3个参数是一个索引,用于说明从目标字符串的哪个位置开始搜索。第4个参数是一个布尔值,用于说明是否进行简单搜索(plain search)。字如其名,所谓简单搜索就是忽略模式而在目标字符串中进行单纯的“查找子字符串”的动作:

请注意,如果没有第3个参数,是不能传入第4个可选参数的。

函数string.match

函数string.match返回的是目标字符串中与模式相匹配的那部分子串,而非该模式所在的位置:

 函数string.gsub

函数string.gsub有3个必选参数:目标字符串、模式和替换字符串(replacementstring),其基本用法是将目标字符串中所有出现模式的地方换成替换字符串

 此外,该函数还有一个可选的第4个参数,用于限制替换的次数:

 除了替换字符串以外,string.gsub的第3个参数也可以是一个函数或一个表,这个函数或表会被调用

函数string.gsub还会返回第2个结果,即发生替换的次数

函数string.gmatch

函数string.gmatch返回一个函数,通过返回的函数可以遍历一个字符串中所有出现的指定模式。例如,以下示例可以找出指定字符串s中出现的所有单词

 模式'%a+'会匹配一个或多个字母组成的序列(也就是单词)。因此,for循环会遍历所有目标字符串中的单词,然后把它们保存到列表words中

模式

Lua语言的解决方案更加简单:Lua语言中的模式使用百分号(percent sign)作为转义符

总体上,所有被转义的字母都具有某些特殊含义(例如'%a'匹配所有字母),而所有被转义的非字母则代表其本身(例如'%.'匹配一个点)。

所谓字符分类,就是模式中能够与一个特定集合中的任意字符相匹配的一项。例如,分类%d匹配的是任意数字。因此,可以使用模式'%d%d/%d%d/%d%d%d%d'来匹配dd/mm/yyyy格式的日期

 这些类的大写形式表示类的补集。例如,'%A'代表任意非字母的字符

在输出函数gsub的返回结果时,我们使用了额外的括号来丢弃第二个结果,也就是替换发生的次数

当在模式中使用时,还有一些被称为魔法字符(magic character)的字符具有特殊含义。Lua语言的模式所使用的魔法字符包括:

 百分号同样可以用于这些魔法字符的转义。因此,'%?'匹配一个问号,'%%'匹配一个百分号

可以使用字符集(char-set)来创建自定义的字符分类,只需要在方括号内将单个字符和字符分类组合起来即可。例如,字符集'[%w_]'匹配所有以下画线结尾的字母和数字,'[01]'匹配二进制数字,'[%[%]]'匹配方括号

还可以在字符集中包含一段字符范围,做法是写出字符范围的第一个字符和最后一个字符并用横线将它们连接在一起。例如,'%d'相当于'[0-9]','%x'相当于'[0-9a-fA-F]'。不过,如果需要查找一个八进制的数字,那么使用'[0-7]'就比显式地枚举'[01234567]'强多了。

在字符集前加一个补字符^就可以得到这个字符集对应的补集:模式'[^0-7]'代表所有八进制数字以外的字符,模式'[^\n]'则代表除换行符以外的其他字符。当然,以大写形式也可以获得补集

还可以通过描述模式中重复和可选部分的修饰符(modifier,在其他语言中也被译为限定符)来让模式更加有用。Lua语言中的模式提供了4种修饰符:

 修饰符+匹配原始字符分类中的一个或多个字符,它总是获取与模式相匹配的最长序列。例如,模式'%a+'代表一个或多个字母(即一个单词

捕获

捕获(capture)机制允许根据一个模式从目标字符串中抽出与该模式匹配的内容来用于后续用途,可以通过把模式中需要捕获的部分放到一对圆括号内来指定捕获。

对于具有捕获的模式,函数string.match会将所有捕获到的值作为单独的结果返回;换句话说,该函数会将字符串切分成多个被捕获的部分

模式'%a+'表示一个非空的字母序列,模式'%s*'表示一个可能为空的空白序列

模式中的两个字母序列被分别放在圆括号中,因此在匹配时就能捕获到它们

 它所匹配的内容依次是:一个左方括号、零个或多个等号、另一个左方括号、任意内容(即字符串的内容)、一个右方括号、相同数量的等号及另一个右方括号

 第1个捕获是等号序列(在本例中只有一个),第2个捕获是字符串内容

替换

函数string.gsub的第3个参数不仅可以是字符串,还可以是一个函数或表。当第3个参数是一个函数时,函数string.gsub会在每次找到匹配时调用该函数,参数是捕获到的内容而返回值则被作为替换字符串。当第3个参数是一个表时,函数string.gsub会把第一个捕获到的内容作为键,然后将表中对应该键的值作为替换字符串。如果函数的返回值为nil或表中不包含这个键或表中键的对应值为nil,那么函数gsub不改变这个匹配

先举一个例子,下述函数用于变量展开(variable expansion),它会把字符串中所有出现的$varname替换为全局变量varname的值:

 _G是预先定义的包括所有全局变量的表

URL编码

我们的下一个示例中将用到URL编码,也就是HTTP所使用的在URL中传递参数的编码方式。这种编码方式会将特殊字符(例如=、&和+)编码为"%xx"的形式,其中xx是对应字符的十六进制值。此外,URL编码还会将空格转换为加号。例如,字符串"a+b=c"的URL编码为"a%2Bb+%3D+c"。最后,URL编码会将每对参数名及其值用等号连接起来,然后将每对name=value用&连接起来。例如,值

 对应的URL编码为"name=al&query=a%2Bb+%3D+c&q=yes+or+no"。

现在,假设要将这个URL解码并将其中的键值对保存到一个表内,以相应的键作为索引,那么可以使用以下的函数完成基本的解码:

 第一个gsub函数将字符串中的所有加号替换为空格,第二个gsub函数则匹配所有以百分号开头的两位十六进制数,并对每处匹配调用一个匿名函数。这个匿名函数会将十六进制数转换成一个数字(以16为进制,使用函数tonumber)并返回其对应的字符(使用函数string.char)。

可以使用函数gmatch来对键值对name=value进行解码。由于键名和值都不能包含&或=,所以可以使用模式'[^&=]+'来匹配它们:

 调用函数gmatch会匹配所有格式为name=value的键值对。对于每组键值对,迭代器会返回对应的捕获(在匹配的字符串中被括号括起来了),捕获到的内容也就是name和value的值。循环体内只是简单地对两个字符串调用函数unescape,然后将结果保存到表cgi中

对应的编码函数也很容易编写。先写一个escape函数,用它将所有的特殊字符编码为百分号紧跟对应的十六进制形式(函数format的参数"%02X"用于格式化输出一个两位的十六进制数,若不足两位则以0补齐),然后把空格替换成加号:

 encode函数会遍历整个待编码的表,然后构造出最终的字符串

 制表符展开

 函数gsub会匹配字符串中所有的制表符并捕获它们的位置。对于每个制表符,匿名函数会根据其所在位置计算出需要多少个空格才能恰好凑够一列(整数个tab):该函数先将位置减去1以从0开始计数,然后加上corr凑整之前的制表符(每一个被展开的制表符都会影响后续制表符的位置)。之后,该函数更新下一个制表符的修正量:为正在被去掉的制表符减1,再加上要增加的空格数sp。最后,这个函数返回由替代制表符的合适数量的空格组成的字符串。

为了完整起见,让我们再看一下如何实现逆向操作,即将空格转换为制表符。第一种方法是通过空捕获来对位置进行操作,但还有一种更简单的方法:即在字符串中每隔8个字符插入一个标记,然后将前面有空格的标记替换为制表符。

 这个函数首先对字符串进行了制表符展开以移除其中所有的制表符,然后构造出一个用于匹配所有8个字符序列的辅助模式,再利用这个模式在每8个字符后添加一个标记(控制字符\1)。接着,它将所有以此标记结尾的空格序列都替换为制表符。最后,将剩下的标记删除(即那些没有位于空格后的标记)。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值