lua语言:闭包模式匹配日期编译模块

Posted traditional

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了lua语言:闭包模式匹配日期编译模块相关的知识,希望对你有一定的参考价值。

lua中的闭包

在lua语言中,函数是严格遵循词法定界(lexical scoping)的第一类值(first-class value)。

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

词法定界意味着,内层函数可以访问外层函数中的变量。

上面两个特性组合起来,为lua语言带来了极大的灵活性。

函数是第一类值

我们来看看lua中是如何体现函数是第一类值的

a = print
a("hello world")  -- hello world
-- 函数可以作为一个变量自由传递


function f(func, str, substr)
    print(func(str, substr))
end

f(string.find, "mashiro", "shi")  -- 3   5

function f(func, str, start, end_)
    print(func(str, start, end_))
end
f(string.sub, "satori", 2, -2)  -- ator
-- 函数可以作为参数传递给另一个函数


function f()
    return string.gsub
end

-- 函数也可以返回一个函数
print(f()("hello mashiro", "mashiro", "satori"))  -- hello satori	1



-- 对于表也是一样的
t = {print, std = io.write }
t[1]("mashiro")  -- mashiro
t["std"]("satori
")  -- satori


-- 两个函数也能替换
math.type = string.sub
print(math.type("xxx", 2, -2))  -- x

标准库table提供了一个sort函数,专门用来对表里面的内容进行排序的,但是怎么排,lua没有提供具体的方法,而是需要我们通过一个函数来指定

t = {
    {name = "mashiro", age = 16},
    {name = "satori", age = 17},
    {name = "nagisa", age = 20},
    {name = "kurisu", age = 18},
}

-- 会对元素进行比较,因为a.age > b.age的话,结果为true
-- 所以年龄大的,会排在前面。在python中,满足条件的排在后面
table.sort(t, function(a, b) return a.age > b.age end)
print(t[1].name, t[1].age)  -- nagisa	20

像这种没有函数名的函数,我们也可以称之为匿名函数。匿名函数不能单独定义,需要作为值进行传递,比如放到表中、作为参数、或者使用变量进行接收等等。

我们看到table.sort它需要接收一个函数作为参数,因此像table.sort这样的函数我们也称之为是高阶函数

非全局函数

由于函数是第一类值,因此一个显而易见的结果就是: 函数不仅可以被存储在全局变量中,还可以存储在表字段和局部变量中。

lib = {}

function add(a, b)
    return a + b
end

lib.a = add

print(lib.a(1, 22))  -- 23

上面是将函数存储在表中,我们在上面已经见识了,下面看看将函数存储在局部变量中是什么样子的。

function outer(a, b)
    function inner(c)
        return a + b + c
    end
    -- 这里必须要返回inner,否则是无法调用inner的
    return inner
end


-- 上面我们就实现了一个闭包
print(outer(1, 2)(33))  -- 36
-- 但是问题来了,我们看到inner居然可以直接调用
-- 因为我们说不管在什么地方,只要没有local关键字,定义的都是全局的
-- 所以尽管不能上来就调用inner,但是当outer调用之后,inner就会被加入到全局变量中
-- 可问题是,结果为啥是6,这是因为inner里面需要a和b,所以当outer调用之后,inner就记住了a和b的值
-- 而我们上面给outer的a和b传递的值是1和2,因此再调用inner的时候,a和b还是1和2
print(inner(3))  -- 6
print(inner(8))  -- 11

-- 重新执行outer,那么inner会被重新定义
-- 此时inner记住的a和b就变成了11和22
print(outer(11, 22)(33))  -- 66
print(inner(1))  -- 34

那么如何将函数定义成局部的呢?

function outer(a, b)
    local function inner(c)
        return a + b + c
    end
    -- 这里必须要返回inner,否则是无法调用inner的
    return inner
end


-- 此时就将inner定义成局部的了,因为我们使用了local关键字
-- 所以我们看到local也可以作用于函数
print(outer(1, 2)(3))  -- 6
print(inner)  -- nil


-- 那如何将函数保存起来呢
function f1(a)
    local f2 = function(b) return a + b end
    return f2
end

print(f1(1)(22))  -- 23
print(f2)  -- nil

将函数保存在局部变量中有一个坑,我们来看看

function f1()
    local function f2(n)
        if n == 1 then
            return 1
        else
            return n + f2(n - 1)
        end
    end
    return f2
end
-- 上面是一个简单的求和,通过递归来实现
print(f1()(100))  -- 5050



-- 如果是下面这种形式呢
function f1()
    local f2 = function(n)
        if n == 1 then
            return 1
        else
            return n + f2(n - 1)
        end
    end
    return f2
end
-- 我们代码和上面的版本没有太大区别,只不过将函数使用局部变量保存了
-- 执行的时候会出现什么结果呢?
f1()(100)

上面递归求和的第二个版本是否存在问题呢?答案是存在的,当调用内部的n + f2(n - 1)的时候,你会发现f2不存在。为什么是这个结果,也很好理解,我们用右边那一大坨函数赋值给f2,但是函数里面在赋值给f2的同时,里面已经就引用了f2。所以此时的f2是一个nil,或者说会去外部去找这个f2。

function f1()
    local f2
    f2 = function(n)
        if n == 1 then
            return 1
        else
            return n + f2(n - 1)
        end
    end
    return f2
end
-- 我们代码和上面的版本没有太大区别,只不过将函数使用局部变量保存了
-- 执行的时候会出现什么结果呢?
print(f1()(100))  -- 5050

此时就没有问题了,因为我们先定义了一个局部变量f2,尽管定义的时候f2还没有正确的值,但是一旦当执行函数的时候,f2就已经有了正确的值。

词法定界

关于词法定界,其实我们上面已经见识到了, 就是内层函数访问外层函数的变量

a = "xxx"

function f1()
    local a = 123
    local function f2()
        return a
    end
    return f2
end

-- 此时返回的是f1中的a,内层函数可以访问外层函数的变量
print(f1()())  -- 123



a = "xxx"

function f1()
    a = 123
    local function f2()
        return a
    end
    return f2
end
-- f1中的a和全局的是一个a
print(f1()(), a)  -- 123	123



a = "xxx"

function f1()
    local a = 123
    local function f2()
        local a = ">>>"
        return a
    end
    return f2
end
-- 自身有a这个变量,就不会到外界去找了
print(f1()())  -- >>>

来个小栗子:

function f1(str)
    local function f2(substr)
        return string.find(str, substr)
    end
    return f2
end

f2 = f1("abcde")
print(f2("ab"))  -- 1	2
print(f2("bcd"))  -- 2	4
print(f2("abcde"))  -- 1	5

lua中的模式匹配

下面来介绍一下lua中的模式匹配,这个很明显是针对字符串的,但是与其它脚本语言不同,lua中并没有正则表达式来进行模式匹配(pattern matching),lua没有提供POSIX风格的正则、也没有提供Perl风格的正则。

之所以这么做的主要原因是大小问题,一个典型的POXIS正则表达式实现需要超过4000行代码,这比lua标准库所有代码的一半还要多。

而lua提供的模式匹配的实现,代码只有不到600行。尽管无法实现正则的所有功能,但也是非常强大的,并且同时还具有和POXIS不同但又可与之匹配的功能。

模式匹配的相关函数

函数 string.find

这个我们在介绍string库的时候,已经说过了,功能就是查找子串在字符串中出现的开始位置和结束位置,但是我们当时介绍的比较简单,而string.find还支持更复杂的功能。

-- 查找第一个子串出现的位置
print(string.find("abc abc", "abc"))  -- 1	3

-- 可以指定第三个参数,表示起始位置
-- 从第2个开始,那么就只能找第二个abc了
print(string.find("abc abc", "abc", 2))  -- 5	7


-- 除了第三个参数,还可以有第四个参数
-- 一个布尔值,表示是否进行简单搜索,默认是false
-- print(string.find("abc [abc", "[abc")),这行代码是会报错的
-- 因为[在lua中具有特殊意义,子串中不可以出现一个单独的[

-- 如果指定为true的话,表示单纯的字符串匹配,子串中的[就没有特殊含义了
print(string.find("abc [abc", "[abc", 0, true))  -- 5	8

函数 string.match

string.match和string.find类似,但是它返回的是一个匹配上的子串,而不是位置。

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

-- 虽然能匹配上,但是貌似没啥卵用
-- 别急,继续看
print(string.match("2020-01-01", "%d+-%d+-%d+"))  -- 2020-01-01

-- 这样是不是就有用了呢?关于%d、以及string.match更高级的用法,我们后面会说

函数 string.gsub

这个函数我们也见过,接收三个参数:字符串、要替换的部分、替换成指定字符串,但是gsub还可以接收第4个参数,表示最多替换多少个。

print(string.gsub("aaa", ‘a‘, ‘b‘))  -- bbb	3
print(string.gsub("aaa", ‘a‘, ‘b‘, 2))  -- bba	2

模式

lua中也提供了其它语言中,类似于d、s之类的功能,但是lua中用的不是反斜杠,而是%,并且%(在模式匹配中)也是转义字符。

  • . 匹配 任意字符
  • %a 匹配 字母
  • %c 匹配 控制字符
  • %d 匹配 数字
  • %g 匹配 处空格之外的可打印字符
  • %l 匹配 小写字母
  • %u 匹配 大写字母
  • %p 匹配 标点符号
  • %s 匹配 空白字符
  • %w 匹配 字母和数字
  • %x 匹配 十六进制数字

除此之外,也提供了+、-、()、[]之类的具有特殊意义的字符,这个有兴趣可以自己查看,这里不说了。

lua中的日期和时间

lua中有两个函数,用于操作日期和时间,我们来看一下。

-- 调用os.time会返回当前时间的时间戳
print(os.time())  -- 1592124891
-- 也可以指定年月日时分秒
print(os.time({year=2020, month=5, day=3, hour=14, min=33, sec=51}))  -- 1588487631


-- 还有os.date
-- 自动打印当前的日期,除此之外还可以传入一个时间戳,来打印指定格式的日期
-- 返回的是一个string类型
print(os.date("%Y-%m-%d"))  -- 2020-06-14
print(os.date("%Y-%m-%d", os.time({year=2020, month=4, day=3})))  -- 2020-04-03

lua中关于日期的符号如下:

技术图片

编译、执行和错误

虽然我们把 Lua 语言称为解释型语言( interpreted language ),但 Lua 语言总是在运行代码前先预编译( precompile )源码为中间代码(这没什么大不了的,很多解释型语言也这样 做)。编译( compilation )阶段的存在听上去超出了解释型语言的范畴,但解释型语言的区分并不在于源码是否被编译,而在于是否有能力(且轻易地)执行动态生成的代码。

下面我们会详细学习lua语言运行代码的过程、编译究竟是什么意思和做了什 么、 Lua 语言是如何运行编译后代码的、以及在编译过程中如何处理错误。

编译

lua中提供了一个load函数,我们来看看是做什么的。

f = load("i = 1 + 1")
-- 当调用f的时候,load里面的字符串就会当成代码来执行
-- 有点类似于python中的exec
print(i)  -- nil
f()
print(i)  -- 2

尽管load的功能很强大,但还是要谨慎使用,因为相比其他的可选函数而言,load的开销比较大。另外,我们当前将load函数的返回值保存了下来,但是也可以不保存。

-- 直接调用
load("s = ‘这是字符串‘")()
print(s)  -- 这是字符串


-- load里面还可以放入一个字符串形式的函数
-- 多行的话,我们可以通过[[ ]]来代替引号
load(
[[
function f()
    return "hello mashiro"
end
]]
)()
print(f())  -- hello mashiro

如果load中的字符串不符合lua的语法规则,那么会得到什么呢?

print(load("s = ‘这是字符串‘"))  -- function: 00000000001bd010

-- 我们看到如果符合语法规则,那么会返回一个函数
-- 不符合的话,会返回nil和错误信息
print(load("s === ‘这是字符串‘"))  -- nil    [string "s === ‘这是字符串‘"]:1: syntax error near ‘==‘


-- 因此最好的办法是使用assert函数
-- 如果里面的值为nil或者false,直接把异常抛出来
-- 比如assert(2 == 2)正常通过,assert(2!=2)就会报错,因为里面的值为false
-- 而如果为false或者nil,那么assert就会将抛出异常
-- 如果是返回了多个值,那么就看第一个值是否为nil或者false
print(load("s = ‘这是字符串‘"))  -- function: 00000000001bd010

-- 我们看到如果符合语法规则,那么会返回一个函数
-- 不符合的话,会返回nil和错误信息
print(load("s === ‘这是字符串‘"))  -- nil    [string "s === ‘这是字符串‘"]:1: syntax error near ‘==‘


assert(load("s === ‘这是字符串‘"))
-- 返回值的第一个为nil,所以报错了,会把异常值抛出来
--[[
C:lualua.exe: lua/5.lua:9: [string "s === ‘这是字符串‘"]:1: syntax error near ‘==‘
stack traceback:
	[C]: in function ‘assert‘
	lua/5.lua:9: in main chunk
	[C]: in ?
]]

关于assert,可能有人不是很清楚,这里来说一下。

-- assert是用于断言的
assert(2 == 2)

print("正常通过")

-- 如果是assert(2 != 2),就通不过了


function f1()
    return 123, nil
end
assert(f1())

-- 上述f1函数返回了两个值,而第一个值为真的,所以正常通过

function f2()
    return nil, 123
end

-- 这里就通不过了,因为f2的返回值的第一个为nil
-- 如果报错了,那么会将返回值的第二个作为异常信息
assert(f2())
--[[
C:lualua.exe: 123
stack traceback:
	[C]: in function ‘assert‘
	lua/5.lua:23: in main chunk
	[C]: in ?
]]

-- assert一旦断言失败,那么程序就终止了,不会再往下走了

回到load函数,load函数里面如果是字符串,那么其实没有什么意义。f = load("i = 1")和function f() i = 1 end两者是完全等价的。

但是,由于后者、也就是定义函数的方式,代码会与其外层的函数一起被编译,所以其执行速度要快得多。与之对比,调用函数 load 时会进行一次独立编译

不过由于涉及到词法定界,上述示例的两段代码不一定完全等价

i = 123
function f1()
    local i = 0
    f = load("i = i + 1; print(i)")
    g = function () i = i + 1;print(i) end
    f() 
    g()
end

f1()
--[[
124
1
]]
print(i)  -- 124


-- 我们看到全局中有一个i,函数f1中也有一个局部变量i
-- 对于load来说,会直接去找全局变量i,因为load总是在全局环境中编译代码段
-- 而内层函数在找i的时候,会现在自身中找、然后是外层函数、最后是全局变量
-- 所以f()执行打印124,此时外部的i也被修改了,内层函数由于找的是局部变量i,所以打印的是1

函数 load 最典型的用法是执行外部代码,比如我们可以在控制台中输入代码,因为收到的是字符串,所以正好可以通过load将其当成代码来执行。

但是这样能够处理的内容有限,所以在lua中还提供了一个loadfile,可以将一个文件里面的字符串当成代码来执行。

function add(a, b)
    return a + b
end


a = 123
b = "xxx"

上面是一个文件1.txt,我们通过loadfile进行加载,加载的时候不需要文件名一定以lua结尾,只是将文件里面的内容读取出来,只要内容符合lua的语法规范即可。

loadfile("1.txt")()

print(add(11, 22))  -- 33
print(a)  -- 123
print(b)  -- xxx

预编译的代码

正如之前所说,lua在运行源代码之前会对源代码进行预编译,lua也可以允许我们手动编译源代码。

编译的方式可以通过luac进行编译,编译的文件已lc结尾。比如将5.lua编译成5.lc,就可以通过:"luac -o 5.lc 5.lua",编译之后lua解释器会像执行普通源代码一样执行编译之后的文件,完成与原来代码一致的动作。

可以自己尝试一下

错误

"人非圣贤孰能无过",因此我们必须尽可能的处理错误。并且由于lua是一种经常被嵌入到应用程序中的扩展语言,所以当错误发生时不能简单的崩溃或者退出。相反只要错误发生,lua必须提供处理错误的方式。

lua语言在遇到非预期情况时会引发错误,例如:将两个非数值类型的值相加,对不是函数的值进行调用,对不是表类型的值进行索引等等(当然我们后面会学习使用原表:metatable来改变上述行为)。我们也可以显示地调用函数error并传入一个错误信息作为参数来引发一个错误。通常,这个函数就是在代码中提示出错的合理方式

error("程序崩溃了")
--[[
C:lualua.exe: lua/5.lua:1: 程序崩溃了
stack traceback:
	[C]: in function ‘error‘
	lua/5.lua:1: in main chunk
	[C]: in ?
]]

由于针对某些情况调用函数error太常见了,所以lua还提供了内置函数assert来完成这类工作。assert不需要多说,会检测第一个参数是否为真,如果为真,那么正常通过;否则报错,同时会将第二个参数作为异常值。

assert总是在执行之前先对参数求值

assert(nil, "hello" .. " " .. "satori")
--[[
C:lualua.exe: lua/5.lua:1: hello satori
stack traceback:
	[C]: in function ‘assert‘
	lua/5.lua:1: in main chunk
	[C]: in ?
]]

这里是先将后面的字符串拼接起来之后,再执行assert,事实上,这算是废话,那个函数在执行前不是先确定参数的。

但是问题来了,我们的目的是希望在异常发生时,能够让其沉默,说人话就是异常捕获。

在lua中实现异常捕获的话,需要使用函数pcall,假设要执行一段lua代码并捕获里面出现的所有错误,那么需要首先将这段代码封装到一个函数中,然后通过pcall来调用这个函数。

function f()
    a = "xxx"
    return 1, 2, 3
end

print(pcall(f))  -- true	1	2	3
print(a)  -- xxx


-- pcall里面需要传入一个函数,然后pcall来调用
-- 如果正确执行,那么pcall的返回值为:true 加上 pcall里面的函数的返回值

-- 如果执行失败,那么返回值为:false 加上 错误信息
-- 也可以直接传入一个匿名函数
print(pcall(function() print("" > 1) end))  -- false	lua/5.lua:15: attempt to compare number with string


-- 如果报错,我们可以手动写入错误信息,通过函数error实现
status, err = pcall(function() error({code=666, err_msg="出错啦"}) end)
print(status, err.code, err.err_msg)  -- false	666	出错啦

lua中异常捕获显然没有传统的try catch方便,但是对于一个小巧的脚本语言也足够了。

错误信息和栈回溯

-- 我们看到pcall里面传入的是一个函数名
-- 这就要求函数不能有参数,或者说pcall在调用的时候不会传递参数
-- 那么函数参数就全部为nil,因此如果函数需要参数该怎么办?
function f(arg)
    if type(arg) ~= "string" then
        error("需要一个字符串")
    end
end

-- 答案是把调用放在一个函数里
pcall(function() f("xxx") end)


-- 另外,如果我们在这里传递的不是一个字符串的话
print(pcall(function()f(123) end))  -- false	lua/5.lua:6: 需要一个字符串
-- 告诉我们需要一个字符串,但是报错提示在第6行,也就是函数f中
-- 但这明显不是f的问题,而是我们在匿名函数中值传递错误了
function f(arg)
    if type(arg) ~= "string" then
        -- 可以指定一个层级,告诉error实际发生错误的地方是第2层
        -- 第一层为f本身
        error("需要一个字符串", 2)
    end
end
print(pcall(function()f(123) end))  -- false	lua/5.lua:25: 需要一个字符串
-- 我们看到报错信息变成了25行,也就是上面这一行

模块和包

我们目前所有的代码都写在一个文件里面,但是内容多了之后,肯定不能都写在一个文件里面,而是需要分文件了。这个时候就要设计到导入模块,在lua中导入模块通过require?"模块名"即可。

-- 我们看到模块在lua中是一个table类型
print(type(math))  -- table

-- 我们调用math.sin相当于从math这张表里面检索sin
-- 在lua5.3中,模块是内嵌在解释器里面的
-- 但是我们也可以按照导入模块的模式进行加载
m = require "math"
-- 我们看到得到的结果是一样
print(type(m), m.sin(m.pi / 2))  -- table	1.0

-- 模块不会重复导入,所以m和math是同一个表
print(m == math)  -- true

自己编写一个模块

-- m.lua

-- 创建一个模块,我们说模块本质上一张表
m = {}

function add(a, b)
    return a + b
end

-- 将add函数加入到模块m中
-- 当然也可以是m.xxx = add, 只不过我们后面在导入这个模块的时候就不能调用m.add了,而是m.xxx
m.add = add

-- 或者
function m.sub(a, b)
    return a - b
end

-- 对于变量也是可以ide
m.a = 123

b = 456
m.b = b


-- 最后将模块返回出去
return m

导入模块

-- 注意:如果直接去找的话,是找不到的,因为只会到默认的路径中去找
-- python的话会把当前模块所在的路径也算进去,但是lua不会
-- 我们需要使用package.path增加搜索路径, 多个搜索路径的话使用;分隔即可
package.path = "D:\\satori\\lua\\?.lua"
m = require "m"
print(m.add(1, 2))  -- 3
print(m.sub(1, 2))  -- -1
print(m.a)  -- 123
print(m.b)  -- 456

以上是关于lua语言:闭包模式匹配日期编译模块的主要内容,如果未能解决你的问题,请参考以下文章

十分钟学会Lua三:编程技巧之闭包与模式匹配

闭包的Lua中

[译] Lua中的闭包

详解 Scala 模式匹配

Unity游戏开发 | 浅谈Lua和C#中的闭包

Lua模式匹配