闭包

Functions in Lua are first-class values with proper lexical scoping

什么意味着“first-class values”(一等公民)?这意味着,在Lua中,函数是具有与数字和字符串等常规值相同权限的值。程序可以将函数存储在变量(全局和局部)和表中,将函数作为参数传递给其他函数,并将函数作为结果返回。

什么意味着“lexical scoping”(词法域)。 这意味着函数可以访问其封闭函数的变量。 (这也意味着Lua正确地包含了lambda。

这两个特性一起为语言提供了极大的灵活性;例如,一个程序可以重新定义一个函数来添加新的功能,或者在运行一段不受信任的代码(例如通过网络接收的代码)时删除一个函数来创建一个安全的环境。更重要的是,这些特性允许我们在Lua中应用来自函数语言世界的许多强大的编程技术。即使您对函数式编程一点都不感兴趣,也值得学习一下如何探索这些技术,因为它们可以使您的程序更小、更简单。

函数作为first-class value

举个例子,函数为什么作为first-class value:

a = (p = print) -- a.p 引用 print 函数
a.p("Hello world") --> Hello world
print = math.sin  -- print 现在引用为math.sin函数
a.p(print(1)) --> 0.84147
math.sin = a.p -- sin现在引用为print函数
math.sin(10, 20) --> 10   20

类似于js,函数作为值的语法糖可以写为:

foo = function(x) return 2*x end

函数是无名的。例如我们在说print函数,实际上是在说一个变量print持有打印函数。

table库提供函数table.sort,接收一个table然后排序。该库提供一个可选的参数用于传入一个排序函数。例如:

network = {
  {name = "grauna", IP = "210.26.30.34"},
  {name = "arraial", IP = "210.26.30.23"},
  {name = "lua", IP = "210.26.23.12"},
  {name = "derain", IP = "210.26.23.20"},
}
table.sort(network, function (a,b) return ( > ) end)

一个函数接受另一个函数作为参数,我们称之为higher-order函数。下面我们实现一个这样的函数:

function derivative(f, delta)
  delta = delta or 1e-4
  return function(x)
           return (f(x + delta) - f(x)) / delta
         end
end

我们可以这样调用它:

c = derivative(math.sin)
 > print(math.cos(5.2), c(5.2))
 --> 0.46851667130038 0.46856084325086
 print(math.cos(10), c(10))
 --> -0.83907152907645 -0.83904432662041

非全局函数

函数不一定只有在全局上可以用,在table的字段和局部变量中都可以使用:

Lib = {}
Lib.foo = function (x, y) return x + y end
Lib.goo = function (x, y) return x - y end
print(Lib.foo(2, 3), Lib.goo(2, 3))  ---> 5   -1

同样的我们还可以使用构造器:

Lib = {
  foo = function (x,y) return x + y end,
  goo = function (x,y) return x - y end
}

更多的,lua提供特定的格式定义这样的函数:

Lib = {}
  function Lib.foo (x,y) return x + y end
  function Lib.goo (x,y) return x - y end

lua支持局部函数的定义语法糖如下:

local funtion f(params)
  body
end

在递归局部函数的定义中出现了一个微妙的问题,因为这种简单的方法在这里不起作用。考虑下一个定义:

local fact = function (n)
  if n == 0 then return 1
  else return n*fact(n-1) -- buggy
  end
end

当Lua在函数体中编译调用f(n - 1)时,还没有定义局部事实。因此,这个表达式将尝试调用一个全局事实,而不是局部事实。我们可以通过先定义局部变量,再定义函数来解决这个问题:

local fact
  fact = function (n)
  if n == 0 then return 1
  else return n*fact(n-1)
  end
end

当然,如果我们有间接递归函数,这个技巧就不起作用。 在这种情况下,我们必须使用与明确的提前定义的方式:

local f -- "forward" declaration
 
 local function g ()
   some code f() some code
 end
 
 function f ()
   some code g() some code
 end

词法域

当我们写一个函数封装另一个函数,它具有完全的权限访问封装函数的局部变量。我们称之为词法域。尽管这个词听起来很明显,但实际上并不是。

词法作用域加上嵌套的一级函数为编程语言提供了强大的功能,但是许多函数不支持这种组合。

让我们从一个简单的例子开始。 假设我们有一个学生姓名列表和一个将姓名映射到年级的表格;我们希望根据他们的成绩对姓名列表进行排序,并以较高的成绩进行排序 t.我们可以这样做:

names = {"Peter", "Paul", "Mary"}
grades = {Mary = 10, Paul = 7, Peter = 8}
table.sort(names, function (n1, n2)
    return grades[n1] > grades[n2] -- compare the grades
  end)

现在假设我们想创建一个函数完成这个任务:

function sortbygrade (names, grades)
  table.sort(names, function (n1, n2)
    return grades[n1] > grades[n2] -- compare the grades
  end)
end

在这个最后一个例子中有趣的一点是,grades既不是全局变量也不是局部变量,而是我们称为的非局部变量(历史原因,也称为upvalues)

为什么这一点如此有趣?因为函数是一类值,可以逃脱其变量的原始作用域。考虑以下代码:

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

c1 = newCounter()
print(c1()) --> 1
print(c1()) --> 2

在这段代码中,匿名函数引用一个非局部变量(count)来保存它的计数器。但是,在调用匿名函数时,变量count似乎超出了范围,因为创建这个变量的函数(newCounter)已经返回。然而,Lua使用闭包的概念正确地处理了这种情况。简单地说,闭包是一个函数加上正确访问非本地变量所需要的所有东西。如果我们再次调用newCounter,它将创建一个新的局部变量count和一个新的闭包,作用于这个新变量:

c2 = newCounter()
 print(c2()) --> 1
 print(c1()) --> 3
 print(c2()) --> 2

因此,c1和c2是不同的闭包。 两者都建立在相同的函数上,但每个函数都作用于局部变量计数的独立实例化。

从技术上讲,Lua有值的是闭包,而不是函数。 函数本身就是一种用于闭包的原型。

在许多情况下,闭包提供了一种有价值的工具。 如我们所见,它们可用作排序等高阶函数的参数。 闭包对于构建其他函数的函数也很有价值,例如我们的newCounter示例或派生示例。 这种机制允许Lua程序结合函数式变成中的复杂编程技术。 闭包对于回调函数也很有用。 当我们在传统的GUI工具包中创建按钮时,就会出现一个典型的例子。 每个按钮都有一个回调函数,当用户按下该按钮时会被调用。 我们希望不同的按钮在按下时执行略有不同的事情。

例如,数字计算器需要十个类似的按钮,每个数字一个。 我们可以用这样的函数创建每个函数:

function digitButton(digit)
  return Button{
        label = tostring(digit),
        action = function()
            	   add_to_display(digit)
                 end
    }
end

在这个例子中,我们假装Button是一个创建新按钮的工具包函数;label是按钮标签;action是按钮按下时要调用的回调函数。 在数字按钮完成任务后很长一段时间,可以回调,但它仍然可以访问数字变量。

闭包在完全不同的上下文中也很有价值。因为函数存储在常规变量中,所以我们可以很容易地在Lua中重新定义函数,甚至是预定义的函数。这是Lua如此灵活的原因之一。通常,当我们重新定义一个函数时,我们需要在新的实现中使用原来的函数。举个例子,假设我们想要重新定义函数sin以角度而不是弧度来计算。这个新函数转换它的参数,然后调用原始函数sin来做实际的工作。我们的代码可能是这样的:

local oldSin = math.sin
  math.sin = function (x)
  return oldSin(x * (math.pi / 180))
end

一个稍微简洁的方法来做这个重新定义如下:

do
  local oldSin = math.sin
  local k = math.pi / 180
  math.sin = function (x)
    return oldSin(x * k)
  end
end

这段代码使用一个do块来限制局部变量oldSin的作用域;按照常规的可见性规则,该变量只在块内可见。所以,访问它的唯一方法是通过新函数。

我们可以使用同样的技术来创建安全的环境,也称为沙箱。 当运行不受信任的代码时,安全环境是必不可少的。 例如,为了限制程序可以访问的文件,我们可以使用闭包重新定义io.open:

do
  local oldOpen = io.open
  local access_OK = function (filename, mode)
    check access
  end
  io.open = function (filename, mode)
    if access_OK(filename, mode) then
      return oldOpen(filename, mode)
    else
      return nil, "access denied"
    end
  end
end

Lua本身的Lua沙箱,具有通常的好处:简单性和灵活性。Lua为我们提供了一种元机制,而不是一个放之四海而皆准的解决方案,因此我们可以根据特定的安全需求调整我们的环境。

函数式编程的尝试

为了给出一个更具体的函数编程示例,在本节中,我们将开发一个简单的几何区域系统。 目标是开发一个系统来表示几何区域, 区域是一组点。 我们希望能够表示各种形状,并以几种方式(旋转、平移、联合等)组合和修改形状。 )。

