《lua程序设计第4版》学习笔记——进阶部分
名词解释
高阶函数:以另一个函数为参数的函数
第一类值:意味着lua语言中的函数和其他常见类型的值同等权限(比如保存到变量、放在表中)
闭包
递归函数定义问题
在编译函数体中的函数时,如果当前函数未定义,会去找全局函数。所以在定义递归函数时,要注意先定义
-- 错误的编写
local fact = function(n)
return n == 0 and 1 or fact(n - 1) -- 因为局部的fact还没定义,所以去找全局的fact
end
-- 正确的编写
local fact
fact = function(n)
return n == 0 and 1 or fact(n - 1)
end
词法定界
当编写一个被其他函数B包含的函数A时,被包含的函数A可以访问包含其的函数B的所有局部变量
function newCouner()
local count = 0
return function ()
count = count + 1
return count
end
end
上面的count是局部变量,理论上新的函数体是访问不到的,但是由于闭包的机制,函数可以逃逸变量的定界范围
模式匹配
函数
- string.find
- string.match
- string.gsub
- string.gmatch
模式
符号 | 含义 |
---|---|
. | 任意字符 |
%a | 字母 |
%c | 控制字符 |
%d | 数字 |
%g | 除空格外的可打印字符 |
%l | 小写字符 |
%p | 标点字符 |
%s | 空白字符 |
%u | 大写字母 |
%w | 字母和数字 |
%x | 十六进制数字 |
其他字符
符号 | 含义 |
---|---|
+ | 重复一次或多次 |
* | 重复零次或多次 |
- | 重复一次或多次(最小匹配) |
? | 可选(出现零次或一次) |
% | 转移字符,比如%% 表示% 这个字符 |
^ | 放在开头,表示某个字符集的补集。比如[^0-7] |
$ | 放在结尾,表示匹配到目标字符串的结尾 |
[ ] | 表示字符集 |
( ) | 表示捕获 |
捕获
把需要捕获的内容放到小括号中
pair = "name = anna"
k, v = string.match(pair, "(%a+)%s*=&s*(%a+)")
print(k, v) --> name anna
样例
-- 替换
string.gsub("hello Lua!", "%a", "%0-%0")
--查找
string.find("123123", "^[+-]?%d+$")
日期和时间
-- 当前时间戳
os.time()
t = os.date("*t")
-- 固定格式时间
os.date("%Y-%m-%d %H:%M:%S", os.time())
-- 生成指定时间
t = os.date("*t")
t.day = t.day + 40
os.time(t)
-- t: {year, month, day, hour, min, sec}
-- 计算时间差值
os.difftime(a, b)
-- 计算程序耗时(cpu时间)
local x = os.clock()
mainWork()
print(os.clock() - x)
编译、执行和错误
编译
- dofile: 从文件运行lua代码
- loadfile: 从文件读取并编译lua代码
- load: 字符串读取并编译lua代码
f = load("i = i + 1")
i = 0
assert(f()); print(i) --> 1
assert(f()); print(i) --> 2
预编译代码
$ luac -o prog.lc prog.lua
$ lua prog.lc
上面编译代码的函数,用.lc结尾的文件,也都是正确的
预编译的特点:
1、不一定比源代码小
2、但是加载更快
3、没办法修改源码,防止hack
错误处理和异常
pcall函数,相当于其他语言的try-catch
local ok, msg = pcall(
function ()
some code
if unexpected_condition then error() end
some code
print(a[i]) -- 潜在错误,a可能不是一个表
some code
end
)
if ok then
pass
else
pass
end
error函数,抛出一个错误。第二个参数level表示:在函数的调用层级中的哪层函数报告错误
function foo(str)
-- 第一层是foo函数自己,第二层是调用foo函数的地方
error(str, 2)
end
foo({11})
xpcall函数,类似pcall,但是他的第二个参数是一个消息处理函数,在这个函数里面可以去获取堆栈信息
xpcall(error code, function ()
debug.traceback()
-- debug.debug()
end)
模块和包
模块导入函数:require
local m = require('math')
加载顺序:
1、先在package.loaded
中检查是否被加载
1.1、表的形式是package.loaded.(modname)
,比如math模块,就是package.loaded.math
2、如果未加载,搜索对应的lua文件(搜索的路径由package.path
指定)
2.1、如果找到了,调用loadfile
3、如果没找到,则搜索C标准库(路径由package.cpath
指定)
3.1、如果找到了,调用loadlib
,这个函数会查找luaopen_(modname)
的函数
编写模块
local M = {}
function M.func()
end
return M
-- package.loader[...] = M
如果不想要最后的return,可以直接给package.loader赋值
子模块
函数require会将点转换为另一个字符,通常是操作系统的目录分隔符
比如调用require "a.b"
,会打开a/b.lua
文件
这个是编译时配置的
迭代器和泛型for
泛型for
语法:
for
var_list
inexp_list
dobody
end
- var_list:控制变量,一般不超过三个,因为for最多返回3个
- exp_list:表达式列表。只有最后一个能够返回多个值,而且只会保留3个值,多余的会舍弃,少的补nil
无状态迭代器
定义:自身不保存任何装填的迭代器,可以在多个循环中使用同一个无状态迭代器,从而避免创建新闭包的开销
比如:ipairs
ps:pairs和ipairs类似,但是pairs用的是基础函数next
元表和元函数
lua中无法对两个table进行操作(比如相加)。
因此lua提供了元表(Metatable),允许我们改变table的行为,每个行为关联了对应的元方法。
类似c++在类中重载operator+等操作符的操作
local mt = {}
mt.__add = function(a, b)
local res = {}
for _, v in pairs(a) do table.insert(res, v) end
for _, v in pairs(b) do table.insert(res, v) end
return res
end
s1 = {10, 20, 30, 40}
s2 = {30, 1}
setmetatable(s1, mt)
setmetatable(s2, mt)
s3 = s1 + s2
for k, v in pairs(s3) do
print(k .. " : " .. v)
end
--[[ 结果:
1 : 10
2 : 20
3 : 30
4 : 40
5 : 30
6 : 1
]]--
元方法
元方法 | 对应操作符 |
---|---|
__add | + |
__sub | - |
__mul | * |
__div | / |
__idiv | |
__mod | % |
__unm | 负数:- |
__concat | 连接运算符:. |
__eq | == |
__lt | < |
__le | <= |
__pow | 幂运算 |
__band | 按位与 |
__bor | 按位或 |
__bxor | 按位异或 |
__bnot | 按位取反 |
__shl | 左移 |
__shr | 右移 |
__tostring | 重点,重载元表的输出 |
__pairs | lua 5.2以上,对应函数pairs |
__index | 查找字段:[] |
__newindex | 对一个表中不存在的索引赋值 |
tostring本质是先检查元方法__tostring,如果有就调用这个
面向对象
成员函数调用
声明和调用的时候,如果用点,第一个参数需要是self
如果用冒号,可以隐藏
function Account.work(self, a) do end
function Account:work(a) do end
冒号的作用就是在一个方法调用中增加一个额外的实参
多重继承
用元方法查找实现
function createClass(...)
local c = {}
local parents = {...} -- 父类列表
setmetatable(c, { __index = function(t, k)
for i = 1, #parents do
local v = parent[i][k]
if v then return v end
end
end})
-- 将c作为其实例的元表
c.__index = c
end
环境
全局变量:_G
lua中的全局变量是存在_G表中的,而且全局变量不需要声明就可以使用,如果手滑打错字很难发现bug,所以可以通过搜索这张表来判断全局变量是否存在
if rawget(_G, var) == nil then end
_ENV
当前环境表,出现调用未声明的变量会优先在这个表里的找。也可以对这个表赋值,从而做些骚操作
local M = {}
_ENV = M
function add(a, b)
return new(a, b)
end
上面这个例子中,调用add,等同于调用M.new
_G 和 _ENV 的关系
通常情况下,_G 和 _ENV 指向的是同一个表,但它们是两个不同的实体。 _ENV 是一个局部变量,所有对“全局变量”的访问实际上都是访问 _ENV 。 _G则是一个在任何情况下都没有任何特殊状态的全局变量。按照定义, _ENV永远指向的是当前的环境 _G在没有手动改变其值的情况下指向的是全局环境。
垃圾收集(GC)
GC流程
标记 -> 清理 -> 清除 -> 析构
1、从根节点遍历所有对象,遍历到的标记为活跃
2、清理阶段,先处理析构器和弱引用表,然后把需要析构的函数放在单独的列表里(复苏的对象就是在这个过程被放在一个单独的列表中的)
3、清除阶段,没有活跃的对象,全部回收
4、调用清理阶段被分离出来对象的析构器
Lua 5.1 使用了增量式垃圾收集器,不必每次停机清理,到达上限时,只会执行一小步
Lua 5.2 引入了紧急垃圾收集,当内存分配失败时,Lua语言会强制执行一次完整的垃圾收集,然后再次尝试分配
弱引用
由__mode字段决定,"k"表示键是弱引用的,"v"表示值是弱引用的
a = {}
mt = {__mode = "k"}
setmetatable(a, mt)
key = {}
a[key] = 1
key = {}
a[key] = 2
collectgarbage() -- 强制垃圾回收
for k, v in pairs(a) do print(v) end --> 2
如上,因为第一个键已经被回收了,所以表中也没有对应的项了
析构器
由__gc方法实现,当该对象被回收时会被调用
o = {x = "hi"}
setmetatable(o, {__gc = function(o) print(o.x) end})
o = nil
collectgarbage() --> hi
复苏
如果在a的析构函数中,访问了b的话,那这个b会被加入到全局变量中,导致b在本次垃圾回收中,无法被回收,在调用第二次gc时,才会回收b,这种情况叫做复苏,编程时需要注意。
协程
所在函数:表coroutine
lua语言提供的是非对称协程,用两个函数控制协程,一个是挂起,一个是恢复
对称协程是只用一个函数,来切换两个协程之间的控制权
使用方法
- coroutine.create(func): 创建协程
- coroutine.status(co): 查看协程状态:挂起、运行、正常和死亡
- coroutine.resume(co): 恢复协程
- coroutine.yield(): 挂起协程
反射
反射是程序用来检查和修改其自身某些部分的能力
虽然lua有部分反射机制,但是还是缺失一些内容,这些内容被调试库
所填补
自省机制
debug.getinfo(foo),可以显示foo函数的各个信息:
- source:函数定义的位置
- short_src:source的精简版本
- linedefined:源代码第一行行号
- lastlinedefined:最后一行行号
- what:说明函数的类型:"Lua"表示lua函数,"C"表示c函数,"main"表示lua语言代码段的主要部分
- name:该函数的名称
- namewhat:说明上一个字段的含义,比如gloal、local等
- nups:该函数上的值的个数
- nparms:参数个数
- isvararg:是否为可变长参数函数,return bool
- activelines:该函数所有活跃行的集合
- func:函数本身
- currentline:查询的是活跃函数才有的,当前执行的代码行
- istailcall:查询的是活跃函数才有的,表示函数是否是被尾调用所调用的
第二个参数,为了更好的性能,而选择自己想要的信息返回:
- n:选择6、7
- f:选择12
- S:选择1、2、3、4、5
- l:选择13
- L:选择11
- u:选择8、9、10
访问变量
访问局部变量:debug.getlocal(函数的栈层次, 变量索引)
访问非局部变量:debug.getupvalue(闭包函数, 变量索引)
这两个函数都有对应的set函数
钩子
在特定的时间发生时,调用注册的钩子函数
function trace(event, line)
local s = debug.getinfo(2).short_src
print(s .. ":" .. line)
end
debug.sethook(trace, "l")
local a = 1 -- 会输出文件名+行号
sethook的第二个参数表示监控的事件:
- c:调用函数的call事件
- r:函数返回的return事件
- l:执行一行新代码的line事件
- 一个计数器:执行完指定数量的指令后产生的count事件
- 不加第二参数:关闭钩子
钩子和debug.debug函数是一个不错的搭配
通过设置call事件的钩子,我们可以监控函数的调用:debug.sethook(hook, "c")
比如可以监听函数调用的次数,控制函数的调用次数(创造沙盒防止dos),控制授权函数的运行等
其他
暂无