0%

《Programming in Lua》读书笔记

这篇文章记录一下阅读 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) -- 输出:8

可以发现:

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))
--[[
输出:
table: 00479BF0
table: 00479BA0
table: 00479CE0
table: 00479D08
5
6
a
b
可以看到域还是没输出。
]]--

可变参数

第 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,这一块比较复杂一点;

其实就是 记忆化搜索 的过程。

第 13 章 Metatables and Metamethods

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) -- {1, 2, 3, 4, 5, 6}
fun_print(s4) -- {1, 2, 3, 4, 5, 6}

-- 测试两个是不同种集合
s1 = Set1.new{1,2,3}
s2 = Set2.new{4,5,6}

s3 = s1 + s2
s4 = s2 + s1

fun_print(s3) -- {1, 2, 3, 4, 5, 6}
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

私有成员

  • 将 Packages 中的函数声明为 local 的(私有)

  • 改进:所有的函数都声明为local的,在 Packages 的最后定义一下公有函数就行。

这一块好多技巧…

学了后面忘了前面… 复习一下;

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 转化为 弧度 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
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() 等价于 #t

t = {n = 10}
print(#t) -- 输出:0

-- 下面做一个有意思的实验
local t = {nil}
print(#t) -- 输出:0

t = {1, nil}
print(#t) -- 输出:1

t = {nil,1}
print(#t) -- 输出:2

t = {nil,nil,1}
print(#t) -- 输出:3

t = {1,nil,2}
print(#t) -- 输出:3

t = {1,nil,2,nil}
print(#t) -- 输出:1

t = {1,nil,2,nil,3}
print(#t) -- 输出:5

t = {1,nil,2,nil,3,nil}
print(#t) -- 输出:3

是不是懵逼了… 我也没搞懂… 具体看这:

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)) -- 输出:1 2 3 4

table.insert(t,2,5); -- 在位置2插入值为5
print(unpack(t)) -- 输出:1 5 2 3 4

local v = table.remove(t) -- 默认在最后删除(并返回)
print(unpack(t)) -- 输出:1 5 2 3

v = table.remove(t,1) -- 删除位置为1的值(并返回)
print(unpack(t)) -- 输出:5 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
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()
    • 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
    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())

    -- 用 “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)
      • 用来设置和获取一个文件当前的存取位置

第 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
      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 堆栈

  • 栈在 Lua 与 C 之间交换值;

  • 栈由Lua来管理,垃圾回收器知道那个值正在被 C 使用;

  • 压入元素:

    1
    2
    3
    4
    5
    6
    void 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 自动的清除栈中返回结果下面的所有内容。
  • 呃,注册函数这一块有点小复杂啊;

第 27 章 撰写 C 函数的技巧

在这一章我们将讨论数组操纵、string 处理、在 C 中存储 Lua 值等一些特殊的机制。

27.1 数组操作

1
2
3
4
5
6
7
8
9
-- 获取数组(table)表中的元素(key对应的value)
void lua_rawgeti (lua_State *L, int index, int key);

-- 设置数组(table)表中的元素(key对应的value,value应该放在栈顶,然后才能设置)
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);


    -- 来将位于栈顶的值放入 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
    3
    int 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
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

// 表示一个 double 类型的数组
typedef struct NumArray
{
int size; // 数组大小
double values[1]; // 相当于是标记一下值
} NumArray;

// 创建数组(Lua 那边传进来一个数组的大小 n)
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); // 创建一个 userdatum(就是一块内存空间,放在栈中)
a->size = n; // 设置一下数组的大小
return 1; // 到这里,一个新的 userdatum 就在栈中了
}

// 设置数组的值(传进来一个 数组指针,下标,值)
static int setarray (lua_State *L) {
NumArray *a = (NumArray *)lua_touserdata(L, 1); // 获取数组(userdatum)
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;
}

// 注册一下 C 函数给 Lua 调用
static const struct luaL_reg arraylib [] =
{
{"new", newarray},
{"set", setarray},
{"get", getarray},
{"size", getsize},
{NULL, NULL}
};

// 打开这个C库
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) -- 创建一个大小为1000的数组
print(a) -- 输出: userdata 的地址
print(array.size(a)) -- 输出: 1000
for i=1,1000 do
array.set(a, i, 1/i)
end
print(array.get(a, 10)) --> 0.1

28.2 Metatables

细节… 复杂起来了;

为数组创建了一个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)收集器。 它使用这两个数字来控制垃圾收集循环:

  • 垃圾收集器间歇率
    • 控制着收集器需要在开启新的循环前要等待多久。
    • 增大这个值会减少收集器的积极性。
  • 垃圾收集器步进倍率
    • 控制着收集器运作速度相对于内存分配速度的倍率。
    • 增大这个值不仅会让收集器更加积极,还会增加每个增量步骤的长度。

欢迎关注我的其它发布渠道