为了实现这个系统,我们可以开始寻找好的数据结构来表示形状;我们可以尝试一种面向对象的方法,并开发一些形状的层次结构。或者我们可以在更高的抽象级别上工作,并通过它们的特征(或指标)函数直接表示我们的集合。(集合a的特征函数是一个函数fA,使得当且仅当x属于a时,fA(x)为真)给定几何区域是一组点,我们可以用它的特征函数来表示一个区域;也就是说, 我们可以用它的特征函数来表示一个区域;也就是说,我们用一个函数来表示一个区域,给定一个点,当且仅当该点属于该区域时,该函数才返回true。

例如,下一个函数表示中心(1.0,3.0)和半径4.5的磁盘(圆形区域):

function disk1(x, y)
  return (x - 1.0)^2 + (y - 3.0)^2 <= 4.5^2
end

我们可以很容易的想到用个工厂来定义这个区域:

function dist(cx, cy, r)
  return function(x,y)
           return (x - cx)^2 + (y - cy)^2 <= r^2
         end
end

disk1 = dist(1.0, 3.0, 4.5)

下一个函数创建轴对齐矩形,给定界:

function rect (left, right, bottom, up)
  return function (x, y)
    return left <= x and x <= right and
    bottom <= y and y <= up
  end
end

略…

模式匹配

与其他几种脚本语言不同,Lua既不使用POSIX regex,也不使用Perl正则表达式进行模式匹配。这个决定的主要原因是大小:POSIX正则表达式的典型实现需要超过4000行代码,这是所有Lua标准库的一半多。相比之下,Lua中模式匹配的实现只有不到600行。

当然,Lua中的模式匹配不能完成完整POSIX实现所能完成的所有工作。然而,Lua中的模式匹配是一个强大的工具,它包含了一些很难与标准POSIX实现匹配的特性。

模式匹配函数

字符串库提供了四个基于模式的函数。我们已经看到了find和gsub;另外两个是match和gmatch(全局匹配)。现在我们将详细讨论它们。

string.find

字符串的函数。查找在给定主题字符串中搜索模式。模式最简单的形式是单词,它只匹配自身的一个副本。例如,模式“hello”将在主题字符串中搜索子字符串“hello”。当string.find找到它的模式,它返回两个值:匹配开始的索引和匹配结束的索引。如果没有找到匹配项,则返回nil:

s = "hello world"
 i, j = string.find(s, "hello")
 print(i, j) --> 1 5
 print(string.sub(s, i, j)) --> hello
 print(string.find(s, "world")) --> 7 11
 i, j = string.find(s, "l")
 print(i, j) --> 3 3
 print(string.find(s, "lll")) --> nil

当匹配成功时,我们可以调用string.sub与find返回的值一起,以获得与模式匹配的字符串部分。对于简单模式,这就是模式本身。

函数string.find有两个可选参数。第三个参数是一个索引,它告诉字符串在什么地方开始搜索。第四个参数布尔值表示纯搜索。顾名思义,普通搜索在主题中执行普通的“查找子字符串”搜索,忽略模式:

> string.find("a [word]", "[")
 stdin:1: malformed pattern (missing ']')
 > string.find("a [word]", "[", 1, true) --> 3 3

