zoukankan      html  css  js  c++  java
  • lua语言(2):闭包、模式匹配、日期、编译、模块

    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
    
  • 相关阅读:
    时间等待太重要!!!
    (引用 )自动化测试报告HTMLtestrunner
    (转载)selenium-webdriver(python)
    lr_convert_string_encoding()转码函数
    分步骤学习自动化测试
    (引用)web安全测试
    Monkey测试
    (学习网址)Python 自动化测试
    (引用) unittest测试驱动之执行测试(三)
    log4net日志组件
  • 原文地址:https://www.cnblogs.com/traditional/p/13192376.html
Copyright © 2011-2022 走看看