这篇文章记录一下阅读 Programming in Lua 时的一些注意点,以及一些重要的模块。
ps:页数指的是书中的页数,不是 PDF 目录的页数;
GitHub - 0kk470/pil4: Lua程序设计第四版习题答案 (Programming in Lua 4th Edition Exercise Solutions)
第 3 章 表达式
P14
注意一下:
and 和 or 的运算结果不是 true 和 false,而是和它的两个操作数相关。
P16
首先我们看一下下面这个例子:
1 | polyline = |
可以发现:
table 中的域(color,thickness,npoints)不会被算入table大小中,所以如果我们直接for i=1,#polyline 去遍历table,无法输出所有的内容。
但是我们可以通过泛型for去遍历,for name in pair(polyline ) do … end
注意语法:
1 | s = "-" |
第 4 章 基本语法
P21
p22
(这里面讲到了,尽量不要改变控制变量的值…)
第 5 章 函数
p27
unpack()
1 | polyline = |
可变参数
第 6 章 再论函数
p32
闭包(这个很重要呀)
- 可以用于持久化数据,保存一些状态
- 可以用于重写函数,保证安全性
p37
尾调用:
因为尾调用不需要额外的栈空间,所以可以用来优化;
注意分辨哪些是真正的尾调用
第 7 章 迭代器与泛型 for
p40
图片中的程序怎么理解?
对于上面的解释,应该是第一次调用的时候,返回的是函数(还未调用),此时i=0(闭包 iter),然后第二次调用闭包 iter() 的时候i才会自增
同理,在这里的for in ipairs 中,ipairs()表达式是先计算好的,返回了三个值(闭包);然后for语句通过in不断去调用这个闭包,达到遍历的效果,而且是无状态的(不用先创建闭包出来)
第7章需要细品,其实是闭包的应用之迭代器
第 8 章 编译·运行·调试
pcall,xpcall
第 9 章 协同程序
重要的
第 12 章 数据文件与持久化
p89
12.1.2 保存带有循环的table,这一块比较复杂一点;
其实就是 记忆化搜索 的过程。
第 13 章 Metatables and Metamethods
1 | Metatables 允许我们改变 table 的行为,例如,使用 Metatables 我们可以定义 Lua 如 |
p92
下面测试一下 算术运算的Metatables
1 |
|
总结:
首先 Metatable 可以理解为一个专门重载表的运算符的集合;
所有关于两个表的操作,都要定义在这个 Metatable 里面,定义的具体运算函数为 Metamethods;
规则:(针对 同一种 / 非同一种 表都适用)
- 第一个 table 的 Metatable 有定义对应的 Metamethods,则用第一个表的;
- 如果第一个表没定义,看第二个表是否有定义,有就用第二个的;
- 都没定义,则会报错
关于 关系运算的Metamethods:
两个 table 的 Metamethods 必须相同才能进行运算;
p97
表相关的 Metamethods
这一部分很妙啊!值得反复阅读;
主要是搞清楚:
- __index metamethod
- 可以理解为访问,当访问表中不存在的域时,会去找 __index metamethod
- __newindex metamethod
- 可以理解为更新,当对表中不存在的域进行赋值时,会去找 __newindex metamethod
第 14 章 环境
这一章我们将讨论一些如何操纵环境的有用的技术。
第 15 章 Packages
p111
私有成员
将 Packages 中的函数声明为 local 的(私有)
改进:所有的函数都声明为local的,在 Packages 的最后定义一下公有函数就行。
这一块好多技巧…
学了后面忘了前面… 复习一下;
Lua中setfenv()_lua setfenv-CSDN博客
第 16 章 面向对象程序设计
p119
这里讲到了 self 的调用;
p119 - 121(16.1 类)
lua 没有类的概念,更像是一种原型模式;
1 | 在这些语言中,对象没有类。相反,每个对象都有一个 prototype |
这里讲到了用 metatable 的操作去做一种 类和对象的关系;
但是实际上感觉讲的不准确… 实际上就是没有类… 就都是对象;
比如:setmetatable(a, {__index = b})
我个人的理解是 b是父对象,a是子对象;
当a找不到属性/方法的时候会去找b的;
这一小节后面的例子有点绕… 需要细品;
p121 - 122(16.2 继承)
这里讲一下为啥能够形成 (祖父-父-子) 的继承链关系:
- 祖父:Account
- 父:SpecialAccount
- 子:s
其实我们把冒号写成点的形式就一目了然了(这里只讲new)
1 | 主要看下面这一段: |
第 17 章 Weak 表
只有对象才会被回收
可以通过weak表来进行垃圾回
通过 metatable 的 __mode 域来表示 weak 表
优化记忆函数:设置 记忆表 的 metatable 为 weak 表(value 为 weak,这样能节省空间)
最后的两个例子好繁琐…
大致的思想就是通过 weak 表来优化默认值的存储,允许被回收
第 18 章 数学库
- y = math.rad(x)
- 将 角度 x 转化为 弧度 y
- x = math.deg(y)
- 将 弧度 y 转换为 角度 x
- math.random
- 生成随机数(可以带 0/1/2 个参数)
- math.randomseed(os.time())
- 设置随机种子(通过系统时间)
第 19 章 table 库
p136(数组大小)
- table.getn() 在 lua 5.0 以上已经被废弃了
1 | table.getn() 等价于 #t |
是不是懵逼了… 我也没搞懂… 具体看这:
lua 中求 table 长度 | 菜鸟教程 (runoob.com)
- 另外 table.setn() 好像也没了… 直接没有这个方法了
p137 插入删除
1 | t = {1,2,3} |
- 排序讲的很清楚了,注意到最后一个例子又用到了闭包(迭代器)
第 20 章 String 库
函数
- string.len(s)
- 返回字符串长度
- string.rep(s,n)
- 返回重复了n次s的字符串
- string.lower(s)
- 将 s 中的大写字母转换成小写
- string.upper(s)
- 将小写转换成大写
- string.sub(s,i,j)
- 截取字符串,可以是负数,表示倒着数的第几个;
- lua 中的字符串不会改变,所有操作之后是返回一个新的字符串;
- string.char(97,98,99)
- 将数字num转化为字符
- 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 | 下面的表列出了 Lua 支持的所有字符类: |
1 | Lua 中的特殊字符如下: |
其他的好复杂(主要是我不会正则表达式…我是fw)
捕获、转换的技巧 也看不懂…
第 21 章 IO库
p157 简单IO模式
- io.input(filename)
- 打开给定的文件,并将其设置为当前输入文件;
- io.output()
- 设置当前的输出文件;
- io.read()
- 从当前输入文件读取字符串
- “*all”:读取整个文件
- “*line” :读取下一行
- “*number” :从串中转换出一个数值
- num :读取 num 个字符到串
- io.lines()
- io读取当前文件的迭代器
p160 完全IO模式
通过文件句柄
f = io.open()
- 打开文件,获得一个文件句柄,失败返回nil
f:read()
- 读取文件f
预定义的流句柄:
- 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
14local 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())
-- 用 “b” 表示打开二进制文件
> lua prog.lua file.dos file.unix'
这样运行时,相当于是吧file.dos file.unix 当作参数传进去了;
arg[1] = file.dos
arg[2] = file.unix- 然后就是一些具体的例子了
其他文件操作(知道一下,用到时具体查)
- tmpfile
- 返回临时文件句柄(rw),用完自动清除;
- flush
- 对文件进行修改
- filehandle:seek(whence,offset)
- 用来设置和获取一个文件当前的存取位置
- tmpfile
第 22 章 操作系统库
os.time()
- 单位:秒
- 返回当前时钟的数值
- 可以带参数,表示距离参数设定的日期的时间(有默认值,可以看一下书)
os.date( “ ”,seconds)
可以理解为time函数的反函数;
这个好高级啊,通过第一个参数设置不同的格式,输出不同的内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18%a abbreviated weekday name (e.g., Wed)
%A full weekday name (e.g., Wednesday)
%b abbreviated month name (e.g., Sep)
%B full month name (e.g., September)
%c date and time (e.g., 09/16/98 23:48:10)
%d day of the month (16) [01-31]
%H hour, using a 24-hour clock (23) [00-23]
%I hour, using a 12-hour clock (11) [01-12]
%M minute (48) [00-59]
%m month (09) [01-12]
%p either "am" or "pm" (pm)
%S second (10) [00-61]
%w weekday (3) [0-6 = Sunday-Saturday]
%x date (e.g., 09/16/98)
%X time (e.g., 23:48:10)
%Y full year (1998)
%y two-digit year (98) [00-99]
%% the character '%'
os.clock()
返回执行该程序 CPU 花去的时钟秒数。
使用:
1
2
3
4
5local 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 的索引。
Hooks
- 书里讲的跟shi一样…啥都没有
- Lua 调试(Debug) | 菜鸟教程 (runoob.com)
- lua学习笔记–注册钩子函数_lua 钩子-CSDN博客
- 简单来说,就是lua中有一些事件触发的时机,我们可以用sethook()将函数绑定到事件上面,这样当事件发生时会调用这个函数;
profiler
- 好高级东西…
第 24 章 C API 纵览
这一部分主要是 lua 和 c 的交互吧
C 和 Lua 之间来能个不协调的问题:
- Lua 自动进行垃圾回收,C需要显示的分配存储单元;
- Lua 中的动态类型和C中的静态类型不一致;
24.1 简介
- lua_open():创建一个新环境(lua_state)
- 所有的标准库以单独的包提供,所以如果你不需要就不会强求你使用它们。
- 可以用 luaopen_io() 注册io库等等;
24.2 堆栈
栈在 Lua 与 C 之间交换值;
栈由Lua来管理,垃圾回收器知道那个值正在被 C 使用;
压入元素:
1
2
3
4
5
6void lua_pushnil (lua_State *L);
void lua_pushboolean (lua_State *L, int bool);
void lua_pushnumber (lua_State *L, double n);
void lua_pushlstring (lua_State *L, const char *s,
size_t length);
void lua_pushstring (lua_State *L, const char *s);int lua_checkstack (lua_State *L, int sz);
- 它检测栈上是否有足够你需要的空间
查询元素:
API 用索引来访问栈中的元素。在栈中的第一个元素(也就是第一个被压入栈的)有索引 1,下一个有索引 2,以此类推。我们也可以用栈顶作为参照来存取元素,利用负索引。
检查一个元素是否是一个指定的类型:
- int lua_is… (lua_State *L, int index);
- lua_isnumber,lua_isstring,lua_istable;
获取栈中元素的类型:
- lua_type
- 类型常量:LUA_TNIL、LUA_TBOOLEAN 、 LUA_TNUMBER 、 LUA_TSTRING 、 LUA_TTABLE 、LUA_TFUNCTION、LUA_TUSERDATA 以及 LUA_TTHREAD。
获取栈中元素的值:
用 lua_to*函数:
int lua_toboolean (lua_State *L, int index); double lua_tonumber (lua_State *L, int index); const char * lua_tostring (lua_State *L, int index); size_t lua_strlen (lua_State *L, int index);
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
- 当元素类型和函数类型不匹配时:
- lua_toboolean、lua_tonumber 和 lua_strlen 返回 0,其他函数返回 NULL。
- 注意:
- 当一个 C 函数返回后,Lua 会清理他的栈,所以,有一个原则:永远不要将指向 Lua 字
符串的指针保存到访问他们的外部函数中。
- 其他堆栈操作
```lua
int lua_gettop (lua_State *L);
-- 返回堆栈中元素个数,它也是栈顶元素的索引;
void lua_settop (lua_State *L, int index);
--[[
lua_settop 设置栈顶(也就是堆栈中的元素个数)为一
个指定的值。如果开始的栈顶高于新的栈顶,顶部的值被丢弃。否则,为了得到指定的
大小这个函数压入相应个数的空值(nil)到栈上。特别的,lua_settop(L,0)清空堆栈。你
也可以用负数索引作为调用 lua_settop 的参数;那将会设置栈顶到指定的索引。利用这
种技巧,API 提供了下面这个宏,它从堆栈中弹出 n 个元素:
#define lua_pop(L,n) lua_settop(L, -(n)-1
]]--
void lua_pushvalue (lua_State *L, int index);
-- lua_pushvalue 压入堆栈上指定索引的一个抟贝到栈顶;
void lua_remove (lua_State *L, int index);
-- lua_remove 移除指定索引位置的元素,并将其上面所有的元素下移来填补这个位置的空白;
void lua_insert (lua_State *L, int index);
--lua_insert 移动栈顶元素到指定索引的位置,并将这个索引位置上面的元素全部上移至栈顶被移动留下的空隔;
void lua_replace (lua_State *L, int index);
--lua_replace 从栈顶弹出元素值并将其设置到指定索引位置,没有任何移动操
作。24.2 有例子
第 25 章 扩展你的程序
p189
用 Lua 做配置文件,书里有个例子,讲的比较清除;
- luaL_loadfile(L, filename)
- 加载 lua 文件
- lua_pcall
- 调用函数
- lua_getglobal(L , “globalname”)
- 每调用一次就把相应的变量值压入lua栈顶
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 自动的清除栈中返回结果下面的所有内容。
- 呃,注册函数这一块有点小复杂啊;
- 快速掌握Lua 5.3 —— 从Lua中调用C函数_lua 如何调用c函数-CSDN博客
- 这里讲的非常清除!!!
第 27 章 撰写 C 函数的技巧
在这一章我们将讨论数组操纵、string 处理、在 C 中存储 Lua 值等一些特殊的机制。
27.1 数组操作
1 | -- 获取数组(table)表中的元素(key对应的value) |
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
13void 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);
-- 来将位于栈顶的值放入 buffer
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
永远不要使用数字作为 registry 的 key,因为这种类型的 key 是保留给 reference 系统使用。
Reference 系统是由辅助库中的一对函数组成,这对函数用来不需要担心名称冲突的将值保存到 registry 中去。
1
2
3int r = luaL_ref(L, LUA_REGISTRYINDEX);
从栈中弹出一个值,以一个新的数字作为 key 将其保存到 registry 中,并返回这个key。我们将这个 key 称之为reference。作用:
- 将一个指向 Lua 值的 reference 存储到一个 C 结构体中。
- 当我们需要这种指针(指向Lua对象的指针,Lua 本身不提供指向其他对象的指针)的时候,我们创建一个 reference 并将其保存在 C 中。
1
2
3
4
5
6-- 要想将一个 reference 的对应的值入栈,只需要:
lua_rawgeti(L, LUA_REGISTRYINDEX, r);
-- 最后,我们调用下面的函数释放值和 reference:luaL_unref(L, LUA_REGISTRYINDEX, r);
-- reference 系统也定义了常量 LUA_NOREF,她是一个表示任何非有效的 reference 的整数值,用来标记无效的 reference。任何企图获取 LUA_NOREF 返回 nil,任何释放他的操作都没有效果。
27.3.3 Upvalue
- C 闭包… 有点高级…
第 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 |
|
1 | -- 下面就是使用的例子了 |
28.2 Metatables
细节… 复杂起来了;
为数组创建了一个metatable(记住userdata 也可以拥有 metatables)。下面,我们每次创建一个新的数组的时候,我们将这个单独 metatable 标记为数组的 metatable。每次我们访问数组的时候,我们都要检查他是否有一个正确的 metatable。
因为 Lua 代码不能改变 userdatum 的 metatable,所以他不会伪造我们的代码。
具体例子可以看一下书;
相当于是通过 metatable 给数组打了一个标记,区分了 数组 和 其他的 userdata;
然后在调用的时候检查一下 userdata 是否是数组;
28.3 访问面向对象的数据
复杂起来了…
这一小节讲的是如何通过面向对象的方式去访问数据;
1 | a = array.new(1000) |
这儿的关键在于__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进阶——垃圾回收_lua 垃圾回收-CSDN博客(这个里面的图比较好)
Lua GC机制原理、过程、源码详解-CSDN博客(这个里面文字阐述的比较好,有流程图和源码分析)
Lua 实现了一个增量标记-扫描(Mark and Sweep)收集器。 它使用这两个数字来控制垃圾收集循环:
- 垃圾收集器间歇率
- 控制着收集器需要在开启新的循环前要等待多久。
- 增大这个值会减少收集器的积极性。
- 垃圾收集器步进倍率
- 控制着收集器运作速度相对于内存分配速度的倍率。
- 增大这个值不仅会让收集器更加积极,还会增加每个增量步骤的长度。