在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\n") -- 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中并没有正则表达式来进行模式匹配(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中有两个函数,用于操作日期和时间,我们来看一下。
-- 调用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:\lua\lua.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:\lua\lua.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:\lua\lua.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:\lua\lua.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语言(2):闭包、模式匹配、日期、编译、模块