在第一个调用中,函数会报错,因为[在模式中有特殊的含义。在第二个调用中,函数将’['视为一个简单的字符串。注意,如果没有第三个参数,我们就不能传递第四个可选参数。

string.match

string.matchfind类似,它也搜索字符串中的模式。但是,它不是返回找到模式的位置,而是返回匹配模式的主题字符串部分:

print(string.match("hello world", "hello")) --> hello

对于“hello”这样的固定模式,此函数毫无意义。它显示了与变量模式一起使用时的强大功能,如下面的示例所示:

date = "Today is 17/7/1990"
 d = string.match(date, "%d+/%d+/%d+")
 print(d) --> 17/7/1990

string.gsub

函数string.gsub有三个强制参数:主题字符串、模式和替换字符串。它的基本用途是将替换字符串替换主题字符串中出现的所有模式:

s = string.gsub("Lua is cute", "cute", "great")
 print(s) --> Lua is great
 s = string.gsub("all lii", "l", "x")
 print(s) --> axx xii
 s = string.gsub("Lua is great", "Sol", "Sun")
 print(s) --> Lua is great

可选的第四个参数限制了替换的数量:

s = string.gsub("all lii", "l", "x", 1)
 print(s) --> axl lii
 s = string.gsub("all lii", "l", "x", 2)
 print(s) --> axx lii

除了字符串,string.gsub的第三个参数也可以是一个函数或一个表,调用它(或索引它)来生成替换字符串;我们将在“替换”一节中讨论这个特性。

函数string.gsub还返回第二个结果,即它进行替换的次数。.

string.match

函数string.gmatch返回一个函数,该函数遍历字符串中出现的所有模式。

例如,下面的例子收集了给定字符串s的所有单词:

s = "some string"
 words = {}
 for w in string.gmatch(s, "%a+") do
 words[#words + 1] = w
 end

我们稍后将讨论,'%a+'模式匹配一个或多个字母字符(即单词)的序列。因此,for循环将遍历subject字符串的所有单词,并将它们存储在列表单词中。

Pattern模式

大多数模式匹配库都使用反斜杠作为转义。然而,这种选择有一些恼人的后果。对于Lua解析器,模式是常规字符串。它们不会被特别对待并且和其他字符串规则一致。只有模式匹配函数将它们解释为模式。因为反斜杠是Lua中的转义字符,所以我们必须转义它,以便将它传递给任何函数。模式自然很难阅读,在任何地方都用“\\”而不是“\”来写也没用。

我们可以用长字符串、双括号之间的封闭模式来改善这个问题。(一些语言推荐这种做法。)然而,长字符串表示法对于通常很短的模式来说似乎很麻烦。此外,我们将失去在模式中使用转义的能力。(一些模式匹配工具通过重新实现通常的字符串转义来绕过这个限制。)

Lua的解决方案更简单:Lua中的模式使用百分号作为转义。(C语言中的一些函数,如printf和strftime,采用了相同的解决方案。)通常,任何转义的字母数字字符都有一些特殊的含义(例如,’%a’匹配任何字母),而任何转义的非字母数字字符都表示自己(例如,’% ')。'匹配一个点)。

我们将从字符类开始讨论模式。字符类是模式中的一个项,它可以匹配特定集合中的任何字符。例如,%d类匹配任何数字。因此,我们可以搜索格式为dd/mm/yyyy的日期,其模式为“%d%d/%d%d/%d%d%d%d%d”:

s = "Deadline is 30/05/1999, firm"
 date = "%d%d/%d%d/%d%d%d%d"
 print(string.match(s, date)) --> 30/05/1999

下表列出了预定义的字符类及其含义:

模式

匹配

.

所有字符

%a

字母

%c

控制字符

%d

数字

%g

除空格外的可打印字符

%l

小写字母

%p

标点符号

%s

空白符

%u

大写字母

%w

数字+字母

%x

十六进制数

这些类中的任何一个的大写版本都表示该类的补码。例如,’%A’表示所有非字母字符

print((string.gsub("hello, up-down!", "%A", ".")))
 --> hello..up.down.

(在打印gsub的结果时,我使用了额外的括号来放弃它的第二个结果,即替换的数量。)

有些字符,称为魔法字符,在模式中使用时具有特殊的含义。Lua中的模式使用以下神奇的字符:

( ) . % + - * ? [ ] ^ $

正如我们所看到的,百分号是这些魔法字符的一个转义。所以,‘% ?‘匹配问号,’%%'匹配百分号本身。我们不仅可以转义魔法字符,还可以转义任何非字母数字字符。当有疑问时,要谨慎行事,使用转义方法。

char-set允许我们创建自己的字符类,将单个字符和类分组在方括号内。例如,字符集'[%w_]'匹配数字字符和下划线,'[01]'匹配二进制数字,'[%[%]]'匹配方括号。要计算一个文本中元音的数量,我们可以这样写代码:

_, nvow = string.gsub(text, "[AEIOUaeiou]", "")

我们还可以将字符范围包括在一个字符集中,方法是将范围的第一个字符和最后一个字符用连字符隔开。我很少使用这个特性,因为大多数有用的范围都是预定义的;例如,

' % d '代替'[0 - 9]',和' % x”替代“[0-9a-fA-F]”。但是,如果需要查找八进制数,可以使用“[0-7]”,而不是像“[01234567]”这样的显式枚举。

我们可以通过用一个插入符号开头来得到任何字符集的补码:模式'[^0-7]'查找任何不是八进制数字的字符,'[^\n]'匹配任何不同于换行的字符。尽管如此,请记住,您可以用大写形式否定简单类:'%S''[^% S]'更简单。

我们可以使用重复和可选部分的修饰符使模式更加有用。模式在Lua提供四个标识符:

标识符

意义

+

1或多个重复

*

0或多个重复

-

0或更多懒惰重复


0或1个

这里重点说一下-标识符。它不是匹配最长的序列,而是匹配最短的序列。有时星号和减号之间没有区别,但通常它们给出的结果相当不同。例如,如果我们尝试查找一个模式为“[_%a][_%w]-”的标识符,我们将只找到第一个字母,因为'[_%w]-'将始终与空序列匹配。另一方面,假设我们想要删除C程序中的注释。许多人会首先尝试

'/%*.*%*/'(即“/*”后面跟一个后跟“*/”的任何字符序列,使用适当的转义进行编写)。然而,因为.*尽量展开,程序中的第一个"/*"只会以最后一个"*/"结束:

test = "int x; /* x */ int y; /* y */"
 print((string.gsub(test, "/%*.*%*/", "")))
 --> int x;

使用-可以得到我们想要的效果:

test = "int x; /* x */ int y; /* y */"
 print((string.gsub(test, "/%*.-%*/", "")))
 --> int x; int y;

与其他一些系统不同,在Lua中我们只能对一个字符类应用修饰符;无法在修饰符下对模式进行分组。例如,没有匹配可选单词的模式(除非该单词只有一个字母)。通常,我们可以使用本章最后介绍的一些高级技术来绕过这个限制。

如果模式以插入符号开头,则它只匹配主题字符串的开头。类似地,如果它以美元符号结尾,则只匹配subject字符串的结尾。我们既可以使用这些标记来限制找到的匹配,也可以使用它们来锚定模式。

模式中的另一个项是'%b',它匹配平衡的字符串。我们把这一项写成'%bxy',其中x和y是任意两个不同的字符;x作为开始字符,y作为结束字符。例如,模式'%b()'匹配字符串中以左括号开始并在相应的右括号结束的部分:

s = "a (enclosed (in) parentheses) line"
 print((string.gsub(s, "%b()", ""))) --> a line

通常,我们将此模式用作'%b()'、'%b[]'、'%b{}'或'%b<>',但是我们可以使用任何两个不同的字符作为分隔符。

最后,“%f[char-set]”表示边界模式。只有当下一个字符在字符集中,而前一个字符不在字符集中时,它才会匹配一个空字符串:

s = "the anthem is the theme"
 print((string.gsub(s, "%f[%w]the%f[%W]", "one")))
 --> one anthem is one theme

“%f[%w]”模式匹配非字母数字和字母数字字符之间的边界,“%f[%w]”模式匹配字母数字和非字母数字字符之间的边界。

因此,给定的模式只匹配字符串“the”作为整个单词。注意,我们必须在括号内编写char-set,即使它是一个单独的类。

边界模式将主题字符串中第一个字符之前和最后一个字符之后的位置视为空字符(ASCII码0)。在前面的示例中,第一个“the”以空字符(不属于集合“[%w]”)和t(属于集合“[%w]”)之间的边界开始。

捕获

捕获机制允许模式提取与模式部分匹配的主题字符串的部分,以供进一步使用。我们通过在括号中写入要捕获的模式部分来指定捕获。

当模式捕获时,函数string.match将每个捕获的值作为单独的结果返回;换句话说,它将字符串分解成捕获的部分。

pair = "name = Anna"
 key, value = string.match(pair, "(%a+)%s*=%s*(%a+)")
 print(key, value) --> name Anna 

 date = "Today is 17/7/1990"
 d, m, y = string.match(date, "(%d+)/(%d+)/(%d+)")
 print(d, m, y) --> 17 7 1990

在模式中,像'%n'这样的项(其中n是一位数字)只匹配第n个捕获的副本。作为一个典型的用法,假设我们想要在一个字符串中找到一个包含在单引号或双引号之间的子字符串。我们可以尝试使用'["'].-["']'这样的模式,即一个引语后面跟任何一个引语;但是我们会遇到像"it's all right"这样的字符串问题。为了解决这个问题,我们可以捕获第一个引用并使用它来指定第二个引用:

s = [[then he said: "it's all right"!]]
 q, quotedPart = string.match(s, "([\"'])(.-)%1")
 print(quotedPart) --> it's all right
 print(q) --> "

第一个捕获是引用字符本身,第二个捕获是引用的内容(匹配'.-'的子字符串)。

一个类似的例子是这个模式,它匹配Lua中的长字符串:

%[(=*)%[(.-)%]%1%]

它将匹配一个左方括号,后跟0个或多个等于号,然后是另一个右方括号,后跟任何内容(字符串内容),然后是一个右方括号,后跟相同数量的等于号,然后是另一个右方括号:

p = "%[(=*)%[(.-)%]%1%]"
 s = "a = [=[[[ something ]] ]==] ]=]; print(a)"
 print(string.match(s, p)) --> = [[ something ]] ]==]

捕获值的第三个用途是在gsub的替换字符串中。与模式类似,替换字符串也可以包含“%n”之类的项,在进行替换时,这些项将更改为相应的捕获。特别是,项目“%0”将成为整个匹配项。(顺便说一下,替换字符串中的百分号必须转义为“%%”。)例如,下面的命令复制字符串中的每个字母,每个字母之间有一个连字符:

print((string.gsub("hello Lua!", "%a", "%0-%0")))
 --> h-he-el-ll-lo-o L-Lu-ua-a!

这一个互换相邻字符:

print((string.gsub("hello Lua", "(.)(.)", "%2%1")))
 --> ehll ouLa

替换

正如我们已经看到的,我们可以使用函数或表作为string.gsub的第三个参数,而不是字符串。当使用函数调用时,string.gsub每次找到匹配的时候都会调用这个函数;每个调用的参数都是捕捉值,函数返回的值成为替换字符串。当使用表调用时,使用string.gsub使用第一个捕获作为键来查找表,关联的值用作替换字符串。如果调用或表查找的结果为nil,则gsub不会更改匹配。

作为第一个例子,下面的函数执行变量扩展:它用全局变量varname的值替换字符串中出现的每个$varname:

function expand (s)
  return (string.gsub(s, "$(%w+)", _G))   -- _G全局表
end
 
name = "Lua"; status = "great"
print(expand("$name is $status, isn't it?"))
--> Lua is great, isn't it?

print(expand("$othername is $status, isn't it?")) -- 全局表中没有对应的值
 --> $othername is great, isn't it?

URL编码

我们的下一个示例将使用URL编码,这是HTTP用来发送嵌入在URL中的参数的编码。这种编码将特殊字符(例如=、&和+)表示为“%xx”,其中xx是十六进制字符代码。之后,它将空格改为加号。例如,它将字符串“a+b = c”编码为“a%2Bb+%3D+c”。最后,它在每个参数名和参数值之间加上一个等号in,并在所有结果对name = value之后加上一个&。例如,值

name = "al"; query = "a+b = c"; q="yes or no"
--->
"name=al&query=a%2Bb+%3D+c&q=yes+or+no".

现在,假设我们要解码这个URL,并将每个值存储在一个表中,根据对应的名称进行索引。下面的函数进行基本的解码:

function unescape (s):
  s = string.gsub(s, "+", " ")
  s = string.gsub(s, "%%(%x%x)", function (h)
 	  return string.char(tonumber(h, 16))
 	end)
  return s
end
 
 print(unescape("a%2Bb+%3D+c")) --> a+b = c

要解码对名称=值,我们使用gmatch。因为名称和值都不能包含与符号或等号,所以我们可以使用'[^&=]+'模式来匹配它们:

cgi = {}
function decode (s)
  for name, value in string.gmatch(s, "([^&=]+)=([^&=]+)") do
    name = unescape(name)
    value = unescape(value)
    cgi[name] = value
  end
end

对gmatch的调用匹配表单name=value中的所有对。对于每一对,迭代器返回相应的捕获(由匹配字符串中的括号标记)作为名称和值的值。循环体简单地将unescape应用于两个字符串并将这对字符串存储在cgi表中。

相应的编码也很容易编写。首先,我们写escape函数;该函数将所有特殊字符编码为百分号,后跟十六进制字符代码(格式选项“%02X”生成一个包含两位数的十六进制数字,使用0作为填充),然后将空格改为加号:

function escape (s)
  s = string.gsub(s, "[&=+%%%c]", function (c)
      return string.format("%%%02X", string.byte(c))
    end)
  s = string.gsub(s, " ", "+")
  return s
end

function encode (t)
  local b = {}
  for k,v in pairs(t) do
    b[#b + 1] = (escape(k) .. "=" .. escape(v))
  end
  -- concatenates all entries in 'b', separated by "&"
  return table.concat(b, "&")
end

t = {name = "al", query = "a+b = c", q = "yes or no"}
print(encode(t)) --> q=yes+or+no&query=a%2Bb+%3D+c&name=al

Tab扩展

像’()'这样的空捕获在Lua中有特殊的意义。这个模式不捕获任何内容(一个无用的任务),而是捕获它在主题字符串中的位置,作为一个数字:

print(string.match("hello", "()ll()")) --> 3 5

(注意,这个示例的结果与我们从string.find的结果不同。查找,因为第二个空捕获的位置在匹配之后。)

一个使用位置捕捉的好例子是在字符串中展开制表符:

function expandTabs (s, tab)
  tab = tab or 8 -- tab "size" (default is 8)
  local corr = 0 -- correction
  s = string.gsub(s, "()\t", function (p)
      local sp = tab - (p - 1 + corr)%tab
      corr = corr - 1 + sp
      return string.rep(" ", sp)
    end)
  return s
end

gsub模式匹配字符串中的所有tab,捕捉它们的位置。对于每个制表符,匿名函数使用这个位置来计算到达一个制表符倍数的列所需的空格数:它从该位置减去1,使其相对于0,并添加corr来补偿前面的制表符。(每个tab的展开会影响下面几个tab的位置)然后更新下一个tab的更正:删除的tab为- 1,添加的空格为sp。最后,它返回一个具有适当空格数的字符串来替换tab。

为了完整起见,让我们看看如何反转这个操作,将空格转换为制表符。第一种方法还涉及到使用空捕捉来操作位置,但是有一种更简单的解决方案:在每8个字符处,我们在字符串中插入一个标记。然后,在标记前面有空格的地方,我们用制表符替换序列空格标记:

function unexpandTabs (s, tab)
  tab = tab or 8
  s = expandTabs(s, tab)
  local pat = string.rep(".", tab)
  s = string.gsub(s, pat, "%0\1")
  s = string.gsub(s, " +\1", "\t")
  s = string.gsub(s, "\1", "")
  return s
end

该函数首先展开字符串以删除之前的任何制表符。然后它计算一个辅助模式来匹配所有8个字符的序列,并使用这个模式在每8个字符之后添加一个标记(控制字符\1)。然后,它为一个或多个空格的所有序列替换一个制表符,后面跟一个标记。最后,删除剩下的标记(前面没有空格的标记)。

转换的技巧

模式匹配是操作字符串的强大工具。我们可以通过调用string.gsub来执行许多复杂的操作。然而,与任何力量一样,我们必须谨慎使用它。

模式匹配并不能替代正确的解析器。对于快速而粗糙的程序,我们可以对源代码进行有用的操作,但是要构建高质量的产品可能比较困难。作为一个好例子,考虑我们在C程序中用来匹配注释的模式:'/%*.-%*/'。如果程序有一个文字字符串包含“/*”,我们可能会得到一个错误的结果:

test = [[char s[] = "a /* here"; /* a tricky string */]]
 print((string.gsub(test, "/%*.-%*/", "<COMMENT>")))
 --> char s[] = "a <COMMENT>

