这篇文章记录一下阅读 Programming in Lua 时的一些注意点,以及一些重要的模块。
ps:页数指的是书中的页数,不是 PDF 目录的页数;
GitHub - 0kk470/pil4: Lua程序设计第四版习题答案 (Programming in Lua 4th Edition Exercise Solutions)
第 3 章 表达式 P14 注意一下:
and 和 or 的运算结果不是 true 和 false,而是和它的两个操作数相关。
P16 首先我们看一下下面这个例子 :
1 2 3 4 5 6 7 8 9 10 11 12 13 polyline = { color="blue" , thickness=2 , npoints=4 , {x=0 , y=0 }, {x=-10 , y=0 }, {x=-10 , y=1 }, {x=0 , y=1 }, 5 , 6 , "a" , "b" , } print (#polyline)
可以发现:
table 中的域(color,thickness,npoints)不会被算入table大小中,所以如果我们直接for i=1,#polyline 去遍历table,无法输出所有的内容。
但是我们可以通过泛型for去遍历,for name in pair(polyline ) do … end
Lua 打印table表内容_lua打印表-CSDN博客
注意语法:
1 2 3 4 5 6 7 8 s = "-" polyline = { "+" = "-" , ["+" ] = "-" , s = "+" , }
第 4 章 基本语法 P21 p22 (这里面讲到了,尽量不要改变控制变量的值…)
第 5 章 函数 p27 unpack()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 polyline = { color="blue" , thickness=2 , npoints=4 , {x=0 , y=0 }, {x=-10 , y=0 }, {x=-10 , y=1 }, {x=0 , y=1 }, 5 , 6 , "a" , "b" , } print (table .unpack (polyline))
可变参数
第 6 章 再论函数 p32 闭包(这个很重要呀)
可以用于持久化数据,保存一些状态
可以用于重写函数,保证安全性
p37 尾调用:
因为尾调用不需要额外的栈空间,所以可以用来优化;
注意分辨哪些是真正的尾调用
第 7 章 迭代器与泛型 for p40
图片中的程序怎么理解 ?
对于上面的解释,应该是第一次调用的时候,返回的是函数(还未调用),此时i=0(闭包 iter),然后第二次调用闭包 iter() 的时候i才会自增
同理,在这里的for in ipairs 中,ipairs()表达式是先计算好的,返回了三个值(闭包);然后for语句通过in不断去调用这个闭包,达到遍历的效果,而且是无状态的(不用先创建闭包出来)
第7章需要细品,其实是闭包的应用之迭代器
第 8 章 编译·运行·调试 pcall,xpcall
第 9 章 协同程序 重要的
第 12 章 数据文件与持久化 Lua string.format用法-CSDN博客
p89 12.1.2 保存带有循环的table,这一块比较复杂一点;
其实就是 记忆化搜索 的过程。
1 2 3 4 Metatables 允许我们改变 table 的行为,例如,使用 Metatables 我们可以定义 Lua 如 何计算两个 table 的相加操作 a+b。当 Lua 试图对两个表进行相加时,他会检查两个表是 否有一个表有 Metatable,并且检查 Metatable 是否有__add 域。如果找到则调用这个__add 函数(所谓的 Metamethod)去计算结果。
p92 下面测试一下 算术运算的Metatables
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 Set1 = {} Set1.mt = {} function Set1.new (t) local set = {} setmetatable (set, Set1.mt) for _, l in ipairs (t) do set[l] = true end return set end function Set1.union (a,b) local res = Set1.new{} for k in pairs (a) do res[k] = true end for k in pairs (b) do res[k] = true end return res end Set1.mt.__add = Set1.union Set2 = {} Set2.mt = {} function Set2.new (t) local set = {} setmetatable (set, Set2.mt) for _, l in ipairs (t) do set[l] = true end return set end function Set2.union (a,b) local res = Set2.new{} for k in pairs (a) do res[k] = b[k] end return res end Set2.mt.__add = Set2.union function fun_tostring (set) local s = "{" local sep = "" for e in pairs (set) do s = s .. sep .. e sep = ", " end return s .. "}" end function fun_print (s) print (fun_tostring(s)) end s1 = Set1.new{1 ,2 ,3 } s2 = Set1.new{4 ,5 ,6 } s3 = s1 + s2 s4 = s2 + s1 fun_print(s3) fun_print(s4) s1 = Set1.new{1 ,2 ,3 } s2 = Set2.new{4 ,5 ,6 } s3 = s1 + s2 s4 = s2 + s1 fun_print(s3) fun_print(s4)
总结:
首先 Metatable 可以理解为一个专门重载表的运算符的集合;
所有关于两个表的操作,都要定义在这个 Metatable 里面,定义的具体运算函数为 Metamethods;
规则:(针对 同一种 / 非同一种 表都适用)
第一个 table 的 Metatable 有定义对应的 Metamethods,则用第一个表的;
如果第一个表没定义,看第二个表是否有定义,有就用第二个的;
都没定义,则会报错
关于 关系运算的Metamethods :
两个 table 的 Metamethods 必须相同才能进行运算;
p97 表相关的 Metamethods
这一部分很妙啊!值得反复阅读;
主要是搞清楚:
__index metamethod
可以理解为访问 ,当访问表中不存在的域时,会去找 __index metamethod
__newindex metamethod
可以理解为更新 ,当对表中不存在的域进行赋值时,会去找 __newindex metamethod
第 14 章 环境 这一章我们将讨论一些如何操纵环境的有用的技术。
第 15 章 Packages p111 私有成员
这一块好多技巧…
学了后面忘了前面… 复习一下;
Lua中setfenv()_lua setfenv-CSDN博客
第 16 章 面向对象程序设计 p119 这里讲到了 self 的调用;
p119 - 121(16.1 类) lua 没有类的概念,更像是一种原型模式;
1 2 3 4 5 在这些语言中,对象没有类。相反,每个对象都有一个 prototype (原型),当调用不属于对象的某些操作时,会最先会到 prototype 中查找这些操作。在 这类语言中实现类(class)的机制,我们创建一个对象,作为其它对象的原型即可(原 型对象为类,其它对象为类的 instance)。类与 prototype 的工作机制相同,都是定义了特 定对象的行为。
这里讲到了用 metatable 的操作去做一种 类和对象的关系;
但是实际上感觉讲的不准确… 实际上就是没有类… 就都是对象;
比如:setmetatable(a, {__index = b})
我个人的理解是 b是父对象,a是子对象;
当a找不到属性/方法的时候会去找b的;
这一小节后面的例子有点绕… 需要细品;
p121 - 122(16.2 继承) 这里讲一下为啥能够形成 (祖父-父-子) 的继承链关系:
祖父:Account
父:SpecialAccount
子:s
其实我们把冒号写成点的形式就一目了然了(这里只讲new)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 主要看下面这一段: Account = {balance = 0 } function Account:new (o) o = o or {} setmetatable (o, self ) self .__index = self return o end 这里我们把它用dot的形式写出来(也就是上一小节最后的样子): function Account.new (Account, o) o = o or {} setmetatable (o, self ) self .__index = self return o end 这里我 self 先不变,但是要知道,函数中的 self 就是函数的第一个参数! 首先第一次继承: SpecialAccount = Account:new() 也就是调用了一次:SpecialAccount = Account.new (Account, nil ) 可以看到此时new函数中的self 是Account; 于此同时 o={},然后 o 的 metatable 为self (也就是Account),然后o返回出去给SpecialAccount(o <=> SpecialAccount); 然后第二次继承:(关键的来了) s = SpecialAccount:new{limit=1000.00 } 这里也就是调用了一次:s = SpecialAccount.new(SpecialAccount,{limit=1000.00 }); 但是但是,SpecialAccount是没有new函数的,所以去SpecialAccount的metatable里面找,找到Account的函数并调用:Account.new(SpecialAccount,{limit=1000.00 }); 可以看到,这里的self 是SpecialAccount,而不是Account!!! 此时我们看看new函数里面做了啥:它把 {limit=1000.00 } 的 metatable 设置为 self (也就是SpecialAccount)!然后返回给s; 也就是说 s 的 metatable 其实是 SpecialAccount,而 SpecialAccount 的 metatable 是 Account; 进而形成了继承链! 一开始看的时候也很懵...
第 17 章 Weak 表
只有对象才会被回收
可以通过weak表来进行垃圾回
通过 metatable 的 __mode 域来表示 weak 表
优化记忆函数:设置 记忆表 的 metatable 为 weak 表(value 为 weak,这样能节省空间)
最后的两个例子好繁琐…
大致的思想就是通过 weak 表来优化默认值的存储,允许被回收
第 18 章 数学库
y = math.rad(x)
x = math.deg(y)
math.random
math.randomseed(os.time())
第 19 章 table 库 p136(数组大小)
table.getn() 在 lua 5.0 以上已经被废弃了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 table .getn () 等价于 #tt = {n = 10 } print (#t) local t = {nil }print (#t) t = {1 , nil } print (#t) t = {nil ,1 } print (#t) t = {nil ,nil ,1 } print (#t) t = {1 ,nil ,2 } print (#t) t = {1 ,nil ,2 ,nil } print (#t) t = {1 ,nil ,2 ,nil ,3 } print (#t) t = {1 ,nil ,2 ,nil ,3 ,nil } print (#t)
是不是懵逼了… 我也没搞懂… 具体看这:
lua 中求 table 长度 | 菜鸟教程 (runoob.com)
另外 table.setn() 好像也没了… 直接没有这个方法了
p137 插入删除 1 2 3 4 5 6 7 8 9 10 11 12 13 t = {1 ,2 ,3 } table .insert (t,4 ); print (unpack (t)) table .insert (t,2 ,5 ); print (unpack (t)) local v = table .remove (t) print (unpack (t)) v = table .remove (t,1 ) print (unpack (t))
排序讲的很清楚了,注意到最后一个例子又用到了闭包(迭代器)
第 20 章 String 库 函数
string.len(s)
string.rep(s,n)
string.lower(s)
string.upper(s)
string.sub(s,i,j)
截取字符串,可以是负数,表示倒着数的第几个;
lua 中的字符串不会改变,所有操作之后是返回一个新的字符串;
string.char(97,98,99)
string.byte(s,i)
将字符串s的第i个字符转化为数字,i默认为1,i可以为负数
string.format()
格式化字符串,和printf一样…
%d 十进制;%x 十六进制;%o 八进制
st,en = string.find(s, p, i)
字符串查找(基于模式匹配,不是posix规范的正则表达式)
从 s 的 i 位置开始,在 s 中找 p ,找到返回 开始st和结束下标en,找不到返回 nil;
s,count = string.gsub(s, p1, p2,times)
全局字符串替换(基于模式匹配,不是posix规范的正则表达式)
从 s 中 找 p1,并替换成 p2(全部替换)
times 是限制要替换几次;
count 是一共替换了几次;
string.gfind()
全局字符串查找(基于模式匹配,不是posix规范的正则表达式)
模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 下面的表列出了 Lua 支持的所有字符类: . 任意字符 %a 字母 %c 控制字符 %d 数字 %l 小写字母 %p 标点字符 %s 空白符 %u 大写字母 %w 字母和数字 %x 十六进制数字 %z 代表 0 的字符 上面字符类的大写形式表示小写所代表的集合的补集。例如,'%A'非字母的字符; print(string.gsub("hello, up-down!", "%A", ".")) --> hello..up.down. 4 4 是替换的次数
1 2 3 4 5 6 Lua 中的特殊字符如下: ( ) . % + - * ? [ ^ $ 特殊字符用 % 转义 其他的用 \ 转义
其他的好复杂(主要是我不会正则表达式…我是fw)
捕获、转换的技巧 也看不懂…
第 21 章 IO库 p157 简单IO模式
io.input(filename)
io.output()
io.read()
从当前输入文件读取字符串
“*all”:读取整个文件
“*line” :读取下一行
“*number” :从串中转换出一个数值
num :读取 num 个字符到串
io.lines()
p160 完全IO模式
通过文件句柄
f = io.open()
f:read()
预定义的流句柄:
io.stdin
io.stdout
io.stderr
IO优化
*local lines, rest = f:read(BUFSIZE, “line”)
读取整个文件时,避免对行进行分割;
代码中的 rest 就保存了任何可能被段划分切断的行。
具体使用看书
二进制文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 local inp = assert (io .open (arg [1 ], "rb" )) local out = assert (io .open (arg [2 ], "wb" )) local data = inp:read ("*all" ) data = string .gsub (data, "\r\n" , "\n" ) out:write (data) assert (out:close ())> lua prog.lua file.dos file.unix' 这样运行时,相当于是吧file.dos file.unix 当作参数传进去了; arg[1] = file.dos arg[2] = file.unix
其他文件操作(知道一下,用到时具体查)
tmpfile
flush
filehandle:seek(whence,offset)
第 22 章 操作系统库
os.time()
单位:秒
返回当前时钟的数值
可以带参数,表示距离参数设定的日期的时间(有默认值,可以看一下书)
os.date( “ ”,seconds)
os.clock()
返回执行该程序 CPU 花去的时钟秒数。
使用:
1 2 3 4 5 local x = os .clock ()做一些操作... print (string .format ("elapsed time: %.2f\n" , os .clock () - x))
os.exit()
os.getnv(“”)
得到“环境变量”的值。以“变量名”作为参数,返回该变量值的字符串:
os.execute( “ “ )
执行一个系统命令(和 C 中的 system
函数等价)
os.setlocale()
设定 Lua程序所使用的区域(locale)。
这个比较高级…
第 23 章 Debug库 自省
debug.getinfo()
第一个参数:数字或者函数;
函数 :返回关于这个函数信息的一个表。
数字 :返回在 n 级栈的活动函数的信息数据。
如果 n=1,返回的是正在进行调用的那个函数的信息。(n=0 表示 C 函数 getinfo 本身)
第二个参数:可以用来指定选取哪些信息。
1 2 3 4 5 'n' selects fields name and namewhat 'f' selects field func 'S' selects fields source, short_src, what, and linedefined 'l' selects field currentline 'u' selects field nup
具体例子可以看书
debug.getlocal(level, index)
访问任何活动状态的局部变量。
参数level:将要查询的函数的栈级别;
参数index:变量的索引。
函数有两个返回值:变量名和变量当前
值。
书中有例子和解释;
debug.getupvalue(func, index)
访问 Lua 函数的 upvalues。和局部变量
不同的是,即使函数不在活动状态他依然有 upvalues(这也就是闭包的意义所在)。所以,
getupvalue 的第一个参数不是栈级别而是一个函数(精确的说应该是一个闭包),第二个
参数是 upvalue 的索引。
Lua的upvalue和闭包_lua upvalue-CSDN博客
Hooks
profiler
第 24 章 C API 纵览
这一部分主要是 lua 和 c 的交互吧
C 和 Lua 之间来能个不协调的问题:
Lua 自动进行垃圾回收,C需要显示的分配存储单元;
Lua 中的动态类型和C中的静态类型不一致;
24.1 简介
lua_open():创建一个新环境(lua_state)
所有的标准库以单独的包提供,所以如果你不需要就不会强求你使用它们。
可以用 luaopen_io() 注册io库等等;
24.2 堆栈
第 25 章 扩展你的程序 p189 用 Lua 做配置文件,书里有个例子,讲的比较清除;
luaL_loadfile(L, filename)
lua_pcall
lua_getglobal(L , “globalname”)
25.1 表操作 书里讲的不是特别清除… 看这里:
lua_gettable 函数详解_lua_getglobal-CSDN博客
这个讲的很清楚;
关键是要理解 lua_gettable:
先把key值压入栈顶
调用 lua_gettable(L, index) 时,会弹出栈顶的key值,然后找到lua栈中对应的index的table,然后再取到 table 的 key 值(table[key]),然后将这个值再次压入栈顶;
lua_gettable和lua_settable - 乐swap火 - 博客园 (cnblogs.com)
lua_settable 可以看这里,也是比较好理解的;
修改表中对应key值的value
先把key入栈,再把value入栈,然后调用 lua_settable(L, index);
此时会把 key 和 value 都弹出栈,然后将lua栈中的对应的index 的table 的 key 修改为value
25.2 调用 lua 函数
将被调用的函数入栈(可以用lua_getglobal,将要执行的函数入栈)
以此将所有参数入栈
使用 lua_pcall 调用函数;
这一小节的最后有详细介绍了lua_pcall 函数;
参数:
lua 栈
参数个数
返回值个数
错误处理函数(0表示没有,如果有,需要在函数和参数入栈之前入栈)
最后从栈中获取函数执行的返回结果
第 26 章 调用 C 函数
将 C 函数注册到 Lua 中去;
用来交互的栈不是全局变量,每一个函数都有他自己的私有栈。
函数在将返回值入栈之前不需要清理栈,因为C函数会返回一个(返回值个数)
C函数返回之后,Lua 自动的清除栈中返回结果下面的所有内容。
呃,注册函数这一块有点小复杂啊;
第 27 章 撰写 C 函数的技巧 在这一章我们将讨论数组操纵、string 处理、在 C 中存储 Lua 值等一些特殊的机制。
27.1 数组操作 1 2 3 4 5 6 7 8 9 void lua_rawgeti (lua_State *L, int index, int key); void lua_rawseti (lua_State *L, int index, int key); index 指向 table 在栈中的位置; key 指向元素在 table 中的位置。
27.2 字符串处理
C函数 接受 lua 的字符串作为参数时的原则:
当字符串正在被访问的时候不要将其出栈;
永远不要修改字符串;
lua_pushlstring(L, start , len) *
从start开始截取长度为len的字符串,并压入栈顶;
start*时一个指针,比如 s+i,s=char*
lua_concat(L,n)
将连接(同时会出栈)栈顶的 n 个值,并
将最终结果(字符串)放到栈顶。
提高连接大量字符串的效率:使用辅助库提供的 buffer 相关函数;
1 2 3 4 5 6 7 8 9 10 11 12 13 void luaL_buffinit (lua_State *L, luaL_Buffer *B); void luaL_putchar (luaL_Buffer *B, char c); void luaL_addlstring (luaL_Buffer *B, const char *s, size_t l); void luaL_addstring (luaL_Buffer *B, const char *s); void luaL_pushresult (luaL_Buffer *B); void luaL_addvalue (luaL_Buffer *B);
27.3 在 C 函数中保存状态 27.3.1 The Registry
Lua 提供了一个独立的被称为 registry 的表,C代码可以自由使用,但 Lua 代码不能访问他。
由于所有的 C 库共享相同的 registry ,你必须注意使用什么样的值作为 key,否则会导致命名冲突。一个防止命名冲突的方法是使用 static 变量的地址作为 key:C 链接器保证在所有的库中这个 key 是唯一的。
书中有使用例子;
27.3.2 References
27.3.3 Upvalue
第 28 章 User-Defined Types in C 28.1 Userdata 我们首先关心的是如何在 Lua 中表示数组的值。Lua 为这种情况提供专门提供一个基本的类型:userdata 。
一个 userdatum 提供了一个在 Lua 中没有预定义操作的 raw 内存区域。
Lua API 提供了下面的函数用来创建一个 userdatum:
**void lua_newuserdata (lua_State L, size_t size);
lua_newuserdata 函数按照指定的大小分配一块内存,将对应的 userdatum 放到栈内,并返回内存块的地址。
下面看一下书中的例子:
在 C 中定义数据类型(数组),然后给 lua 去使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 typedef struct NumArray { int size; double values[1 ]; } NumArray; static int newarray (lua_State *L) { int n = luaL_checkint (L, 1 ); size_t nbytes = sizeof (NumArray) + (n - 1 )*sizeof (double ); NumArray *a = (NumArray *)lua_newuserdata (L, nbytes); a->size = n; return 1 ; } static int setarray (lua_State *L) { NumArray *a = (NumArray *)lua_touserdata (L, 1 ); int index = luaL_checkint (L, 2 ); double value = luaL_checknumber (L, 3 ); luaL_argcheck (L, a != NULL , 1 , "`array' expected" ); luaL_argcheck (L, 1 <= index && index <= a->size, 2 , "index out of range" ); a->values[index-1 ] = value; return 0 ; } static int getarray (lua_State *L) { NumArray *a = (NumArray *)lua_touserdata (L, 1 ); int index = luaL_checkint (L, 2 ); luaL_argcheck (L, a != NULL , 1 , "'array' expected" ); luaL_argcheck (L, 1 <= index && index <= a->size, 2 , "index out of range" ); lua_pushnumber (L, a->values[index-1 ]); return 1 ; } static int getsize (lua_State *L) { NumArray *a = (NumArray *)lua_touserdata (L, 1 ); luaL_argcheck (L, a != NULL , 1 , "`array' expected" ); lua_pushnumber (L, a->size); return 1 ; } static const struct luaL_reg arraylib [] = { {"new" , newarray}, {"set" , setarray}, {"get" , getarray}, {"size" , getsize}, {NULL , NULL } }; int luaopen_array (lua_State *L) { luaL_openlib (L, "array" , arraylib, 0 ); return 1 ; }
1 2 3 4 5 6 7 8 a = array.new(1000 ) print (a) print (array.size(a)) for i=1 ,1000 do array.set(a, i, 1 /i) end print (array.get(a, 10 ))
细节… 复杂起来了;
为数组创建了一个metatable(记住userdata 也可以拥有 metatables)。下面,我们每次创建一个新的数组的时候,我们将这个单独 metatable 标记为数组的 metatable。每次我们访问数组的时候,我们都要检查他是否有一个正确的 metatable。
因为 Lua 代码不能改变 userdatum 的 metatable,所以他不会伪造我们的代码。
具体例子可以看一下书;
相当于是通过 metatable 给数组打了一个标记,区分了 数组 和 其他的 userdata;
然后在调用的时候检查一下 userdata 是否是数组;
28.3 访问面向对象的数据 复杂起来了…
这一小节讲的是如何通过面向对象的方式去访问数据;
1 2 3 4 5 a = array.new(1000 ) array.size(a) => a:size()
这儿的关键在于__index 元方法(metamethod)的使用。对于表来说,不管什么时候只要找不到给定的 key,这个元方法就会被调用。
28.5 Light Userdata 《Lua5.4 源码剖析——基本数据类型 之 UserData》 - 知乎 (zhihu.com)
Light userdata 真正的用处在于可以表示不同类型的对象。当 full userdata 是一个对象的时候,它等于对象自身;另一方面,light userdata 表示的是一个指向对象的指针,同样的,它等于指针指向的任何类型的 userdata。所以,我们在 Lua 中使用 light userdata表示 C 对象。
Lua 垃圾回收 Lua的垃圾回收可以参考下面这几篇文章,讲的比较详细:
Lua的垃圾回收机制详解_lua垃圾回收机制-CSDN博客
【Lua学习笔记】Lua进阶——垃圾回收_lua 垃圾回收-CSDN博客 (这个里面的图比较好)
Lua GC机制原理、过程、源码详解-CSDN博客 (这个里面文字阐述的比较好,有流程图和源码分析)
Lua 垃圾回收 | 菜鸟教程 (runoob.com)
Lua 实现了一个增量标记-扫描 (Mark and Sweep )收集器。 它使用这两个数字来控制垃圾收集循环:
垃圾收集器间歇率
控制着收集器需要在开启新的循环前要等待多久。
增大这个值会减少收集器的积极性。
垃圾收集器步进倍率
控制着收集器运作速度相对于内存分配速度的倍率。
增大这个值不仅会让收集器更加积极,还会增加每个增量步骤的长度。