《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 in exp_list do body 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函数的各个信息:

  1. source:函数定义的位置
  2. short_src:source的精简版本
  3. linedefined:源代码第一行行号
  4. lastlinedefined:最后一行行号
  5. what:说明函数的类型:"Lua"表示lua函数,"C"表示c函数,"main"表示lua语言代码段的主要部分
  6. name:该函数的名称
  7. namewhat:说明上一个字段的含义,比如gloal、local等
  8. nups:该函数上的值的个数
  9. nparms:参数个数
  10. isvararg:是否为可变长参数函数,return bool
  11. activelines:该函数所有活跃行的集合
  12. func:函数本身
  13. currentline:查询的是活跃函数才有的,当前执行的代码行
  14. 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),控制授权函数的运行等

其他

暂无

posted @ 2022-07-11 09:14  二律背反GG  阅读(160)  评论(0编辑  收藏  举报