具有这种内容的字符串很少。对于我们自己的使用,这个模式可能会完成它的工作,但是我们不应该发布一个有这种缺陷的程序。

通常,模式匹配对于Lua程序来说是非常有效的:我的新机器计算4.4 MB文本(850个K-words)中的所有单词所用的时间不到0.2秒。但我们可以采取预防措施。我们应该使模式尽可能具体;松散模式比特定模式要慢。一个极端的例子是'(.-)%$',用于获取字符串中第一个美元符号之前的所有文本。如果主题字符串有美元符号,则一切正常,但是假设字符串不包含任何美元符号。该算法将首先尝试匹配从t的第一个位置开始的模式。当字符串结束时,字符串的第一个位置的模式失败。然后,算法将再次执行整个搜索,从字符串的第二个位置开始,结果发现模式也不匹配,重复搜索字符串中的每个位置。这将花费一个二次的时间,导致在我的新机器中为一个200K字符的字符串花费超过4分钟。我们可以通过在字符串的第一个位置使用^(.-)%$锚定模式来纠正这个问题。如果在第一个位置找不到匹配项,锚就会告诉算法停止搜索。有了锚,比赛在百分之一秒内进行。

还要注意空模式,即匹配空字符串的模式。例如,如果我们尝试匹配名称与’%a*'的模式,我们会发现名称无处不在:

i, j = string.find(";$% **#$hello13", "%a*")
print(i,j) --> 1 0

在本例中,调用string.find已正确地找到字符串开头的空字母序列。在本例中,调用string.find已正确地找到字符串开头的空字母序列。

-修饰符结尾的模式是没有意义的,因为它只匹配空字符串。这个修饰符总是需要一些东西来固定它的扩展。类似地,包括模式.*是棘手的,因为这种建设可以扩大远远超过我们的预期。

有时,使用Lua本身来构建模式是很有用的。我们已经在函数中使用这个技巧将空格转换为制表符。作为另一个示例,让我们看看如何在文本中查找长行,例如包含70多个字符的行。长行是与换行不同的70个或更多字符的序列。我们可以用字符类'[^\n]'来匹配一个不同于换行的字符。因此,我们可以将一个长行匹配为一个字符重复70次的模式,并以该模式的重复结束(以匹配该行的其余部分)。我们可以用string.rep来创建它,而不是手工编写这个模式:

pattern = string.rep("[^\n]", 70) .. "+"

再举一个例子,假设我们要进行不区分大小写的搜索。实现此目的的一种方法是将模式中的任何字母x更改为类'[xX]',即一个类包含原始字母的小写和大写版本。我们可以用一个函数自动转换:

function nocase (s)
  s = string.gsub(s, "%a", function (c)
      return "[" .. string.lower(c) .. string.upper(c) .. "]"
    end)
  return s
end

print(nocase("Hi there!")) --> [hH][iI] [tT][hH][eE][rR][eE]!

有时,我们希望用s2替换s1的所有普通字符,而不将任何字符视为有魔力的字符。如果字符串s1和s2是文字,我们可以在编写字符串时向魔法字符添加适当的转义。如果这些字符串是变量值,我们可以使用另一个gsub为我们放转义:

s1 = string.gsub(s1, "(%W)", "%%%1")
 s2 = string.gsub(s2, "%%", "%%%%")

在搜索字符串中,我们转义了所有非字母数字字符(因此是大写W),而在替换字符串中,我们只转义了百分号。模式匹配的另一个有用的技术是在实际工作之前对字符串进行预处理。

假设我们想要将文本中的所有带引号的字符串都改为大写,其中一个带引号的字符串以双引号(")开头和结尾,但可能包含转义的引号("\""):

"This is \"great\"!".

处理这种情况的一种方法是对文本进行预处理,将有问题的序列编码为其他东西。例如,我们可以将“\”编码为“\1”。但是,如果原始文本已经包含一个“\1”,我们就有麻烦了。进行编码并避免此问题的一个简单方法是对所有序列进行编码“\x”“\ddd”,其中ddd为字符x的十进制表示:

function code (s)
  return (string.gsub(s, "\\(.)", function (x)
        return string.format("\\%03d", string.byte(x))
      end))
end

现在,编码字符串中的任何序列“\ddd”都必须来自编码,因为原始字符串中的任何“\ddd”也已编码。因此,解码是一个简单的任务:

function decode (s)
  return (string.gsub(s, "\\(%d%d%d)", function (d)
        return "\\" .. string.char(tonumber(d))
      end))
end

现在我们可以完成任务了。由于编码的字符串不包含任何转义的引号("\""),我们可以用引号来搜索引号内的字符串'"._"':

s = [[follows a typical string: "This is \"great\"!".]]
 s = code(s)
 s = string.gsub(s, '".-"', string.upper)
 s = decode(s)
 print(s) --> follows a typical string: "THIS IS \"GREAT\"!".

模式匹配函数对UTF-8字符串的适用性取决于模式。文字模式没有问题,因为UTF-8的关键属性是任何字符的编码都不会出现在任何其他字符的编码中。字符类和字符集只对ASCII字符有效。例如,模式'%s'适用于UTF-8字符串,但它只匹配ASCII空格;它将不匹配额外的Unicode空格,如非中断空格(U+00A0)或蒙古元音分隔符(U+180E)。

明智的模式可以为Unicode处理带来一些额外的功能。一个很好的例子是预定义的模式utf8。恰匹配一个UTF-8.charpattern。utf8库对这个模式的定义如下:

utf8.charpattern = [\0-\x7F\xC2-\xF4][\x80-\xBF]

第一部分是一个类,它匹配ASCII字符(range [0, 0x7F])或多字节序列的初始字节(range [0xC2, 0xF4])。第二部分匹配零个或多个连续字节(范围[0x80,0 xbf])

最常使用的词

本章编写一个程序找出文本中最常使用的词,将单词结果按出现次数排序

local counter = {}

for line in io.lines() do
  for word in string.gmatch(line, "%w+") do
    counter[word] = (counter[word] or 0) + 1
  end
end

local words = {} -- list of all words found in the text

for w in pairs(counter) do
  words[#words + 1] = w
end

table.sort(words, function (w1, w2)
    return counter[w1] > counter[w2] or
    counter[w1] == counter[w2] and w1 < w2
  end)

-- number of words to print
local n = math.min(tonumber(arg[1]) or math.huge, #words)

for i = 1, n do
  io.write(words[i], "\t", counter[words[i]], "\n")
end

日期和时间

标准库提供了很少的功能来操纵日期和时间在Lua。 和往常一样,它提供的都是标准C库中可用的内容。 不过,尽管表面上很简单, 我们可以用这个基本的支持做很多事情。

Lua对日期和时间使用两种表示方式。 第一个是通过一个数字,通常是整数。 虽然ISOC没有要求,但在大多数系统中,这个数字是秒数 一些固定的日期,叫做时代。 特别是,在POSIX和Windows系统中,时代是1970年1月1日0时UTC。

Lua用于日期和时间的第二个表示形式是表。这样的日期表有以下重要字段:year, month, day, hour, min, sec, wday, yday, and isdst。除isdst以外的所有字段都有整数值。前六个字段有明显的含义。wday字段是一周中的哪一天(1代表的是星期日);yday字段是一年中的哪一天(1月1日)。isdst字段是一个布尔值,如果夏令时生效,则为真。例如,1998年9月16日23:48:10(一个星期三)对应如下表:

{year = 1998, month = 9, day = 16, yday = 259, wday = 4,
 hour = 23, min = 48, sec = 10, isdst = false}

日期表不编码时区。 这要由程序来正确解释时区。

os.time函数

local date = os.time()
local day2year = 365.242 -- days in a year
local sec2hour = 60 * 60 -- seconds in an hour
local sec2day = sec2hour * 24 -- seconds in a day
local sec2year = sec2day * day2year -- seconds in a year
-- year
print(date // sec2year + 1970) -- 2020.0
-- hour 
print(date % sec2day // sec2hour + 8) -- 12
-- minutes
print(date % sec2hour // 60) -- 10
-- seconds
print(date % sec2day % 60) -- 43

os.date函数

函数os.date,尽管它的名称,但它是os.time的一种反转:它将一个表示日期和时间的数字转换为一些更高级的表示,无论是日期表还是字符串。 它是 第一个参数是格式字符串,描述我们想要的表示形式。 第二个参数是数字日期-时间;如果没有提供,则默认为当前日期和时间。

要生成日期table,我们使用格式字符串“*t”。 例如,调用os.date(“*t”,906000490)返回下表:

{year = 1998, month = 9, day = 16, yday = 259, wday = 4,
 hour = 23, min = 48, sec = 10, isdst = false}

格式化参照这个表:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CtJqH82G-1587953988183)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20200310122758522.png)]

例如:

print(os.date("%Y%m%d")) --- 20200310

Date–Time 操作

当os.date创建日期表时,它的字段都在适当的范围内。 但是,当我们将日期表给os.time时,它的字段不需要规范化。 这是一个重要的工具用于操纵日期和时间。

如果要得到40天前的日期:

t = os.date("*t") -- get current time
 print(os.date("%Y/%m/%d", os.time(t))) --> 2015/08/18
 t.day = t.day + 40
 print(os.date("%Y/%m/%d", os.time(t))) --> 2015/09/27

得到6个月后的日期:

t = os.date("t")
t.month = t.month + 6

计算两个日期的差值:

local t5_3 = os.time({year=2015, month=1, day=12})
 local t5_2 = os.time({year=2011, month=12, day=16})
 local d = os.difftime(t5_3, t5_2)
 print(d // (24 * 3600)) --> 1123.0

Bits和Bytes

数据结构

Lua用table实现arrays, records, lists, queues, sets

Arrays

在Lua中,使用整型作为下标的table配合初始化就能达到其他语言构建数组的效果:

local a = {}
for i = 1, 1000 do 
  a[i] = 0
end

lua给我们提供的一个便利的技巧就是使用#来获取长度。但这必须从1开始。

二维和多维数组

local mt = {}
for i = 1, N do
  local row = {}
  for j = 1, M do
    row[j] = 0
  end
  mt[i] = row
end

链表

空链表:

list = nil

链表结构:

list = {next = list, value = v}

遍历链表:

local l = list
while l do 
  visit l.value
  l = l.next
end

队列,双端队列

使用insert、remove方法实现这些操作。下面举例实现一个双端队列:

function listNew()
  return {first = 0, last = -1}
end

function pushFirst(list, value)
  local first = list.first - 1
  list.first = first
  list[first] = value
end

function pushLast(list, value)
  local last = list.last + 1
  list.last = last
  list[last] = last
end

function popFirst(list)
  local first = list.first
  if first > list.last then error("list is empty") end
  local value = list[first]
  list[first] = nil
  list.first = first + 1
  return value
end

function popLast(list)
  local last = list.last
  if list.first > last then errot("list is empty") end
  local value = list[last]
  list[last] = nil
  list.last = last - 1
end

反转table

正如我所说,以前,我们很少在Lua搜索。 相反,我们使用我们所称的索引表或反向表。

假设我们有一张表:

days = {"Sunday", "Monday", "Tuesday", "Wednesday",
 "Thursday", "Friday", "Saturday"}

那么反转表就是:

revDays = {["Sunday"] = 1, ["Monday"] = 2,
 ["Tuesday"] = 3, ["Wednesday"] = 4,
 ["Thursday"] = 5, ["Friday"] = 6,
 ["Saturday"] = 7}

实现方式如下:

revDays = {}
for k, v in pairs(days) do
  revDays[v] = k
end

Set和背包

set的实现如下:

function Set(list)
  local set = {}
  for _, l in ipairs(list) do set[l] = true end
  return set
end

bag,也称为multisets。与常规集合不同的是,每个元素可以出现多次。

function insert(bag, element)
  bag[element] = (bag[element] or 0) + 1
end

function remove(bag, element)
  local count = bag[element]
  bag[element] = (count and count > 1) and count - 1 or nil
end

String buffer

例如我们从文件中读取字符串然后拼接。通常代码如下:

local buff = ""
for line in io.lines() do
  buff = buff .. line .. "\n"
end

尽管它看起来很无辜,但Lua中的这个代码可能会对大文件造成巨大的性能惩罚:例如,在我的新机器上读取4.5MB文件需要30秒以上。

为什么? 为了了解发生了什么,让我们想象一下,我们在读取循环的中间;每行有20个字节,我们已经读取了大约2500行,所以Buff是一个具有50kB的字符串。 当Lua连接时 它分配一个具有50020字节的新字符串,并将来自Buff的50000字节复制到这个新字符串中。 也就是说,对于每一条新的线路,Lua移动大约50kB的内存,并增长。 在阅读了100条新行(只有2k B)之后,Lua已经移动了超过5MB的内存。 当Lua完成读取350kB时,它已经移动了超过50GB。 (这个问题 不是Lua所特有的:其他语言,其中字符串是不可变的值,呈现类似的行为,Java是一个著名的例子。

在我们继续下去之前,我们应该指出,尽管我说了很多,但这种情况并不是一个普遍的问题。 对于小字符串,上面的循环是好的。 要读取整个文件,Lua提供io.read(“a”) 备选办法,它一次读取整个文件。 然而,有时我们必须面对这个问题。 Java提供了StringBuffer类来改进问题。 在Lua中,我们可以使用一个表作为字符串缓冲区。调用方法table.concat,它返回给定列表所有字符串的了解。 使用concat,我们可以编写我们的前一个循环如下:

local buff = {}
for line in io.lines() do
  buff[#buff + 1] = line .. "\n"
end
local s = table.concat(buff)

我们还可以优化,因为table.concat提供了一个可选的参数作为连接:

local buff = {}
for line in io.lines() do
  buff[#buff + 1] = line
end
local s = table.concat(buff, "\n") .. "\n"

最后一步同样可以用这样的方法优化:

local buff = {}
for line in io.lines() do
  buff[#buff + 1] = line
end
t[#t + 1] = ""
local s = table.concat(buff, "\n")

数据文件和序列化

编译、执行和错误

尽管我们将Lua称为解释语言,但Lua总是在运行源代码之前将其预编译为中间形式。(这没什么大不了的:许多解释性语言都是如此。)编译阶段在解释语言中可能听起来不合适。然而,解释语言的显著特征并不是它们没有被编译,而是它们可以(并且容易)执行动态生成的代码。我们可以说,dofile这样的函数的存在使我们有权将Lua称为解释语言。

在本章中,我们将更详细地讨论Lua运行其块的过程、编译意味着什么(和做什么)、Lua如何运行编译的代码以及如何处理Lua中的错误 。

编译

以前,我们引入了dofile作为一种原始操作来运行Lua代码的块,但dofile实际上是一个辅助函数:函数loadfile完成了艰苦的工作。 就像dofile,loadfile 从文件中加载Lua块,但它不运行块。 相反,它只编译块并将编译后的块作为函数返回。 此外,与dofile不同,loadfile不会引发错误 但是返回错误代码。 我们可以将dofile定义如下:

function dofile(filename) 
  local f = assert(loadfile(filename))
  return f()
end

对于简单的任务,dofile非常方便,因为它可以在一次调用中完成整个任务。然而,loadfile更加灵活。在出现错误的情况下,loadfile返回nil加上错误消息,这允许我们以自定义的方式处理错误。此外,如果我们需要多次运行一个文件,我们可以调用loadfile一次,然后多次调用它的结果。这种方法比多次调用dofile要便宜得多,因为它只编译一次文件。(与语言中的其他任务相比,编译是一项开销较大的操作。)

函数load类似于loadfile,只是它从字符串或函数中读取其块,而不是从文件中读取。例如:

f = load("i = i + 1")

在此代码之后,f将是一个函数:

i = 0
f(); print(i) --> 1
f(); print(i) --> 2

函数load是强大的,我们应该小心使用它。 它也是一个昂贵的功能(与一些替代方案相比),并可能导致无法理解的代码。 在你使用它之前,确保没有更简单的方法来解决手头的问题。

如果我们想做一个权宜之计的dostring(即加载和运行块),我们可以直接调用加载结果:

load(s){}

但是,如果有任何语法错误,Load将返回nil,最终的错误消息将类似于“attempt to call a nil value”。 为了更清晰的错误信息,最好使用断言:

assert(load(s))()

load最典型的使用是运行外部代码(即来自我们程序之外的代码片段)或动态生成的代码。 例如,我们可能希望绘制一个用户定义的函数;用户输入函数代码,然后我们使用load来评估它。 注意,Load期望一个块,即语句。 如果要评估表达式,可以在表达式前面加上前缀return,这样我们就得到了一个语句,该语句返回给定表达式的值。 见例子:

print("Enter your expression:")
local line = io.read()
local func = assert(load("return " .. line))
print("the value of your expression is " .. func())

我们也可以用读取器函数作为它的第一个参数来调用load。 读取器函数可以部分地返回块;load连续地调用读取器,直到返回nil,这是块的结束信号。 例如,下一次调用相当于loadfile:

f = load(io.line(filename, "L"))

省略一段。。

预编译代码

Lua在运行源代码之前预先编译源代码。 lua还允许我们以预先编译的形式分发代码。

最简单的方法制作一个预先编译的文件,在Lua行话中,也称为二进制块-是与标准分布中的Luac程序一起使用的。 例如,下一次调用将创建一个新的文件prog.lc,其中包含一个预编译版的 prog.lua:

luac -o prog.lc prog.lua

Lua解释器可以执行这个新文件,就像普通的Lua代码一样,执行完全与它的原始源代码:

lua prog.lc

Lua接受预先编译的代码,大部分在它接受源代码的任何地方。 特别是,loadfileload都接受预先编译的代码。

举个例子,我们可以通过Lua写一个最小的luac

p = loadfile(arg[1])
f = io.open(arg[2], "wb")
f:write(string.dump(p))
f:close()

这里的关键函数是String.dump:它接收一个Lua函数,并以字符串的形式返回其预编译的代码,经过适当的格式化后由Lua加载回来。

luac程序提供了一些其他有趣的选择。 特别是,选项-l列出编译器为给定块生成的操作码。 例如:

a = x + y - z

luac -l生成:

main <stdin:0,0> (7 instructions, 28 bytes at 0x988cb30)
0+ params, 2 slots, 0 upvalues, 0 locals, 4 constants, 0 functions
    1 [1] GETGLOBAL 0 -2 ; x
    2 [1] GETGLOBAL 1 -3 ; y
    3 [1] ADD 0 0 1
    4 [1] GETGLOBAL 1 -4 ; z
    5 [1] SUB 0 0 1
    6 [1] SETGLOBAL 0 -1 ; a
    7 [1] RETURN 0 1

预编译形式的代码并不总是比原始代码小,但加载速度更快。 另一个好处是,它提供了保护,防止意外变化的来源。 但与源代码不同 恶意损坏的二进制代码可以崩溃Lua解释器,甚至执行用户提供的机器代码。 当运行通常的代码时,没有什么可担心的。 但是,你应该避免运行预编译形式的不可信代码。 函数load对此任务有一个正确的选项。

除了必需的第一个参数外,load还有三个参数,它们都是可选的。第二个是块的名称,仅在错误消息中使用。第四个论点是环境,我们将在第22章环境中讨论。第三个论点是我们感兴趣的;它控制可以加载哪些类型的块。如果存在,这个参数必须是一个字符串:字符串“t”只允许文本(正常)块;“b”只允许二进制(预编译)块;默认的“bt”允许两种格式。

错误

我们必须以最好的方式处理错误。 由于Lua是一种扩展语言,经常嵌入到应用程序中,因此它不能在错误发生时简单地崩溃或退出。 相反,每当出现错误时,Lua必须提供处理它的方法。

任何Lua遇到的意外情况都会引起错误。 当程序试图添加不是数字的值、调用不是函数的值、不是表的索引值时,就会发生错误 等等。 (我们可以使用元表修改这种行为,我们将在后面看到) 我们还可以显式地调用函数error来提出这个错误,将错误消息作为参数。 一般来说, 这个函数是在代码中发出错误信号的适当方法:

print("enter a number")
n = io.read("n")
if not n then error("invalid input") end

这种在某种条件下调用错误的构造是如此常见,以至于Lua只为这项工作具有一个内置函数,称为断言:

print("enter a number")
n = assert(io.read("*n"), "invalid input")

函数assert检查其第一个参数是否为false,并简单地返回此参数;如果参数为false,则assert抛出一个错误。它的第二个参数,消息,是可选的。 然而,断言是一个常规函数。 因此,Lua总是在调用函数之前评估其参数。 如果我们写一些代码如下:

n = io.read()
assert(tonumber(n), "invalid input:" .. n .. "is not a number")

即使n是一个数字,Lua也总是做连接。 在这种情况下使用显式测试可能更明智。

当函数发现意外情况(异常)时,它可以假设两种基本行为:它可以返回错误代码(通常为nil或false),或者它可以抛出错误,调用error。 在这两个选项之间没有固定的选择规则,但我使用以下指南:容易避免的异常应该引起错误;否则,它应该返回错误代码。

例如,让我们考虑一下math.sin。 当被叫到桌子上时,它应该如何表现? 假设它返回一个错误代码。 如果我们需要检查错误,我们必须写这样的东西:

local res = math.sin(x)
if not res then -- error?
  error-handling code

但是,在调用函数之前,我们可以很容易地检查此异常:

if not tonumber(x) then -- x is not a number ?
  error-handling code

错误处理和异常

对于许多应用程序,我们不需要在Lua中进行任何错误处理;应用程序执行此处理。 所有Lua活动都是从应用程序的调用开始的,通常要求Lua运行 一大块。 如果有任何错误,此调用将返回错误代码,以便应用程序可以采取适当的操作。 在独立解释器的情况下,它的主循环只是打印错误 并继续显示提示并运行给定的命令。

但是,如果我们想处理Lua代码中的错误,我们应该使用函数pcall(protected call)来封装我们的代码。

假设我们要运行一段Lua代码,并捕获运行该代码时引发的任何错误。 我们的第一步是将这段代码封装在一个函数中, 我们经常使用匿名函数来实现这一点。 然后,我们通过pcall调用该函数:

local ok, msg = pcall(function()
      some code
      if unexpected_condition then error() end
      some code 
      print(a[i])
      some code 
    end)

if ok then -- 当调用pcall时没有产生错误
     regular coe
else -- pcall产生错误
     error-handling code
end

函数pcall在受保护模式下调用其第一个参数,以便在函数运行时捕获任何错误。 无论发生什么,pcall函数都不会引起任何错误。 如果没有 错误,pcall返回true,加上调用返回的任何值。 否则,它返回false,加上错误消息。

尽管它的名称,错误消息不一定是字符串;更好的名称是error object,因为pcall将返回我们传递给error的任何Lua值:

local status, err = pcall(function() error({code = 121}) end)
print(err.code) --> 121

这些机制提供了我们在Lua进行异常处理所需的一切。 我们抛出一个异常与错误,并抓住它与pcall。 错误消息标识错误类型。

错误信息和回溯

虽然我们可以使用任何类型的值作为错误对象,但通常错误对象是描述错误的字符串。 当出现内部错误时(例如试图索引非表式价值),Lua生成错误对象,在这种情况下,错误对象总是字符串;否则,错误对象是传递给函数error的值。 当对象是字符串时,Lua尝试添加一些 有关错误发生地点的信息:

local status, err = pcall(function () error("my error") end)
print(err) --> stdin:1: my error

位置信息给出了块的名称(例中的stdin)加上行号(例中的1)。

函数错误有一个额外的第二个参数,它给出了它应该报告错误的级别。 我们使用这个参数来责怪别人的错误。 例如,假设我们编写一个函数它的第一个任务是检查它是否被正确调用:

function foo(str)
  if type(str) ~= "string" then 
    error("string expected")
  end
  regular code
end

事实上,Lua指出是Foo函数的错误——毕竟,是它称之为error——而不是真正的罪魁祸首,即调用者。 为了纠正这个问题,我们通知error,它正在报告错误是在调用层次结构中的第二级(第一级是我们自己的函数):

function foo(str)
  if type(str) ~= "string" then 
    error("string expected", 2)
  end
  regular code
end

通常,当发生错误时,我们想要的调试信息比发生错误的位置要多。 至少,我们需要一个回溯,显示导致错误的完整调用堆栈。当pcall返回其错误消息时,它会破坏堆栈的一部分(从它到错误点的部分)。 因此,如果我们想要一个回溯,我们必须在pcall返回之前构建它。为了达到这个目的,Lua提供函数xpcall。 它像pcall一样工作,但它的第二个参数是消息处理程序函数。 如果发生错误,Lua在堆栈展开之前调用此消息处理程序,因此t 它可以使用调试库来收集它想要的关于错误的任何额外信息。 两个常见的消息处理程序是debug.debug,它给我们一个Lua提示,这样我们就可以自己检查了 当错误发生时发生了什么;以及debug.traceback,它用traceback构建了一个扩展的错误消息。后者是独立解释器用来构建错误信息的函数 。

模块和包

通常,Lua不设置策略。 相反,Lua提供了足够强大的机制,让开发人员团队实施最适合他们的策略。 然而,这种方法对模块并不能很好的适用。 模块系统的主要目标之一是允许不同的组共享代码。 缺乏共同政策阻碍了这种分享。

从5.1版本开始,Lua为模块和包定义了一组策略(一个包是模块的集合)。 这些政策不要求语言提供任何额外的便利;程序员可以使用我们到目前为止看到的东西来实现它们。 程序员可以自由使用不同的策略。 当然,替代实现可能导致无法使用foreign模块的程序,foreign程序不能使用该模块。

从用户的角度来看,模块是一些代码(Lua或C),可以通过require函数加载,并创建和返回一个表。模块导出的所有东西,比如函数和常量,都定义在这个表中,这个表作为一种名称空间。

例如,所有标准库都是模块。 我们可以这样使用数学库:

local m = require("math")
print(m.sin(3.14)) --> 0.0015

独立的编译器预加载所有标准库类似于:

math = require("math")
string = require("string")

预加载使得我们可以直接使用类似于math.sin()

使用table来实现模块的一个明显好处是,我们可以像任何其他表一样操纵模块,并使用Lua来创建额外的便利。 在大多数语言中,模块不是“first-class”值(也就是说,它们不能存储在变量中,不能作为参数传递给函数等。 )。

通常调用模块中的函数如下:

local mod = require("mod")
mod.foo()

当然,本地变量的名称可以自定义。

local m = require("mod")
m.foo()

也可以给函数名自定义名称:

local m = require("mod")
local f = m.foo()
f()

函数require

require的第一步是检查table package.loaded模块是否已经加载。如果是,require返回其相应的值。因此,一旦一个模块被加载,其他调用 要求相同的模块只需返回相同的值,而不再一次运行代码。

如果模块尚未加载,则require搜索具有模块名称的Lua文件。 (此搜索由变量package.path引导,我们将在后面讨论。) 如果它找到这样的文件用loadfile然后加载。 结果是一个我们称为loader的函数。 加载程序是一个函数,当调用时,加载模块。

如果require无法找到具有模块名称的Lua文件,它将搜索具有该名称的C库(在这种情况下,搜索由变量package.cpath引导。)如果它找到一个C库,它就用低级函数package.loadlib加载它,查找一个名为luaopen_modname的函数。本例中的加载程序是loadlib的结果,它是用Lua函数表示的C函数luaopen_modname

无论模块是在Lua文件中还是在C库中找到的,require现在有一个加载程序。 要最终加载模块,require用两个参数调用加载程序:模块名和 它得到加载程序的文件的名称。 (大多数模块忽略了这些参数。) 如果加载程序返回任何值,require返回此值并将其存储在package.loaded表中,使得他在未来对这个模块的调用是相同的。 如果加载程序不返回任何值,并且package.loaded[@rep[modname]]仍然是空的,require表现为模块返回true。 如果没有这个修正,对require的后续调用将再次运行该模块。

要强制要求加载同一个模块两次,我们可以:

package.loaded.modname=nil

下次需要该模块时,require将从头来过。

针对require的一个常见投诉是,它不能将参数传递给正在加载的模块。 例如,数学模块可以在度数和弧度之间进行选择:

-- bad code
local math = require("math", "degree")

这里的问题是require的主要目标之一是避免多次加载模块。一旦一个模块被加载,它将被程序中再次需要它的任何部分重用。如果相同的模块需要不同的参数,就会产生冲突。如果你真的想让你的模块有参数,最好是创建一个显式的函数来设置它们,如下所示:

local mod = require("mode")
mod.init(0, 0)

如果初始化函数返回模块本身,我们可以这样写代码:

local mod = require("mode").init(0, 0)

无论如何,请记住模块本身只加载一次;它负责处理冲突的初始化。

重命名一个模块

通常,我们使用模块的原始名称,但有时我们必须重命名模块以避免名称冲突。典型的情况是,我们需要加载同一模块的不同版本,例如用于测试。Lua模块没有在内部固定它们的名称,因此通常重命名. Lua文件就足够了。但是,我们不能编辑C库的目标代码来纠正它的luaopen_*函数的名称。为了允许这样的重命名,require使用了一个小技巧:如果模块名包含连字符,那么在创建luaopen_*函数名时,需要去掉连字符后面的后缀。例如,如果一个模块被命名为mod-v3.4, require期望它的open函数被命名为luaopen_mod,而不是luaopen_mod-v3.4(无论如何它都不是一个有效的C名称)。因此,如果我们需要使用两个名为mod的模块(或同一个模块的两个版本),我们可以将其中一个重命名为mod-v1。当我们调用m1 = require("mod-v1")时,require将查找重命名的文件mod-v1,并在该文件中查找原来名为luaopen_mod的函数。

路径搜索

搜索者

在Lua中编写模块的基本方法

在Lua中创建模块的最简单方法非常简单:我们创建一个表,将所有要导出的函数放在其中,然后返回这个表。“复数的简单模块”说明了这种方法

local M = {}

-- 创建新的复数
local function new(r, i)
  return {r = r, i = i}
end

-- 添加 new 到模块
M.new = new

-- 常量 i
M.i = new(0, 1)

function M.add(c1, c2)
  return new(c1.r + c2.r, c1.i + c2.i)
end

function M.sub(c1, c2)
  return new(c1.r - c2.r, c1.i - c2.i)
end

function M.mul(c1, c2)
  return new(c1.r * c2.r - c1.i * c2.i, c1.r * c2.i + c1.i * c2.r)
end

local function inv(c)
  local n = c.r ^ 2 + c.i ^ 2
  return new(c.r/n, -c.i/n)
end

function M.div (c1, c2)
  return M.mul(c1, inv(c2))
end

function M.tostring (c)
  return string.format("(%g,%g)", c.r, c.i)
end

return M

注意,我们如何将new和inv定义为私有函数,只需将它们声明为块的本地函数即可。

有些人不喜欢最后的返回语句。消除它的一种方法是将模块表直接分配到package.loaded

local M = {}
package.loaded[...] = M
-- 接下来和上面一样编写,只不过不需要返回

另一个方式:

local function new (r, i) return {r=r, i=i} end

-- defines constant 'i'
local i = complex.new(0, 1)

other functions follow the same pattern

return {
  new = new,
  i = i,
  add = add,
  sub = sub,
  mul = mul,
  div = div,
  tostring = tostring,
}

这种方法的优点是什么?我们不需要在每个名字前面加上m或类似的前缀;有明确的出口清单;我们定义和使用导出函数和内部函数的方式与模块内部相同。缺点是什么?导出列表在模块的末尾,而不是在模块的开头,作为一个快速文档,它更有用;导出列表有点多余,因为每个名字都要写两遍。(最后一个缺点可能会变成优点,因为它允许函数在模块内外有不同的名称,但我认为程序员很少这样做。)

无论如何,请记住,无论我们如何定义一个模块,用户都应该能够以一种标准的方式使用它:

local cpx = require("complex")
print(cpx.tostring(cpx.add(cpx.new(3, 4), cpx.i))) -->(3, 5)

稍后,我们将看到如何使用一些高级Lua特性(如元数据和环境)来编写模块。但是,除了一种检测错误创建的全局变量的良好技术外,我只在模块中使用基本方法。

子模块和包