资料来源:《Lua程序设计》
使用的版本:5.4.3
设计目的
嵌入应用程序中,为程序提供灵活的扩展和定制功能。
Lua中不用花括号({}
)表示代码块,而是用do...end
或是循环语句...end
的方式替代。
– 运行lua程序 –
1、进入交互编程模式
到达应用的安装目录,运行lua程序。
2、将源代码文件作为lua程序的参数
将lua代码保存到一个文本文件中(通常以lua作为后缀,但不是必须的),运行lua程序,将该文件作为参数:
文件内容:
print("hello, world")
[root@localhost ~]# lua test.lua
hello, world
[root@localhost ~]# cp test.lua qwe
[root@localhost ~]# lua qwe
hello, world
3、像shell脚本那样在第一行指定编译器、赋予执行权限
同样,后缀名根本无所谓(就目前来说)。
#!/usr/local/bin/lua
print("hello, world")
[root@localhost lua_practice]# ./test.lua
hello, world
[root@localhost lua_practice]# cp -a test.lua qwe
cp: overwrite ‘qwe’? y
[root@localhost lua_practice]# ./qwe
hello, world
– 注释 –
单行注释
2个减号(–)
多行注释
--[[
多行注释内容
多行注释内容
]]--
技巧:
--[[
多行注释内容
多行注释内容
--]]--
使用这种方式进行多行注释,如果想要恢复被注释掉的多行内容,则直接在第一行的--[[
前再加上一个减号即可。
C++同样适用:
/*
多行注释内容
多行注释内容
//*/
– 变量 –
默认全局变量。
全局变量不需要声明,访问未初始化的全局变量不会出错,只会得到nil
。
给变量赋值就等于创建了全局变量;用nil
给全局变量赋值就等于将全局变量删除。
变量的域
Lua 变量有三种类型:全局变量、局部变量、表中的域。
全局变量
默认变量都是全局类型,即使首次出现在语句块或函数里。默认值均为nil
。
function foo()
a = 10
b = 5
print(a + b)
end
foo()
print(a + b)
/* 正常运行。如果改为:
print(a + b)
foo()
则会报错:"尝试对nil值执行数学运算"(大概这意思)
*/
根据以上特性,也可把Lua变量理解为:所有可能的变量一开始就都存在了,只不过它们的值都为
nil
,无法使用而已。
局部变量
创建变量时使用local
进行修饰。
a = 5 -- 全局变量
local b = 5 -- 局部变量
function joke()
c = 5 -- 全局变量
local d = 6 -- 局部变量
end
joke()
print(c,d) --> 5 nil
do -- Lua中的代码块
local a = 6 -- 局部变量
b = 6 -- 对局部变量重新赋值
print(a,b); --> 6 6
end
print(a,b) --> 5 6
应该尽可能的使用局部变量,有两个好处:
- 避免命名冲突。
- 访问局部变量的速度比全局变量更快。
补充!
似乎全局变量与局部变量可以重名
i = 20
local i = 10
f = load("i=i+1; print(i)")
g = function() i=i+1; print(i) end
f()
g()
-----------
[root@localhost lua_practice]# ./test.lua
21
11
赋值
Lua可同时对多个变量赋值
等号左侧:多出的变量的值设为nil
;
等号右侧:多出的值被抛弃。
> a, b = 1, 2
> a
1
> b
2
> a = 1, 2
> a
1
> a, b = 1
> b
nil -- 尽管b已经有了有效值“2”
Lua会先计算等号右侧的所有值,然后再执行赋值操作
通过这一特性能很方便地实现变量交换:
-- swap 'x' for 'y'
x, y = y, x
-- swap 'a[i]' for 'a[j]'
a[i], a[j] = a[j], a[i]
– 数据类型 –
变量不需要定义类型,只需要为其赋值即可。只有数据才拥有类型。
8种基本类型:
数据类型 | 描述 |
---|---|
nil | 最简单,只有唯一值nil,表示一个无效值(在条件表达式中相当于false)。 |
boolean | 布尔值,包含两个值:false和true。 |
number | 双精度类型的实浮点数 |
string | 字符串,由一对双引号或单引号来表示 |
function | 由 C 或 Lua 编写的函数 |
userdata | 表示任意存储在变量中的C数据结构 |
thread | 表示执行的独立线程,用于执行协同程序 |
table | Lua 中的表(table)其实是一个**“关联数组”(associative arrays),数组的索引可以是数字、字符串或表类型**。在 Lua 里,table 的创建是通过"构造表达式"来完成,最简单构造表达式是{},用来创建一个空表。 |
可以使用type
函数来测试给定变量的值的类型。
print(type("Hello world")) --> string
print(type(10.4*3)) --> number
print(type(print)) --> function
print(type(type)) --> function
print(type(true)) --> boolean
print(type(nil)) --> nil
-- type函数的返回值类型为string
print(type(type(X))) --> string
nil
一个值单独作为一个类型。
删除作用
用nil
给变量赋值就等于是删除该变量。
!!在进行比较时应该加上双引号
因为type
函数的返回值时字符串,所以作为对比的结果也得是字符串。
> type(x) == nil
false
> type(x) == "nil"
true
boolean
布尔类型。只有2个可选值:true
、false
。
Lua中判断语句只把false
和nil
视为false
,其余的值都为true
,包括数字0
和空字符串""
。
number(待补充)
又可细分为双精度类型和整数类型。
string
用一对双引号或单引号来表示。
也可以使用双方括号来表示“一块”字符串。(像代码段那样的一大段字符串)
html = [[
<html>
<head></head>
<body>
<a href="http://www.runoob.com/">菜鸟教程</a>
</body>
</html>
]]
补充
如果字符串中出现像a=b[c[i]]
这样的内容,其中的“]]
”会导致双方括号提前结束。可使用[==[
和]==]
来作为字符串的首位标记。(等号数量任意,但必须前后一致)
例:
> str=[==[hello, world!]==]
> str
hello, world!
隐式转换
对数字字符串执行算术操作时,Lua会尝试将其转换成一个数字。
> print("2" + 8)
10
> print("2e4" * 3)
60000.0
> print("-3e1" + 5)
-25.0
> print("hello" + 1)
stdin:1: attempt to add a 'string' with a 'number'
stack traceback:
[C]: in metamethod 'add'
stdin:1: in main chunk
[C]: in ?
…
连接字符串
使用2个句号..
连接字符串
> print("hello " .. "world")
hello world
> print("hello " + "world")
stdin:1: attempt to add a 'string' with a 'string'
stack traceback:
[C]: in metamethod 'add'
stdin:1: in main chunk
[C]: in ?
使用#
计算字符串的长度。
> print(#"hello")
5
> str="world"
> print(#str)
5
table (表)
table通过“构造表达式”来完成。最简单构造表达式是{},用来创建一个空表。
-- 创建一个空表
tbl1 = {}
-- 直接初始表
tbl2 = {"apple", "pear", "orange", "grape"}
-- 初始化表同时指定键和值(键为字符串时不需要加引号)
> arr={a = 1, b = 2, c = 3}
> print(arr.a, arr.b, arr.c)
1 2 3
Lua 中的表(table)其实是一个"关联数组"(associative arrays),数组的索引可以是数字或者是字符串。
str={}
str["a"]=1
str["b"]=3
str["c"]=5
str[1]=2
str[2]=4
str[3]=6
for k, v in pairs(str) do
print(k .. " : " .. v)
end
// ?后4位的输出顺序不确定
[root@localhost lua_practice]# ./test.lua
1 : 2
2 : 4
c : 5
3 : 6
a : 1
b : 3[root@localhost lua_practice]# ./test.lua
1 : 2
2 : 4
a : 1
3 : 6
c : 5
b : 3
[root@localhost lua_practice]# ./test.lua
1 : 2
2 : 4
3 : 6
a : 1
b : 3
c : 5
Lua中表的数字索引默认从1开始
local tbl = {"apple", "pear", "orange", "grape"}
for key, val in pairs(tbl) do
print("Key", key)
end
[root@localhost lua_practice]# ./test.lua
Key 1
Key 2
Key 3
Key 4
table不固定长度
使用未赋值的索引搜索表都会得到nil
。
> asd={}
> asd[a]
nil
表的struct“特性”
之所以记为“特性”是为了方便自己理解而已。
能通过方括号对表进行索引([]
),当索引为字符串时,也能通过句号运算符(.
)进行访问。
> str={}
> str["a"] = 1
> str["b"] = 2
> str.a -- 等价于“str["a"]”而不是“str[a]”
1
function
function name(param1[, param2, ...])
...
end
Lua中,函数被视为"第一类值(First-Class Value)",可以存在变量里。
function testFun(tab,fun)
for k ,v in pairs(tab) do
print(fun(k,v));
end
end
tab={key1="val1",key2="val2"};
-- 使用匿名函数作为函数testFun的第二个参数
testFun(tab,
function(key,val)
return key.."="..val;
end
);
thread(线程 待补充)
userdata(自定义类型 待补充)
能将C/C++中的任意数据类型存储到Lua变量中(通常是struct或指针)。
强制类型转换
Lua提供了数值与字符串之间的自动转换。
针对字符串的所有算术操作会尝试将字符串转换为数值;相反,在需要字符串的地方出现了数值时,会把数值转换为字符串。(例如..
操作符)
tonumber
可以将一个字符串显式转换成数值。
第2个参数帮助函数识别当前字符串表示的是哪个进制的数字。如果数字无效,返回nil。
> tonumber("100101", 2)
37
> tonumber("fff", 16)
4095
> tonumber("13", 5)
8
> tonumber("987", 8)
nil
– 运算符 –
- 算术运算符
- 关系运算符
- 逻辑运算符
- 其他运算符
算术运算符
设定 A 的值为10,B 的值为 20:
操作符 | 描述 | 实例 |
---|---|---|
+ | 加法 | A + B 输出结果 30 |
- | 减法 | A - B 输出结果 -10 |
* | 乘法 | A * B 输出结果 200 |
/ | 除法 | B / A 输出结果 2 |
% | 取余 | B % A 输出结果 0 |
^ | 乘幂 | A^2 输出结果 100 |
- | 负号 | -A 输出结果 -10 |
关系运算符
设定 A 的值为10,B 的值为 20:
操作符 | 描述 | 实例 |
---|---|---|
== | 等于,检测两个值是否相等,相等返回 true,否则返回 false | (A == B) 为 false。 |
~= | 不等于,检测两个值是否相等,不相等返回 true,否则返回 false | (A ~= B) 为 true。 |
> | 大于,如果左边的值大于右边的值,返回 true,否则返回 false | (A > B) 为 false。 |
< | 小于,如果左边的值大于右边的值,返回 false,否则返回 true | (A < B) 为 true。 |
>= | 大于等于,如果左边的值大于等于右边的值,返回 true,否则返回 false | (A >= B) 返回 false。 |
<= | 小于等于, 如果左边的值小于等于右边的值,返回 true,否则返回 false |
逻辑运算符
设定 A 的值为 true,B 的值为 false:
操作符 | 描述 | 实例 |
---|---|---|
and | 逻辑与操作符。 若 A 为 false,则返回 A,否则返回 B。 | (A and B) 为 false。 |
or | 逻辑或操作符。 若 A 为 true,则返回 A,否则返回 B。 | (A or B) 为 true。 |
not | 逻辑非操作符。与逻辑运算结果相反,如果条件为 true,逻辑非为 false。 |
用短路思维理解:
and操作符遇到false
时会短路,返回,否则返回右侧的操作数;
例如:A and B and C
,返回第一个为false
的操作数,否则返回最后一个操作数C
。
or操作符遇到true
时会短路,返回,否则返回右侧的操作数。
例子:A or B or C
,返回第一个为true
的操作数,否则返回C
。
其他运算符
操作符 | 描述 | 实例 |
---|---|---|
… | 连接两个字符串 | a…b ,其中 a 为 "Hello " , b 为 “World”, 输出结果为 “Hello World”。 |
# | 一元运算符,返回字符串或表的长度。 | #“Hello” 返回 5 |
运算符优先级
从高到低的顺序:
^
not - (unary)
* / %
+ -
..
< > <= >= ~= ==
and
or
除了 ^
和 ..
外所有的二元运算符都是左连接的。
a+i < b/2+1 <--> (a+i) < ((b/2)+1)
5+x^2*8 <--> 5+((x^2)*8)
a < y and y <= z <--> (a < y) and (y <= z)
-x^2 <--> -(x^2)
x^y^z <--> x^(y^z)
– 数组 –
就是以整数作为索引的表。
支持负数索引(理所当然的)。
多维数组
以二维为例:
1、建立二维数组
array = {}
for i=1,3 do
array[i] = {}
for j=1,3 do
array[i][j] = i*j
end
end
2、建立一个一维数组,通过公式实现二维公式
对多维数组从左往右、从上往下编号k:k = row * maxColumns + col
array = {}
maxRows = 3
maxColumns = 3
for row = 1, maxRows do
for col = 1, maxColumns do
array[row * maxColumns + col] = row * col
end
end
– 字符串 –
https://www.runoob.com/lua/lua-strings.html
字符串本身没什么好记的。转义字符也是ASCII通用的。
但鉴于书中对字符串的操作花了很长的篇幅,且模式匹配与字符串紧密联系,甚至能通过Lua代码来编写代码,所以有必要记录字符串相关知识。
基础部分看上文的-- 数据类型 – string
注意!
Lua中的字符串是不可变的,除非为其赋予新的字符串值。
string库函数基本(给予已知的假设)不会“修改”字符串,而是会返回一个新的字符串作为结果。
string库常用函数
Lua中对字符串的操作大多是由string库提供的。
string.upper(argument)
字符串全部转为大写字母。
string.lower(argument)
字符串全部转为小写字母。
string.sub(s, i[, j])
截取字符串。参数i
、j
划定截取的起止区间s[i, j]
,支持负数索引,j
省略时截取到s
末尾。
返回截取的子串(是个新的字符串变量)。
string.gsub(mainString, findString, replaceString, num)
在字符串中替换指定子串。
mainString
为要执行替换操作的字符串, findString
为被替换的子串,replaceString
要替换的字符,num
替换次数(可以忽略,则全部替换),
例:
> string.gsub("aaaa","a","z",3);
zzza 3
string.find (str, substr, [init, [end]])
在str
中搜索substr
。(第三个参数为索引),返回substr
所占的区间。不存在则返回 nil。
例:
> string.find("Hello Lua user", "Lua", 1)
7 9
string.reverse(arg)
字符串反转。
例:
> string.reverse("Lua")
auL
string.format(…)
返回一个类似C/C++printf
的格式化字符串。
例:
> string.format("the value is: %d",4)
the value is: 4
string.char(arg) string.byte(arg[, int])
两个函数可互相视为反函数。
char 将整型数字参照ASCII码转成字符并连接;
byte 转换字符为整数值(可以指定某个字符,默认值为1,即第一个字符,支持负数索引)。
例:
> string.char(97, 98, 99, 100)
abcd
> string.byte("ABCD")
65
> string.byte("ABCD", -1)
68
> string.byte("ABCD",4)
68
string.len(arg)
计算字符串长度。
string.rep(string, n)
返回字符串string的n个拷贝
> string.rep("abcd",2)
abcdabcd
string.match(str, pattern, init)
string.match()只寻找源字串str中的第一个配对. 参数init可选, 指定搜寻过程的起点, 默认为1。
只有当第2个参数是模式匹配时,返回值才有意义。如果第2个参数是确切的字符串,则返回值就是第2个参数。
> string.match("hello, world. ll", "ll")
ll
> string.match("hello, world.", "%a+")
hello
string.gmatch(str, pattern)
回一个迭代器函数,每一次调用这个函数,返回一个在字符串 str 找到的下一个符合 pattern 描述的子串。如果参数 pattern 描述的字符串没有找到,迭代函数返回nil。
> for word in string.gmatch("Hello Lua user", "%a+") do print(word) end
Hello
Lua
user
– table (表) –
table是关联性数组,可以使用数字(整型和浮点数)或字符串作为索引。
大小不固定。
创建、赋值、删除
-- 初始化表
mytable = {}
-- 指定值
mytable[1]= "Lua"
-- 移除引用
mytable = nil
-- lua 垃圾回收会释放内存
“当我们为 table a 并设置元素,然后将 a 赋值给 b,则 a 与 b 都指向同一个内存。如果 a 设置为 nil ,则 b 同样能访问 table 的元素。如果没有指定的变量指向a,Lua的垃圾回收机制会清理相对应的内存。”
自己的理解:创建表实际上是创建了一个匿名表实例,变量名只起到类似智能指针一样的作用。只有当没有任何一个变量指向表实例时,才会销毁该对象、释放内存。
> str={}
> str[1.2]=3
> str[1.2]
3
> a=str
> a[1.2]
3
> str=nil
> str[1.2]
stdin:1: attempt to index a nil value (global 'str')
stack traceback:
stdin:1: in main chunk
[C]: in ?
> a[1.2]
3
-- 修改结果共享 --
> b=a
> b[1.2]
3
> b[1.2]=4
> a[1.2]
4
table库函数(sort待补充)
table.concat (table [, sep [, start [, end]]])
将表中的元素连接成一个字符串。
sep
:字符串,作为元素间的分隔。
start
、end
:整数,标识区间。
> str={"qwe", "asd", "zxc"}
> table.concat(str, ", ")
qwe, asd, zxc
table.insert (table, [pos,] value)
table.remove (table [, pos])
用于插入和删除数组元素,形参的作用如其名所示。
table.sort (table [, comp])
默认从小到大排序。comp
应该是个函数,待补充。
!重要补充,关于表的键
从书中P.223得知,可以将表作为表的键,以此保证该键的唯一性:
local key = {}
t[key] = value
然后尝试了一下,函数也可以作为表的键。
首先要明确:
t = {}
t[a] 和 t["a"] 是不同的!
t["a"]:以字符串“a”作为键,还可以通过 t.a 来访问;
t[a]:a是个变量,会先将a求值后再进行索引
证:
> t[2] = 4
> a = 2
> t[a] -- 变量a先被求值得2,再索引t[2]
4
以书中内容为基础进行理解:
key = {}
foo = function print(0) end
t = {}
t[key] = 1
t[foo] = 2
似乎是以表key
和函数foo
的地址作为索引,所以能保证键的唯一性。
但是
不需要创建表key
和函数foo
也能直接以用“变量”作为表的键。
> t[qwe] = 100
> t[qwe] = t[qwe] + 11
> t[qwe]
111
这时qwe
明显不是字符串,那这是什么类型的键?它是唯一的吗?
一个将函数作为键的有用的例子
#!/usr/local/bin/lua
function fn1(x) print(x) end
function fn2(x) print("fn2") end
t = {}
t[fn1] = "foo"
t[fn2] = "foo"
-- 用ipairs没有输出结果
for i, v in pairs(t) do i(v) end
[root@localhost ~]# ./test.lua
foo
fn2
– 函数 –
Lua把所有的代码段都当做匿名函数。
Lua 函数主要有两种用途:
- 完成指定的任务,这种情况下函数作为调用语句使用;
- 计算并返回值,这种情况下函数作为赋值语句的表达式使用。
定义格式:
optional_function_scope function function_name( argument1, argument2, argument3..., argumentn)
function_body
return result_params_comma_separated
end
optional_function_scope
:可选的local,决定函数是全局函数还是局部函数。默认全局。
function_name
:函数名。
function_body
:函数体。
result_params_comma_separated
:返回值。Lua函数可分会多个值,每个值用逗号隔开。
简化版定义:
[local] function fun_name( arg1[, arg2, ..., argn])
...
return r1[, r2, ..., rn]
end
补充
对于同一作用域内重名的变量,函数似乎会优先选择局部变量
i = 20
local i = 10
f = load("i = i + 1; print(i)")
g = function() i = i + 1; print(i) end
f()
g()
-----------
[root@localhost lua_practice]# ./test.lua
21
11
Lua中的函数是在运行时定义。
函数的参数只有一个字符串常量时,可以省略括号。常见于require
函数。
require "mod"
Lua中同名同形参列表的函数不会起冲突,而是对函数进行重定义
#!/usr/local/bin/lua
function foo()
print("hi")
end
foo()
function foo()
print("hello")
end
foo()
[root@localhost ~]# ./test.lua
hi
hello
函数作为参数
myprint = function(param)
print("这是打印函数 - ##",param,"##")
end
function add(num1, num2, foo)
result = num1 + num2
-- 调用传递的函数参数
foo(result)
end
-- myprint 函数作为参数传递
add(2, 5, myprint)
多返回值
在return后列出要返回的值的列表即可返回多值:
function maximum (a)
local mi = 1 -- 最大值索引
local m = a[mi] -- 最大值
for i,val in ipairs(a) do
if val > m then
mi = i
m = val
end
end
return m, mi
end
print(maximum({8, 10, 23, 12, 5}))
返回值的保留
函数根据被调用情况调整返回值数量。
-
当函数作为一条单独的语句被调用时,所有返回值都被丢弃。
foo()
-
当函数作为表达式调用时,只保留第一个返回值。
a = foo() + b
-
只有当函数是一系列表达式中的最后一个表达式时,才会保留所有返回值。
“一系列表达式”
又可分为4种情况:
- 多重赋值
- 函数调用时传入的实参列表
- 表构造器
- return语句
例子:
function foo () return "a", "b" end
-------- 多重赋值 --------
x = foo() -- x="a"
x, y = foo() -- x="a", y="b"
x, y, z = foo2(), 3 -- x="a", y=3, z=nil
-------- 函数调用 --------
print(1, foo()) -- 1, a, b
print(foo(), 1) -- a, 1
-------- 表构造器 --------
t = {foo(), 1} -- t = {"a", 1}
t = {1, foo()} -- t = {1, "a", "b"}
-------- return语句 --------
function func ()
return foo()
end
可变参数
和 C 语言类似,在函数参数列表中使用三点 ...
表示函数有可变的参数。
补充!:三点...
就是可变参数的象征,可以直接将其视为一个“特殊的变量使用”。
例子:
function foo(...)
local x,y = ...;
print(x,y)
end
foo(1)
foo(1, 2)
foo(1, 2, 3)
-------------------------------
[root@localhost lua_practice]# ./test.lua
1 nil
1 2
1 2
function add(...)
local s = 0
-- {...} 表示一个由所有变长参数构成的数组
for i, v in ipairs{...} do
s = s + v
end
return s
end
print(add(3, 4, 5, 6, 7)) --->25
有时候需要几个固定参数加上可变参数,固定参数必须放在变长参数之前:
function fwrite(fmt, ...) ---> 固定的参数fmt
return io.write(string.format(fmt, ...))
end
fwrite("runoob\n") --->fmt = "runoob", 没有变长参数。
fwrite("%d%d\n", 1, 2) --->fmt = "%d%d", 变长参数为 1 和 2
select
如果遍历变长参数时,变长参数中含有nil
,那么就用select
函数来访问变长参数。
select('#', …)
返回可变参数的长度。
select(n, …)
用于返回从起点n 开始到结束位置的所有参数列表。
#!/usr/local/bin/lua
function f(...)
-- a获得可变参数的第3个元素
a = select(3, ...)
print (a)
print (select(3, ...))
print (select('#', ...))
end
f(1, 2, 3, 4, 5)
[root@localhost lua_practice]# ./test.lua
3
3 4 5
5
select('#', ...)
可作为数组for循环的第2个参数。
– 闭包 –
网上和书上一堆解释和定义,没一个好懂的。
自己的理解:闭包是一个“有状态的函数”,类似于C++中的仿函数。
一个闭包例子:
function newCounter ()
local count = 0
return function ()
count = count + 1
return count
end
end
-- 创建闭包实例
c1 = newCounter()
c2 = newCounter()
--[[
c1、c2使用同一个函数建立,但内部的变量“count”是相互独立的
--]]--
print(c1()) -- 1
print(c1()) -- 2
print(c2()) -- 1
通过上述例子理解:
C++视角
函数newCount
对应C++中的类
;
c1 = newCounter()
就等于生成了一个newCount实例
,各个实例之间相互独立,之后可将实例c1
作为函数使用。
Lua视角
newCounter
函数的返回值是个匿名函数,
c1 = newCounter()
-- 差不多等价于
local count = 0
c1 = function ()
count = count + 1
return count
end
总之就是用变量c1
保存了匿名函数以及变量count
。
newCounter
函数就是个封装变量的工厂,其返回值才是代表着闭包主体的函数。
– 迭代器 –
闭包 控制变量 迭代函数 不可变常量 控制变量的初值
迭代器是一种可以遍历一个集合中所有元素的代码结构。在Lua中常用函数表示迭代器。每一次调用函数,都会返回集合中的下一个元素。
所有迭代器都要保存一些状态,以识别当前迭代所处的位置以及下一步的位置。
C语言会将这些状态保存在结构体中;对于Lua而言则通过闭包实现。
一个简单的迭代器例子:
function values (t)
local i = 0
return function ()
i = i + 1
return t[i]
end
end
迭代器假设接收的参数t
是一个数组,创建一个变量i
,从0开始递增返回t[i]
,直到遍历完所有元素后返回nil
。
使用:(每次调用iter
都会返回下一个元素)
t = {10, 20, 30}
iter = values(t)
while true do
local element = iter()
if element == nil then break end
print(element)
end
-- 更简单的形式,泛型for就是为了配合迭代器而设计的
t = {10, 20, 30}
-- for的参数只有1个,对应闭包只有1个返回值 --
for element in values(t) do
print(element)
end
P.200的例子:编写迭代器可能很麻烦,但使用迭代器却十分简单。Lua的最终用户一般也不会去定义迭代器,而是使用宿主应用已提供的迭代器。
泛型for循环
泛型for循环的语法:
for var-list in exp-list do
body
end
var-list
:变量列表;exp-list
:表达式列表。
变量列表的第一个变量成为控制变量,其值为nil
时循环结束。
for做的第一件事是对in
后面的表达式求值。表达式应该返回3个值供for保持:迭代函数、不可变状态、控制变量的初始值。多余的丢弃,不足的nil
补齐。
之后是一些无法理解的解释,其中提到“从for代码结构的立足点看,不可变状态根本没有意义”。
for var_1, ..., var_n in explist do block end
-- 等价于
do
local _f, _s, _var = explist
while true do
local var_1, ..., var_n = _f(_s, _var)
_var = var_1
if _var == nil then break end
block
end
end
-- 运行过程
a0 = _var
a1 = _f(_s, a0)
a2 = _f(_s, a1)
...
直到 ai == nil
for中的其他变量是每次调用_f后得到的额外返回值。
无状态迭代器
自身不保存任何状态。可以在多个循环中使用同一个无状态迭代器。
如之前所说,for循环会以_f(_s, _var)
的方式调用迭代函数,一个无状态迭代器只根据不可变状态
和控制变量
这2个变量的值来为迭代生成下一个元素。
ipairs
典型的例子就是ipairs
:
a = {1, 2, 3}
for i, v in ipairs(a) do
print(i, v)
end
不可变状态是表a
,控制变量是当前的索引值i
。
实现:
local function iter (t, i)
i = i + 1
local v = t[i]
if v then
return i, v
end
end
function ipairs (t)
return iter, t, 0
end
使用ipairs(t)
时,ipairs(t)
返回3个值,然后调用iter(t, 0)
,得到1, t[1]
;第二次迭代调用iter(t, 1)
,得到2, t[2]
……
也可以跳过闭包,直接使用iter
:
a = {...}
for i, v in ipairs(a)
do ... end
-- 等价于
for i, v in iter, a, 0
do ... end
pairs
pairs
使用的跌迭代器是函数next
。
next
会以随机次序返回表中的下一个键值对(k-v)
实现:
function pairs (t)
return next, t, nil
end
for k, v in pairs(t)
do ... end
-- 等价于
for k, v in next, t
do ... end
– 二者的区别
ipairs
从1开始按序遍历,键遇到第一个nil
时停止。
pairs
会遍历所有键值对,无序。
例子:
#!/usr/local/bin/lua
t = {
[1] = "t1",
[2] = "t2",
[4] = "t4"
}
print("ipairs:")
for i, v in ipairs(t) do print(i, v) end
print("")
print("pairs:")
for i, v in pairs(t) do print(i, v) end
[root@localhost ~]# ./test.lua
ipairs:
1 t1
2 t2
-- 在3的位置断开了,ipairs提前结束
pairs:
4 t4
1 t1
2 t2
如果将[1] = "t1"
的索引[1]
改为其他值,ipairs
就完全没有输出。
网站教程的补充
不可变状态
也可以是一个常量。
function square(itMax, num)
if num < itMax
then
num = num + 1
return num, num * num
end
end
for i, n in square, 3, 0
do
print(i, n)
end
输出结果为:
1 1
2 4
3 9
在这个例子中,不可变常量
是个整数(而不是一个表)作为控制变量
的上限,且控制变量也不是作为表的索引,而是作为一个变量参与了数学运算。
遍历链表的迭代器
比较直接的反应是吧当前节点当作控制变量,方便迭代器能返回下一个结点:
local function getnext (node)
return node.next
end
function traverse (list)
return getnext, nil, list
end
这样会导致跳过第一个结点。改进:
local function getnext (list, node)
if not node then
return list
else
return node.next
end
end
function traverse (list)
return getnext, list, nil
end
将头结点作为不可变状态
。
!总结
所谓的控制变量
、迭代函数
、不可变状态
这些是针对泛型for循环的语法规范,是迭代器这一概念的一个子集而已。
例如最初的简单例子就是一个闭包,迭代函数是匿名的,接收一个表作为参数,每次遍历都会返回表的下一个元素。
function values (t)
local i = 0
return function ()
i = i + 1
return t[i]
end
end
而针对泛型for设计的闭包就要显式定义迭代函数,并且要保证:
- 第1个参数是不可变的(
不可变状态
),要么是一个表、要么是控制变量的上/下限; - 第2个参数是
控制变量
,通过它来让泛型for循环迭代。
local function iter (t, i)
i = i + 1
local v = t[i]
if v then
return i, v
end
end
function ipairs (t)
return iter, t, 0
end
那么针对这两种不同的情况,在理解泛型for循环时需要区别对待吗?
不需要。只需要注意迭代函数的返回值
和外层函数的参数
就行了。
迭代函数的返回值
按顺序赋值给var-list
;将外层函数视为普通的函数传入正确的参数即可。
真正的迭代器、生成器风格的迭代器
P.206
生成器风格的迭代器
目前为止的迭代器并没有进行实际的迭代,真正的迭代是由for循环完成的,迭代器知识为每次的迭代提供连续的值。也行称为生成器更好。
真正的迭代器
老版本的Lua中还没有for语句(P.207,原文就是这么说的,可能是指泛型for循环吧),真正的迭代器在内部实现迭代,需要接收一个函数作为参数,这个函数在循环的内部被调用。
也就是将for循环搬入了迭代器内,并且
原来for循环的函数体
需要作为参数传入迭代器内。
– 流程控制 –
没什么特殊的,只记语法就行了。
if
if(布尔表达式)
then
--[ 在布尔表达式为 true 时执行的语句 --]
end
if…else
if(布尔表达式)
then
--[ 布尔表达式为 true 时执行该语句块 --]
else
--[ 布尔表达式为 false 时执行该语句块 --]
end
if…elseif…
if(布尔表达式)
then
--[ 布尔表达式为 true 时执行该语句块 --]
elseif (布尔表达式)
then
--[ 布尔表达式为 true 时执行该语句块 --]
else
--[ 布尔表达式为 false 时执行该语句块 --]
end
– 循环语句 –
while
while(condition)
do
statements
end
例子:
a=10
while( a < 20 )
do
print("a 的值为:", a)
a = a+1
end
for
数值for循环
for var=exp1,exp2,exp3
do
<执行体>
end
var
从 exp1
变化到 exp2
,每次变化以 exp3
为步长递增 var
,并执行一次 “执行体”。
exp3
是可选的,默认为1。
和C/C++的for基本一样,3条语句的分隔字符改为了“
,
”,且第3个语句可省略,默认为1。
例子:
-------- 例1 --------
for i=1,f(x)
do
print(i)
end
-------- 例2 --------
for i=10,1,-1
do
print(i)
end
-------- 例3 --------
function f(x)
print("function")
return x*2
end
for i=1,f(5)
do
print(i) -- f(5)的值为10
end
for的三个表达式在循环开始前一次性求值,以后不再进行求值。比如上面的f(x)只会在循环开始前执行一次,其结果用在后面的循环中。
泛型for循环
泛型 for 循环通过一个迭代器函数来遍历所有值,类似 java 中的 foreach 语句。
for 索引, 索引对应的元素 in ipairs(a)
do
print(i, v)
end
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end
i
是数组索引值,v
是对应索引的数组元素值。ipairs
是Lua提供的一个迭代器函数,用来迭代数组。
repeat … until
repeat…until 循环的条件语句在当前循环结束后判断。
结构上类似于do…while。
判断条件为true时才会结束循环。
repeat
statements
until( condition )
例子:
a = 10
repeat
print("a的值为:", a)
a = a + 1
until( a > 15 )
break、continue
break没什么特别的。
Lua不支持continue
语句,而是用goto
来达成同样的效果。
goto
将控制流程无条件地转到被标记的语句处。
goto Label
Label 的格式为:
:: Label ::
例子:
---------- 例1 ----------
local a = 1
::label:: print("--- goto label ---")
a = a+1
if a < 3 then
goto label -- a 小于 3 的时候跳转到标签 label
end
---------- 例2 ----------
i = 0
::s1:: do
print(i)
i = i+1
end
if i>3 then
os.exit() -- i 大于 3 时退出
end
goto s1
实现continue
for i=1, 3 do
if i <= 2 then
print(i, "yes continue")
goto continue
end
print(i, " no continue")
::continue::
print("i'm end")
end
– 模块与包 –
看描述像是把一个表当做C++的类来使用。
模块系统的主要目标之一就是允许不同的人共享代码,从Lua5.1开始为模块和包定义了一系列的规则。
一个模块就是一些代码(Lua或C语言编写的),这些代码可以通过require
函数加载,然后创建和返回一个表。这个表类似于命名空间,其中定义的内容就是模块导出的内容,比如函数和常量。
经常使用的标准库就是模块。
例如:使用math库的函数等价于:
math = require "math" -- 变量名可随意
print(math.sin(3.14))
使用表来实现模块的优点之一是,能像操作普通标那样操作模块。
才反应过来!之前使用库函数时,那种类似于C++“成员函数”的调用方法就是Lua表的结构特征。
local mod = require "mod"
mod.foo()
-- 为个别函数提供不同的名称 --
local mod = require "mod"
local f = m.foo
f()
-- 只引入特定的函数 --
-- 其实是只保存了返回的表的特定函数而已吧,其余部分抛弃了
local f = require "mod".foo
f()
require
require
也是个普通的函数。
函数行为
1、检查模块是否加载
require
会先在表package.loaded
(环境变量?)中检查模块是否已被加载,如果已被加载就返回相应的值。所以一旦一个模块被加载过,后续对同一模块调用require
都将返回同一个值,不再运行任何代码。
2、查找模块
如果模块未被加载,require
根据package.path
指定的路径搜索具有指定模块名的Lua文件。如果找到了,就用loadfile
将其进行加载,获得一个加载函数。
如果找不到指定模块名的Lua文件,就会根据package.cpath
指定的路径搜索C标准库。如果找到了,使用package.loadlib
进行加载,这个函数会查找名为luaopen_模块名
的函数,作为加载函数。
3、调用加载函数
require
得到了加载函数后,通过2个参数(模块名
、加载函数所在文件名称
)调用加载函数,最终加载模块。
如果加载函数有返回值,require
就会返回这个值,将其保存在package.loaded
表中。如果没有返回值且package.loaded[@rep{modname}]
为空,就假设模块的返回值为true,以保证以后require
的调用不会重复加载模块。
标准库的加载函数返回的是模块本身,所以可以实现
local f = require "mod".foo
删除加载模块
package.loaded.模块名 = nil
加载模块时输入参数
实际上Lua是不支持的。require
主要目的之一就是避免重复加载模块,不同参数的同名模块之间会产生冲突。
如果的确需要具有参数的模块,最好需要一个显式的函数来设置参数。
local mod = require "mod"
mod.init(0, 0)
-- 如果加载函数返回的是模块本身
local mod = require "mod".init(0, 0)
模块重命名
有时需要用到同一模块的不同版本。
Lua模块直接改名就行了。
C模块的luaopen_*
函数无法修改,但require
已经有了解决方案:如果模块名中包含连字符(-
),则只会用连字符之前的内容来创建luaopen_*
函数的名称。
例如一个C模块的名称为mod-v3.4
,那么require
会认为该模块的加载函数是luaopen_mod
而不是luaopen_mod-v3.4
(这也不是一个C语言函数的有效名称)。
如果将模块名修改为mod-v3.5
,当调用
require "mod-v3.5"
时,函数require
会找到改名后的文件mod-v3.5
,并将其中原名为luaopen_mod
的函数作为加载函数。
搜索路径
Lua模块和C模块的搜索逻辑相同。
函数require
使用的路径是一组模板。形式类似于shell中的$PATH,每个模板都是包含可选问号的文件名,require
使用模块名替换每个问号,挨个检查模板,直到找到对应文件。
例:(require
只处理分号(;
)和问号(?
))
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua
-- 调用 require "sql" 会尝试打开如下文件:
sql
sql.sql
c:\windows\sql
/usr/local/lua/sql/sql.lua
require
搜索Lua模块的路径是变量package.path
的当前值。package模块被初始化后,会把变量package.path
设置成环境变量LUA_PATH_5_3
如果该环境变量未被定义,则尝试另一个环境变量LUA_PATH
。
搜索器
实际上require
是通过搜索器来执行搜索的。数组package.searchers
列出了函数require
使用的所有搜索器。当所有搜索器被调用都找不到模块时,require
抛出异常。
搜索器就是一个函数,可以自定义搜索器并增加到数组package.searchers
中,使得require
有很高的灵活性。
模块编写
书中例子(P.192)有点复杂,先上自己的例子
mod.lua
后缀名必须有
local mod = {}
mod.var = 100
mod.scrib = "一个模块例子"
function mod.foo1()
print("公有函数foo")
end
local function foo()
print("私有函数foo")
end
function mod.foo2()
foo()
end
-- 使用“成员变量”时必须加上“mod.”
function mod.set(i, str)
mod.var = i
mod.scrib = str
end
function mod.show()
print(mod.var, mod.scrib)
end
return mod
test.lua
#!/usr/local/bin/lua
mod = require "mod"
mod.foo1()
mod.foo2()
mod.show()
mod.set(5, "hello")
mod.show()
[root@localhost lua_practice]# ./test.lua
公有函数foo
私有函数foo
100 一个模块例子
5 hello
收获:require
的参数"mod"
是文件名,而不是文件内实际的模块名。在保持mod.lua
内容不变的前提下修改文件名和require
函数的参数,仍能正常运行。
加载机制
对于自定义的模块,模块文件不是放在哪个文件目录都行,函数 require 有它自己的文件路径加载策略,它会尝试从 Lua 文件或 C 程序库中加载模块。
require 用于搜索 Lua 文件的路径是存放在全局变量 package.path
中,当 Lua 启动后,会以环境变量 LUA_PATH
的值来初始这个环境变量。如果没有找到该环境变量,则使用一个编译时定义的默认路径来初始化。
如果没有 LUA_PATH
这个环境变量,也可以自定义设置,在当前用户根目录下打开 .profile
文件(没有则创建,打开 .bashrc
文件也可以),例如把 "~/lua/"
路径加入 LUA_PATH
环境变量里:
#LUA_PATH
export LUA_PATH="~/lua/?.lua;;"
文件路径以 “;” 号分隔,最后的 2 个 “;;” 表示新加的路径后面加上原来的默认路径。
子模块和包
Lua支持具有层次结构的模块名,通过点来分隔名称中的层次。例如名为mod.sub
的模块是mod
模块的子模块。
一个包是一棵由模块组成的完整的树,是Lua语言中用于发行程序的单位。
– 元表和元方法 –
Lua中每种类型的值都有一套可预见的操作集合,一些类型的操作集合是未定义的,例如无法将两个表相加,无法对函数作比较……
元表可以修改一个值在面对一个未知操作时的行为。例如a、b都是表,可以通过元表定义a + b
操作。Lua会检查两者之一是否是元表且具有__add
字段,如果找到了,就调用该字段的值,即元方法(是个函数),通过元方法计算表的和。
每个值可以有元表。表
和用户数据类型
可以拥有各自独立的元表,其他类型的值共享对应类型所述的同一个元表。
Lua中只能为表设置元表,如果要为其他类型的值设置元表,必须通过C代码或调试库完成。
设置和获取元表
setmetatable(t, mt)
的返回值似乎是t
。
t = {}
print(getmetatable(t)) -- nil
meta_t = {}
setmetatable(t, meta_t)
print(getmetatable(t) == meta_t) -- true
元表的使用
例子
假设有一个用于集合的简单模块(P.214,以下代码简化)
local Set = {}
function Set.new (l)
local set = {}
...
end
function Set.union (a, b)
...
end
function Set.intersection (a, b)
...
end
function Set.toString (set)
...
end
return Set
如果想用+
来计算2个集合的并集,那么可以让所有表示集合的表共享一个元表,在元表中定义集合的行为:
local mt = {}
修改用于创建集合的函数Set.new
,只需要加入一行代码:
function Set.new (l)
local set = {}
setmetatable(set, mt) --
...
end
这样一来,所有由Set.new
创建的集合都具有了一个相同的元表。
最后,向元表中加入元方法__add
:
mt.__add = Set.union
补充一点:(容易被Lua的自由冲昏脑子而忽略的)
t = {} t.a = 1 t.b = 5 function add (a, b) return a + b end t.foo = add print(t.foo(t.a, t.b))
如果把
t.foo = add
放在函数add的定义之前,则t.foo
对应的值是nil
,而不是函数。只有在函数定义后,
t.foo
才能获得函数的地址。这一点在C++中是理所应当的。在这里强调是因为Lua教材中的例子过于自由,容易忽略这一点。
之后对集合使用加法a + b
就等价于Set.union(a, b)
– 类型不同的表达式
对于两个集合间的操作,使用哪个元表是确定的。但如果一个表达式中混合了具有不同元表的值:
s = Set.new(1, 2, 3)
s = s + 1
Lua会按如下步骤查找:
- 如果第1个值有元表且有对应的元方法,则使用那个元方法,忽略第2个值;
- 否则考察第2个值是否有元表、对应的元方法。
- 如果都没有,抛出异常。
以上式子会调用Set.union(s, 1)
(1 + s
和"hello" + s
同理),union
函数不支持整数参数,仍会抛出异常。
元表对应于运算符的键
2个下划线(_
)开头。(仅一部分)
__add +
__sub -
__mul *
__div /
__idiv floor除法
__unm 负数
__mod %
__pow 幂运算
__band 按位与
__bor 按位或
__boxr 按位异或
__bnot 按位取反
__shl 左移位
__shr 右移位
__concat ..
__eq ==
__lt <
__le <=
-- ~=、>、>= 没有元方法
-- a ~= b 转换为 not (a == b)
-- a > b 转换为 b < a
-- a >= b 转换为 b <= a
相等比较有限制,如果两个对象类型不同,则不会调用元方法,直接返回false。
自定义的元方法
元表本质上是一个普通的表,所以任何人都能使用它。在元表中自定义字段也是常见的。
__tostring
print({})
函数print
总是调用tostring
来进行格式化输出。
而函数tostring
会先检查值是否有一个元方法__tostring
,如果有,就会调用这个元方法来完成工作。
对于上文的例子,就相当于
mt.__tostring = Set.tostring
__metatable
函数getmetatable
和setmetatable
也用到了元方法用于保护元表,使得用户无法看到也不能修改集合的元表。
设置了字段__metatable
的值,getmetatable
会返回该值,而setmetatable
会引发错误。
#!/usr/local/bin/lua
t = {}
t1 = {}
setmetatable(t, t1)
t1.__metatable = "FUCK OFF"
print(getmetatable(t))
setmetatable(t, {})
---------------------
[root@localhost ~]# ./test.lua
FUCK OFF
/usr/local/bin/lua: ./test.lua:12: cannot change a protected metatable
stack traceback:
[C]: in function 'setmetatable'
./test.lua:12: in main chunk
[C]: in ?
__pairs
当一个对象拥有__pairs
元方法时,函数pairs
会调用这个元方法来完成遍历。
表相关的元方法
__index
元表中最常用的键。在面向对象部分会解释。
当访问表中一个不存在的字段时会得到nil
。实际上,这些访问并不是立即返回nil
,而是先引发解释器查找一个名为__index
的元方法,如果没有这个元方法,才返回nil
,否则返回值由元方法来提供。
找到__index
元方法时,会用表名和不存在的键为参数去调用元方法,将得到的返回值作为自己的返回值(即访问不存在的键得到的结果)。
__index
元方法不一定需要接收参数,甚至不一定是函数。
例子:
关于继承的原型示例:
相当于默认值,模板
prototype = {x = 0, y = 0, width = 100, height = 100}
声明构造函数new
,创建共享同一个元表的新窗口:
local mt = {}
function new (o)
setmetatable(o, mt)
return o
end
定义元方法__index
-- 第1个参数是表名,这里不需要
mt.__index = function (_, key)
return prototype[key]
end
使用:
w = new{x = 10, y = 20}
print(w.width) -- 100
表w
中没有width
字段,但拥有元方法__index
,通过表
和不存在的键
调用元方法,最后得到的返回值是prototype[width]
,为100。
–元方法不一定是函数
上述例子中的元方法可以修改为
mt.__index = prototype
当__index
元方法为一个表时,就直接访问这个表,同样能返回prototype[width]
。
__newindex
与__index
类似,不同之处在于__newindex
用于表的更新,而__index
用于表的查询。
对一个表中不存在的索引赋值时,查找__newindex
元方法,如果元方法是个函数,则调用它;如果元方法是个表,则对表执行赋值。
一个例子:
__newindex = function (t, n, v) ...
t
为触发元方法的表名,n
为变量名,v
为新的值。
设定表的默认值
通常表中的所有字段默认值都是nil
,可通过元表修改默认值。
手动指定默认值
-- 只要调用了函数的表触发了“__index”,就返回值“d”
function setDefault(t, d)
local mt = {__index = function () return d end}
setmetatable(t, mt)
end
tab = {x = 10, y = 20}
print(tab.x, tab.z) -- 10 nil
setDefault(tab, 0)
print(tab.x, tab.z) -- 10 0
通过setDefault
函数手动指定某个表的默认值。
在表中存储自己的默认值
上一个例子中函数setDefault
为所有需要默认值的表创建了一个闭包
和一个新的元表
。如果需要默认值的表很多,则开销较大。
为了使所有表都能使用同一个元表,可以用一个额外的字段保存每个表的默认值。
流程:
-
所有表都约定好默认值用字段“
___
”(3个下划线_
)保存; -
表对象调用函数
setDefault
设置各自的默认值,绑定元表; -
访问表中不存在的字段,就会直接返回表自身的“
___
”字段的值,即默认值。
local mt = {__index = function (t) return t.___ end}
function setDefault(t, d)
t.___ = d
setmetatable(t, mt)
end
这个例子中,只创建了一个额外的表。
确保保存默认值的特殊键的唯一性
创建一个新的表,将其作为键
和上一个方法基本一样,只不过使用
local key = {}
作为存储默认值的键(留神别搞丢了)。
local key = {}
local mt = {__index = function (t) return t[key] end}
function setDefault(t, d)
t[key] = d
setmetatable(t, mt)
end
将
表k
作为表t的索引
来确保表t
的键的唯一性。
使用对偶表示和记忆(下文 垃圾回收–弱引用表)
对偶表示
键弱引用表。
使用一个弱引用表来存储所有对象的属性。表对象的元方法从弱引用表中取数据,使用函数setDefault
来设置特定对象的默认值。
local defaults = {}
setmetatable(defaults, {__mode = "k"})
local mt = {__index = function (t) return defaults[t] end}
function setDefault (t, d)
defaults[t] = d
setmetatable(t, mt)
end
记忆
值弱引用表。键为默认值,值为元表。
使用函数setDefault
为表对象设置默认值,相同的默认值共享同一个元方法。
local metas = {}
setmetatable(metas, {__mode = "v"})
function setDefault (t, d)
local mt = metas[d]
if mt == nil then
mt = {__index = function () return d end}
metas[d] = mt
end
setmetatable(t, mt)
end
跟踪对表的访问
创建一个**代理(proxy)**作为中介,代理是一个空的表,具有跟踪用户的访问
并将访问重定向到原来的表
的元方法。代理对于用户是透明的。
function track (t)
local proxy = {}
local mt = {
-- 输出追踪信息(可修改),用键k检索表t
__index = function (_, k)
print("*access to element " .. tostring(k))
return t[k]
end,
-- 输出追踪信息(可修改),用键k,值v更新表t
__newindex = function (_, k, v)
print("*update of element " .. tostring(k) .. " to " .. tostring(v))
t[k] = v
end,
-- 通过next获取键值,如果键不为空,输出追踪信息返回键值
__pairs = function ()
return function (_, k)
local nextkey, nextvalue = next(t, k)
if nextkey ~= nil then
print("*traversing element " .. tostring(nextkey))
end
return nextkey, nextvalue
end
end,
-- 普通地返回表长
__len = function () return #t end
}
setmetatable(proxy, mt)
return proxy
end
上述代码结构简化一下:
function track (t)
local proxy = {} -- 代理
local mt = { -- 创建元表、元方法
... -- 定义元方法
}
setmetatable(proxy, mt) -- 绑定
return proxy -- 以代理作为返回值
end
使用
> dofile("test.lua")
> t = {}
> t = track(t) -- t保存的是原来的表t的代理
> t[2] = 1
*update of element 2 to 1
> t[2]
*access to element 2
1
> t
table: 0x82fb30
> t = track(t)
> t
table: 0x830650 -- 一个新的表
只读的表
通过代理的概念很容易实现。只跟踪对表的更新操作并抛出异常即可。
例子:只涉及__index
和__newindex
function readOnly (t)
local proxy = {}
local mt = {
__index = t,
__newindex = function (t, k, v)
error("attempt to update a read-only talbe", 2)
end
}
setmetatable(proxy, mt)
return proxy
end
– 文件IO – 1
– 编译、执行、错误 –
Lua属于解释型语言。
Lua代码在运行前会预编译源码为中间代码。
编译
交互式Lua界面中,使用dofile(文件路径)
函数来加载代码,其实它是一个辅助函数,loadfile
才完成了核心工作。
书中并没有说明编译的过程,而是介绍了2个可在程序运行时编译代码的函数。
loadfile
从文件中加载代码段,编译代码,将编译后的代码段作为一个函数返回。
function dofile (filename)
local f = assert(loadfile(filename))
return f
end
loadfile函数不会抛出异常。
load
类似于loadfile
,但load
函数是从一个字符串或函数中读取代码。
f = load("i = i + 1")
等价于
f = function () i = i + 1 end
通常不会使用字符串常量作为
load
的参数。第1种写法在
load
执行时会进行一次独立的编译,第2种写法执行速度更快。
执行以上load
函数后,就能将f
作为函数调用,使变量i
的值+1。
> i
nil
> f()
(报错)
> i=1
>
> f()
> i
2
> f()
> i
3
如果要一次性地使用一段代码,可以直接调用load
的返回值:load(s)()
与函数的区别
local总会在全局环境中编译代码段。
i = 20
local i = 10
f = load("i=i+1; print(i)")
g = function() i=i+1; print(i) end
f()
g()
-----------
[root@localhost lua_practice]# ./test.lua
21
11
动态生成代码
load
常用于执行外部代码或动态生成代码。
一些动态生成代码的例子:(书里的例子看着很迷,自己想了一个凑合看)
#!/usr/local/bin/lua
var = 0
print("当前变量var的值为0,请输入要对var执行的操作: ")
local line = io.read()
local f = assert(load(line))
print("要执行的操作已记入函数f,函数f要执行多少次? ")
local num = io.read()
num = (num == "") and 5 or num
for i = 1, num
do
f()
str = string.format("i=%d, var=%d", i, var)
print(str)
end
---------------------------------------
[root@localhost lua_practice]# ./test.lua
当前变量var的值为0,请输入要对var执行的操作:
var=var+2
要执行的操作已记入函数f,函数f要执行多少次?
i=1, var=2
i=2, var=4
i=3, var=6
i=4, var=8
i=5, var=10
[root@localhost lua_practice]# ./test.lua
当前变量var的值为0,请输入要对var执行的操作:
var=var+1
要执行的操作已记入函数f,函数f要执行多少次?
3
i=1, var=1
i=2, var=2
i=3, var=3
对于load
的参数就像正常编写代码那样就行了,所以也可以执行赋值操作。
load("local x = ...; return " .. line)
总结
load
和loadfile
当有错误发生时,会返回nil
和错误信息。它们不会改变或创建变量,写入文件等。
这2个函数将程序段编译为一种中间形式,然后以函数的方式返回。
预编译
lua -o test.lc test.lua
lua test.lc
load
和loadfile
都可以接受预编译代码。
错误处理
Lua语言经常被嵌入在应用程序中,当错误发生时不能简单地崩溃或退出。Lua应尽可能地提供处理错误的方式。
error
显式调用error函数并传入一个错误信息作为参数来引发一个错误。
#!/usr/local/bin/lua
n = io.read("n")
if n == 1
then
error("n = 1")
else
print("n = 2")
end
-----------------------------------
[root@localhost ~]# ./test.lua
1
/usr/local/bin/lua: ./test.lua:8: n = 1
stack traceback:
[C]: in function 'error'
./test.lua:8: in main chunk
[C]: in ?
[root@localhost ~]# ./test.lua
2
n = 2
error
还有可选参数用于指出调用层次中的哪层函数应该为错误负责。
function foo (str)
if type(str) ~= "string"
then
error("string expected")
end
...
end
如果为foo
函数传入了非字符串参数,Lua会默认认为是foo
发生了错误,而实际上导致错误的是foo
的调用者。为了纠正这一点,对error
做如下修改:
error("string expected", 2)
assert
n = assert(io.read("n"), "invalid asdqwe")
-----------------------------------
[root@localhost ~]# ./test.lua
qwe
/usr/local/bin/lua: ./test.lua:4: invalid asdqwe
stack traceback:
[C]: in function 'assert'
./test.lua:4: in main chunk
[C]: in ?
assert
函数会检查第1个参数是否为真,如果为真则返回该参数;否则引发一个错误。
第2个参数是可选的错误信息。
file = assert(io.open(name, "r"))
如果open
执行错误,第2个返回值会成为assert的第2个参数。
pcall
如果要在Lua中处理错误,就应该使用pcall
函数来封装代码。
local ok, msg = pcall(function ()
...
end
)
if ok
then
...
else
...
end
pcall
会以保护模式来调用它的第1个参数,pcall
不会引发错误。
如果没有错误发生,pcall
返回true
和第1个参数的所有返回值;否则,返回false
和错误信息(可以是任何类型)。
local status, err = pcall(function () error({code=121}) end)
print(err.code)
xpcall
除了发生错误的位置,有时还希望错误发生时获得更多的调试信息,至少要得到错误发生时完成整函数调用栈的栈回溯。
而pcall
返回时,部分的调用栈已经被破坏了,可以使用xpcall
及时处理有关错误的信息。
xpcall
与pcall
类似,但第2个参数是消息处理函数。常用的有debug.debug
和debug.traceback
。
没例子。
– 面向对象 –
Lua中的一张表就是一个对象。拥有状态;拥有一个与其值无关的标识(self
,类似于this
指针);具有与创建者和被创建位置无关的生命周期。
self
为什么需要self
?
定义一个表的成员函数:
table = {var = 0}
function table.sub (num)
table.var = table.var - num
end
等价于
talbe["sub"] = function (num) table.var = table.var - num end
函数"sub"
实际上是表table
的一个键,所以函数table.sub
必须带上表名table
才能正常调用,也只能作用于表table
。
如果改变了表名,表的“成员变量”
仍能正常访问,但“成员函数”
无法正常运行:
print(table.var) -- 0
table.sub(5)
print(table.var) -- -5
tmp, table = table, nil
print(tmp.var) -- -5
tmp.sub(5) -- 报错“attempt to index a nil value (global 'table')”
!补充:
对函数进行修改:
function table.sub (num) print("Additional Message!!!") table.var = table.var - num end
调用
tmp.sub()
后的输出:[root@localhost ~]# ./test.lua Additional Message!!! /usr/local/bin/lua: ./test.lua:6: attempt to index a nil value (global 'table') stack traceback: ./test.lua:6: in field 'sub' ./test.lua:11: in main chunk [C]: in ?
可看到,实际导致错误的是语句
table.var = table.var - num
。在上文中table
被置为nil
,所以导致了错误。
综上所述,table.sub
就只是一个普通的表中的函数
,严格遵守Lua语法,并没有什么特殊的地方。
只不过从面向对象思想的角度看,就会像书中那样得出“违反对象拥有独立生命周期的原则。”
这一结论。
解决方法:
为了使table.sub
函数脱离特定的全局变量table
,能通过任何引用了该表的变量调用,就得使用额外参数self
:
#!/usr/local/bin/lua
function table.sub (self, num)
print("Additional Message!!!")
self.var = self.var - num
end
tmp, table = table, nil
print(tmp.var) -- 0
tmp.sub(tmp, 5) -- 成功运行,没有报错
print(tmp.var) -- -5
!补充:
其实在这里**
self
并不是一个关键字**(就目前来看)!它代表的就只是一个形参而已,随便换成别的名称都可以让函数正常运行,位置也不是固定的。例如
#!/usr/local/bin/lua table = {var = 0} function table.sub (num, self0) print("Additional Message!!!") self0.var = self0.var - num end tmp, table = table, nil tmp.sub(5, tmp) -- 成功运行
定义、调用时使用冒号(:
)替代函数名中的点号(.
),可以省略形参self
:
function table:sub (num)
print("Additional Message!!!")
self.var = self.var - num
end
tmp, table = table, nil
tmp:sub(5)
使用冒号时,函数内部的形参必须名为self
。
类
通过元表来实现,作为类的表提供一个
new
函数,在那个函数中,创建新表、将自身作为新标的元表、并将self
作为__index
字段的值。
C++中的类:使用class定义一个类,然后生成实例,对实例进行操作。
Lua中没有类的概念,通过表来实现类。
参考基于原型的语言:每个对象都有一个原型,对象遇到未知操作时到原型中去查找。在Lua中实现类,只需创建一个专门被用作其他对象的原型对象即可。
例如让对象B成为对象A的原型:
setmetatable(A, {__index = B})
调用表A中不存在的函数时,触发
__index
,到表B中寻找。
此时B就相当于类,A相当于类B的一个实例:
setmetatable(Instance, {__index = Class})
例子:
创建一个银行账户类(只列出部分):
local mt = {__index = Account}
Account = {balance = 0}
function Account.new (obj)
obj = obj or {}
setmetatable(obj, mt)
return obj
end
创建一个银行账户实例:
a = Account.new{balance = 0}
a:deposit(100)
表a中不存在键"deposit"
,触发元方法,元表中__index
字段对应的是表Account
,进而在表Account
中查找键"deposit"
。大致情况:
a:deposit(100) --> a.deposit(a, 100) --> getmetatable(a).__index.deposit(a, 100)
所以a:deposit(100)
等价于Account.deposit(a, 100)
使用表
Account
中的函数Account.deposit
,处理表a
中的数据。
改进代码
1、直接将作为类的表Account
设为对象表
的元表;
2、对new函数也使用冒号。
function Account:new (obj)
obj = obj or {}
self.__index = self
setmetatable(obj, self)
return obj
end
调用Account:new
时,self
形参得到的实参是Account
,Account
作为新对象的元表,触发__index
后返回自身。
除了为对象实例
提供成员函数
,类Account
还能为新对象提供常量和默认值。
b = Account:new()
print(b.balance) -- 0
变量
b
得到的是一个空表,访问键"balance"
时触发__index
从元表中获取相应字段。之后对键"balance"
的访问不会再涉及到元方法了。
继承
将元表的关系链继续延伸:a是b的元表、b是c的元表,那么a是基类、b是派生类、c是类b的一个对象。
从
类A
中生成的实例B
可视为基类A的派生类B
来生成新的实例C
。代码没有变化。
假设要从类Account
中派生一个子类SpecialAccount
。
SpecialAccount = Account:new()
s = SpecialAccount:new{limit = 1000.00}
SpecialAccount
是一个空表,s = SpecialAccount:new
触发元方法,实际调用的是Account.new
:SpecialAccount:new{limit = 1000.00} 等价于 SpecialAccount.new(SpecialAccount, {limit = 1000.00})
此时
Account.new
函数中的self
值为SpecialAccount
,所以SpecialAccount
也成为了新创建的表的元表,并且__index
指向SpecialAccount
。新创建的表被变量s
接收。从Lua的表结构看,是有3张表:
s SpecialAccount(s的元表) Account(表SpecialAccount的元表)
从面向对象的角度看,3者间形成了继承关系
Account --(派生)--> SpecialAccount --(实例化)--> s
当讨论仅限于表Account
和表SpecialAccount
中时,二者是类和实例的关系;将新创建的表s
纳入讨论,表Account
和表SpecialAccount
又成了基类与派生类的关系,而s
是派生类SpecialAccount
的实例。
当执行s:deposit(100.00)
时,s
会查找SpecialAccount
,SpecialAccount
会查找Account
,找到deposit
字段后执行函数Accoun.deposit(s, 100.00)
。
就像C++中的派生类那样,SpecialAccount
也可以重写基类成员函数。
在Lua中的实现如上文所说,SpecialAccount
的实例会先在SpecialAccount
中寻找相应字段,如果找到了重载的函数,就不会在SpecialAccount
中触发__index
,而是直接调用SpecialAccount
的函数。
Lua的优势
在Lua中无需为了指定一种新行为而创建一个新类。如果只有单个对象需要某种特殊行为,可以直接在该对象中实现这个行为。
假设有一个客户s1
的透支额度总是其余额的10%,那么就可以只修改这个账户(实例)。
function s1:getLimit()
return self.balance * 0.10
return
就是单独为表
s1
添加了一个函数。
多重继承
需要2个独立的函数来实现多重继承。
func1
:搜索函数,参数为1个字段和1个数组。期待该数组的成员是表,遍历数组的所有成员,寻找字段k。
func2
:实现多重继承,参数为可变参数,是多重继承的基类。在func2
中创建新表,新表的元表是个匿名表,__index
字段的值是个匿名函数,匿名函数返回func1
的结果。
一个派生类可以继承自多个基类,所以应定义一个独立的createClass
函数来创建子类,其参数为新类的所有基类。
实现
-- 尝试在plist列出的所有基类中寻找字段k(k为函数或变量名)
local function search (k, plist)
for i = 1, #plist do
local v = plist[i][k]
if v then return v end
end
end
function createClass (...)
local c = {}
local parents = {...} -- 父类列表
--[[
新表c是多继承的派生类,它的的元表是个匿名表,通过元方法返回search函数的结果,search函数从多继承的基类中寻找子类实例需要的字段
形参t为表c地址,k为要从表c获取的字段
]]--
setmetatable(c, {__index = function (t, k)
return search(k, parent)
end})
-- 将c设为元表
c.__index = c
-- 新表的new函数
function c:new (obj)
obj = obj or {}
setmetatable(obj, c)
return obj
end
return c
end
例子:
2个基类
Account = {}
... -- Account 相关的部分省略
Name = {}
function Named:getname ()
return self.name
end
function Named:setname (n)
self.name = n
end
多继承
NamedAccount = createClass(Account, Named)
account = NamedAccount:new{name = "Paul"}
print(account:getname())
调用account:getname()
发生的事:
1、表account中没有“getname”字段,触发__index,到NamedAccount寻找相应字段;
2、表NamedAccount中也没有“getname”字段,触发__index,运行元方法,得到search函数的返回结果;
3、search函数遍历{Account, Named},在2个表中寻找“getname”字段,返回匹配的值或nil。
最终是在表Named中找到“getname”字段,调用Named:getname(account)
这种方法的搜索具有一定复杂性,所以多重继承的性能不如单继承。
相对于单继承,
“在多个基类中查找相应字段”
这一动作需要显式编程实现。
改进性能
一种改进多继承性能的简单方法是将被继承的方法复制到子类中。新类c
的__index
元方法改为
setmetatable(c, {__index = function (t, k)
local v = search(k. parents)
t[k] = v -- 将返回结果保存在表c中
return v
end})
**缺陷:**修改基类函数的定义会很麻烦,因为只有在派生类对象首次使用基类的函数时才会触发__index
,使得对基类函数的修改不会沿着继承层次向下传播。
私有性 ?
Lua语言中标准的对象实现方式没有提供私有性机制。一方面是使用表来表示对象所导致的;另一方面Lua也尽量避免冗余和限制,如果不想访问一个对象的内容,不去访问就是了。
可通过2个表来表示1个对象:一个表用来保存对象的状态,另一个表同来保存对象的接口。
私有成员变量
例子:
self
只是一个普通的变量名,随便起。
function newAccount (initialBalance)
-- 表1,保存对象的状态
local self = {balance = initialBalance}
local withdraw = function (v) self.balance = self.balance - v end
local deposit = function (v) self.balance = self.balance + v end
local getBalance = function (v) return self.balance end
-- 表2,保存对象的接口
return {
withdraw = withdraw,
deposit = deposit,
getBalance = getBalance
}
end
account = newAccount(100)
print(account.getBalance()) -- 100
account.withdraw(30)
print(account.getBalance()) -- 70
所以局部变量self不会在函数结束时被销毁吗???
私有成员函数
定义和共有函数一致,不把它放在最后返回的接口中就行了。
单方法对象
在函数中直接返回另一个函数就行了。
诸如io.linex
和string.gmatch
这样的迭代器,一个在内部保存了状态的迭代器就是一个单方法对象。
function newObject (value)
return function (action, v)
if action == "get" then return value
elseif action == "set" then value = v
else error("invalid action")
end
end
end
d = newObject(0)
print(d("get")) -- 0
d("set", 10)
使用闭包比使用一个表的开销更低,这种对象实现方式很高效,虽然调用的语法有点奇怪。
使用这种方式不能实现继承。
对偶表示
通常,使用键来把属性关联到表:
table[key] = value
对偶表示:创建一个新的表,把原来的表名作为键,表中的字段作为新表名:
key = {}
key[table] = value
例子:
银行账户的实现中,把所有账户的余额放在表balance中,而不是存在各自的账户实例里。
function Account.withdraw (self, v)
balance[self] = balance[self] - v
end
a = Account:new()
b = Account:new()
a:withdraw(10) -- balance[a] = balance[a] - v
b:withdraw(20) -- balance[b] = balance[b] - v
优点
这样的好处在于私有性。即使一个函数可以访问一个账户,除非它能同时访问表balance
,否则就无法访问余额。
如果表balance
是一个在类Account
内部保存的局部变量,那么只有Account
的成员函数才能访问它。
缺陷
一旦把账户作为表balance
中的键,这个账户对于垃圾收集器而言就永远不会变成垃圾,直到某些代码将其从表中显式地移除。
– 环境 –
Lua把所有的全局变量保存在一个普通表_G
中。
#!/usr/local/bin/lua
function foo()
for n, v in pairs(_G) do
print(n, '\t', v)
end
end
foo()
[root@localhost lua_practice]# ./test.lua
_G table: 0x1d4e9c0
_VERSION Lua 5.4
type function: 0x41e740
rawequal function: 0x41eba0
...
定义、使用全局变量相当于使用表_G
:
#!/usr/local/bin/lua
-- 为表_G设置元方法
setmetatable(_G, {
__index = function (_, n)
print("attempt to read undeclared variable" .. n)
end
}
)
print(a)
[root@localhost ~]# ./test.lua
attempt to read undeclared variablea
nil
全局变量的声明
在大型程序中,输入错误的全局变量名可能会导致难以发现的bug,可以通过元方法检测所有对全局表中不存在键的访问:
设置__newindex
和__index
的元方法,在试图访问不存在的全局变量时,会报错。
setmetatables(_G, {
__newindex = function (_, n)
error("attempt to write to undelcare variable" .. n, 2)
end,
__index = function (_, n)
error("attempt to read undelcare variable" .. n, 2)
end
})
rawset / rawget
rawset
设置了保险后,可以通过rawset
函数来声明新的变量,它可以绕过元方法。
function declare (name, initval)
rawset(_G, name, initval or false)
end
-- or false 保证新变量一定会得到一个不为nil的值。
另一种方法是把对新全局变量的复制操作限制在仅能在函数内进行。
用到debug.getinfo(2, "S")
函数,它返回一个表,what
字段能表示该函数被调用的位置是在主代码段、Lua函数还是C函数。
debug.getinfo(2, "S")
函数的效果:#!/usr/local/bin/lua function foo1() print(debug.getinfo(2, "S").what) end function foo2() foo1() end print(debug.getinfo(2, "S").what) foo1() foo2()
[root@localhost lua_practice]# ./test.lua C main Lua
__newindex = function (t, n, v)
local w = debug.getinfo(2, "S").what
if w ~= "main" and w ~= "C" then
error("attempt to write to undelcare variable" .. n, 2)
end
rawset(t, n, v)
end
rawget
设置了保险后,通过rawget
函数,在不报错的情况下测试一个变量是否存在,它可以绕过元方法。
rawget(_G, var)
允许值为nil的全局变量
引入一个辅助表保存已声明的变量名。
local declaredNames = {}
setmetatables(_G, {
__newindex = function (t, n, v)
if not declaredNames[n] then
local w = debug.getinfo(2, "S").what
if w ~= "main" and w ~= "C" then
error("attempt to write to undelcare variable" .. n, 2)
end
declaredNames[n] = true
end
rawset(t, n, v) -- 进行赋值
end,
__index = function (_, n)
if not declaredNames[n] then
error("attempt to read undelcare variable" .. n, 2)
else
return nil
end
end
})
非全局环境
Lua语言没有全局变量。只是语言尽可能构建出了“拥有全局变量”这一幻觉而已。
自由名称:没有关联到显式声明上的名称。
_ENV
例如,下面代码中,x
和y
是自由名称。
local z = 10
x = y + z
Lua语言编译器将代码段中所有的自由名称x
转换为_ENV.x
,所以上方代码等价于:(_ENV
自然也不是全局变量)
local z = 10
_ENV.x = _ENV.y + z
> print(_ENV) table: 0x237d9c0
在交互模式下,每行代码都作为一段独立的代码,所以每行都会有一个不同的
_ENV
变量。除非用do-end将代码段包围起来。
Lua中将所有代码段都当做匿名函数,所以上方代码实际上应该是如下形式:
Lua中将所有代码段都当做匿名函数,所以上方代码实际上应该是如下形式:
local _ENV = some value
return function (...)
local z = 10
_ENV.x = _ENV.y + z
end
_ENV
的初始值可以是任意表(也可以不是表),一个这样的表被称为一个环境。为了维持全局变量存在的假象,Lua语言在内部维护了一个表作为全局环境。
在加载一个代码段时,函数load
会使用预定的**上值(?)**来初始化全局环境。
使用_ENV
_ENV是个普通的变量,可以对其赋值,访问。
赋值_ENV = nil
会使得后续代码无法直接访问全局变量。
#!/usr/local/bin/lua
a = 10
print(a)
local printf = print
_ENV = nil
printf(20) -- 等价于“_ENV = nil”之前的print(20)
printf(a) -- 报错,丢失了全局变量“a”
print(20) -- 报错,丢失了全局变量“print”
所以也能通过对_ENV
赋值来改变代码段使用的环境,例如绕过局部声明。
#!/usr/local/bin/lua
a = 11
local a = 22
print(a) -- 22
print(_ENV.a) -- 11
print(_G.a) -- 11
通常_G
和_ENV
指向的是同一个表。
一旦改变了环境,所有全局访问都将使用新表。如果新环境是空的,会丢失所有全局变量,通常会把一些有用的值放入新环境:
a = 15
_ENV = {g = _G}
a = 1
g.print(_ENV.a, g.a)
g.print(_ENV.a, g.a)
被编译为: _ENV.g.print(_ENV.a, _ENV.g.a)
等价于: _G.print(_ENV.a, _G.a)
使用继承的方式来把旧环境装入新环境
a = 1
local newgt = {}
setmetatable(newgt, {__index = _G}
_ENV = newgt
print(a)
尝试分析:
print(a)被编译为:_ENV.print(a),_ENV中没有print字段,触发__index字段的元方法,从_G中获得print字段。
通过修改环境来修改函数的输出
在函数中使用自由名称,修改了_ENV
就能修改函数输出。
_ENV = {_G = _G}
local function foo ()
_G.print(a)
end
a = 10
foo() -- 10
_ENV = {_G = _G, a = 20}
foo() -- 20
local _ENV
如果定义了一个名为_ENV
的局部变量,对自由名称的引用将会绑定到这个新变量上。
a = 10
do
local _ENV = {print = print, a = 14}
print(a) -- 14
end
print(a) -- 10
因此,可以使用私有环境定义一个函数(直接将_ENV
作为函数的形参,不需要在函数中使用它):
#!/usr/local/bin/lua
function foo (_ENV)
return function () return a end
end
var = foo{a = 10} -- 等价于var = foo({a = 10})
print(var()) -- 10
var = foo{a = 15}
print(var()) -- 15
环境和模块 ?
没看懂。
_ENV和load ?
再使用load加载代码时可以指定运行环境。
env = {}
loadfile("config.lua", "t", env)()
这么做的意义目前理解不了。
– 垃圾回收 –
Lua语言使用自动内存管理。程序不需要显式释放内存,Lua垃圾收集自动地删除成为垃圾的对象。
再智能的垃圾收集器也需要辅助。
Lua语言中用来辅助垃圾收集器的主要机制:
弱引用表:收集Lua语言中还可以被程序访问的对象;
析构器:收集不在垃圾收集器直接控制下的外部对象;
函数collectgarbage:控制垃圾收集器的步长。
弱引用表
弱引用:不在垃圾收集器考虑范围内的对象引用。如果一个对象的所有引用都是弱引用,那么垃圾收集器将会回收这个对象,并删除这些弱引用。
弱引用表:元素均为弱引用的表。
正常情况下,垃圾收集器不会回收一个在可访问的表中作为键或值的对象。
弱引用表有3种类型:具有弱引用的键、具有弱引用的值、键和值都是弱引用。
定义
一个表是否是弱引用表由其元表中的__mode
字段决定。__mode
字段存在时,值是一个字符串:
"k" : 表的键是弱引用
"v" : 表的值是弱引用
"kv" :表的键和值都是弱引用
无论是哪种类型的弱引用表,只要有一个键或值被回收了,那么对应的整个元素都会被从表中删除。
例子
#!/usr/local/bin/lua
a = {}
mt = {__mode = "v"} -- 可尝试将值改为"k",或将下一行注释掉
setmetatable(a, mt)
value = {}
a[1] = value
print(a[1])
value = {}
a[2] = value
print(a[1]) -- 表a中有2个表
print(a[2])
collectgarbage() -- 强制进行垃圾回收
print("-----------------")
print(a[1]) -- a[1]索引的表被回收
print(a[2]) -- a[2]索引的表仍被变量key所引用,因此保留
[root@localhost ~]# ./test.lua
table: 0x1c048e0
table: 0x1c048e0
table: 0x1c042f0
-----------------
nil
table: 0x1c042f0
记忆函数
空间换时间是一种常见的编程技巧。通过记忆函数执行的结果,在后续使用相同参数再次调用该函数时,直接返回之前记忆的结果。
例子
假设有一个服务器,服务器接受的请求是一字符串形式表示的Lua语言代码。每收到一个请求就执行load
函数,再调用编译后的函数。
为了节省load
的昂贵开销,可以用一个辅助表记忆所有函数load
的执行结果,在调用load
函数前,先检查辅助表,再决定是否执行load
函数。
local results = {}
function mem_loadstring (s)
local res = results[s]
if res == nil then
res = assert(load(s))
results[s] = res
end
return res
end
缺陷
可能会导致资源浪费。有些命令可能只出现一次,表results
会逐渐变得臃肿,耗尽服务器的内存。
改进
可以使用弱引用表解决这一问题。
如果表results
具有弱引用的值,那么每个垃圾收集周期都会删除所有那个时刻未使用的编译结果:
local results = {}
setmetatable(results, {__mode = "v"})
扩展
记忆技术(memorization technique)还可以用来确保某类对象的唯一性。
例如,有一个系统用具有3个取值范围相同的字段red、green、blue的表来表示颜色,一个简单地颜色工厂函数每被调用一次就生成一个颜色:
function createRGB (r, g, b) return {red = r, green = g, blue = b} end
使用记忆技术,可以为相同的颜色复用相同的表。需要额外的一张表来存放所有颜色表:
local colors = {} setmetatable(colors, {__mode = "v"}) function createRGB (r, g, b) local key = string.format("%d-%d-%d", r, g, b) local color = colors[key] if color == nil then color = {red = r, green = g, blue = b} colors[key] = color end return color end
使用这种表现,可以将2个颜色使用
==
运算符来比较。因为如果2个变量引用的是同一种颜色,那么2个变量引用的一定是同一个表。随着时间推移,垃圾收集器会清理表
colors
,所以一种指定的颜色(在一段时间内未使用时)在不同的时间内可能由不同的表来表示。
对象属性(对偶表示)
弱引用表的另一种应用是将属性与对象关联起来。
在各种情况下,需要把某些属性绑定到某个对象上,例如函数的函数名、表的默认值、数组的大小等。
当要保存属性的对象是一个表时,可以通过唯一键把属性存储在表自身中。
但如果对象不是一个表,就无法保存属性。即使对象是表,有时也不想把属性保存在表中,例如想保持属性的私有性时,或不想让属性干扰表的遍历。
对偶表示是为对象和属性提供映射的一种理想方法。之前提到,使用对偶表示的缺点是Lua语言无法及时回收不再需要的对象,使用弱引用表就能解决这一问题。
将对偶表示的外部表的键设为弱引用。
析构器
垃圾收集器除了回收对象,还可以帮助程序员释放外部资源,析构器就用于这一目的。
析构器是一个与对象关联的函数,当对象即将被回收时会被调用。
Lua语言通过元方法__gc
实现析构器。
obj = {x = "hello"}
setmetatable(obj, {__gc = function (obj) print(obj.x) end})
obj = nil
collectgarbage() -- hello
创建一个带有元方法__gc
的表,然后修改与该表联系的唯一变量、执行垃圾回收。在垃圾回收期间,Lua发现表已经不能再访问了,因此调用表的析构器,即元方法__gc
。
通过给对象设置元方法来将其标记为**“需要析构的对象”。在进行设置时,元表必须要有元方法__gc
,后期补上的无效**。
将上面的例子稍作修改:
#!/usr/local/bin/lua
obj = {x = "hello"}
mt = {}
mt.__gc = function (obj) print(obj.x) end
setmetatable(obj, mt)
--[[
- 如果元方法__gc在调用了setmetatable之后才进行设置,
- 则Lua无法将对象obj识别为“需要析构的对象”,
- 垃圾回收期间不会有任何输出。
]]--
-- mt.__gc = function (obj) print(obj.x) end
obj = nil
collectgarbage() -- hello
如果在设置时还无法确定析构函数,就先给__gt
一个任意值作为占位符:
mt = {__gt = true}
当垃圾处理器在同一个周期中析构多个对象时,会按照登记顺序逆序调用析构函数(出栈?)。
例子:(一个链表的构造方法)
mt = {__gc = function (obj) print(obj[1]) end}
list = nil
for i = 1, 3 do
list = setmetatable({i, link = list}, mt)
end
list = nil
collectgarbage()
[root@localhost ~]# ./test.lua
3
2
1
复苏
当一个析构器被调用时,它的参数时正在被析构的对象。如果这个对象在析构期间变成活跃的,就称其为临时复苏。在析构器执行期间,如果析构器把该对象存储在全局变量中,使得该对象在析构器返回后仍可访问,就是永久复苏。(?)
由于复苏的存在,Lua语言会在两个阶段中回收具有析构器的对象:
1、垃圾收集器首次发现某个具有析构器的对象不可访问时,将其复苏并放入等待队列中。一旦析构器开始运行,Lua就将该对象标记为已被析构。
2、下一次垃圾收集器又发现这个对象不可访问时,就将这个对象删除。
垃圾收集器
Lua5.0以前,使用的是标记-清除式垃圾收集器。这种收集器会时不时停止主程序的运行来执行一次完整的垃圾收集周期。每个周期由4个阶段组成:标记、清理、清除、析构。(P.267)
标记阶段把根节点集合
标记为活跃;
清理阶段处理析构器和弱引用表。先把被标记为需要析构,但没有被标记为活跃的对象标记为活跃,放在一个单独的列表中。然后便利了弱引用表并从中移除键或值未被标记的元素。
清理阶段遍历所有对象,回收所有未被标记为活跃的对象,清理标记。
析构阶段调用清理阶段被分离出的对象的析构器。
Lua5.1使用了增量式垃圾收集器,与老版收集器异样的步骤,但可以与解释器并发运行。
Lua5.2引入了紧急垃圾收集。当内存分配失败时,Lua会强制进行一次完整的垃圾收集,然后再尝试分配。
– 协程 – !?
没有使用Lua编程的经验,理解不了这部分知识。
Lua 协同程序(coroutine)与线程类似:协程时一系列可执行语句,有自己的栈、局部变量指令指针。同时协程与其他协程共享全局变量和几乎一切资源。
协程与线程的区别:一个多线程程序可以并行运行多个线程,而协程需要彼此协作地运行,特定时刻只能有一个协程运行,且只有当正在运行的协程显式地要求被挂起时其执行才会暂停。
Lua提供的是非对称协程,需要2个函数来控制携程的执行,一个用于挂起,另一个用于恢复。
相关函数
协程相关的所有函数都被放在表coroutine
中。
coroutine.create
创建新协程。该函数只有一个参数,将要执行的代码(协程体)包装在匿名函数中作为create
的参数。返回一个thread
类型的值。
#!/usr/local/bin/lua
co = coroutine.create(function () print("hello, world") end )
print(type(co))
[root@localhost ~]# ./test.lua
thread
coroutine.status
协程有4中状态:挂起(suspended)、运行(running)、正常(normal)、死亡(dead)。可通过函数coroutine.status
来检查。
coroutine.create(co)
coroutine.resume
协程刚创建时处于挂起状态,调用函数coroutine.resume
启动或继续挂起状态的协程。
协程运行结束后变成死亡状态。
coroutine.resume
运行在保护模式中,如果协程在运行中出错,Lua会将错误信息返回给函数resume
。
函数参数:第1个参数为协程变量,之后的可选参数为传递给协程函数的参数。
函数返回值!:第1个参数为boolean
类型。为true
:后续的参数对应yield
的参数;为false
:后续参数是错误信息。
当协程运行结束时,协程体的返回值成为对应函数resume
的返回值。
不带参数的唤醒;
#!/usr/local/bin/lua
co = coroutine.create(function () print("hello, world") end )
coroutine.resume(co)
带参数的唤醒:
#!/usr/local/bin/lua
co = coroutine.create(function (name) print("hello," .. name) end )
print(coroutine.resume(co, " world!"))
print(coroutine.resume(co, " world!"))
-------------------------------------
[root@localhost ~]# ./test.lua
hello, world!
true
false cannot resume dead coroutine
与yield
函数配合使用:
#!/usr/local/bin/lua
co = coroutine.create(function (a, b)
print(0)
coroutine.yield(a + b, a - b)
print(1)
end)
print(coroutine.resume(co, 10, 5))
-------------------------------------
[root@localhost ~]# ./test.lua
0
true 15 5
coroutine.yield
挂起调用该函数的协程。最简单的方法是在协程体内直接调用coroutine.yield()
。
coroutine.yield
的返回值是对应的resume
的参数:
#!/usr/local/bin/lua
co = coroutine.create(function (x)
print("co1", x)
v1, v2 = coroutine.yield()
print("co2", v1, v2)
end)
coroutine.resume(co, 1)
coroutine.resume(co, 2, 3)
-------------------------------------
[root@localhost ~]# ./test.lua
co1 1
co2 2 3
!补充,所谓“对应的resume和yield”
假设下列运行都没有出错。
协程一开始是挂起状态。
调用函数resume(arg1, ...)
让协程开始运行,并将(arg1, ...)
传递给协程函数,作为协程函数的形参列表。 (此时函数resume(arg1, ...)
的调用还未返回!)
协程运行,直到遇到函数yield(ret1, ...)
调用,协程在调用yield(ret1, ...)
的位置挂起。函数resume(arg1, ...)
返回,返回值是(ret1, ...)
。(此时函数yield(ret1, ...)
的调用还未返回!)
调用
yield
函数时,yield
函数的参数就是“想传递给函数resume的调用者的数据”
。
再次调用函数resume(arg1, ...)
,函数yield(ret1, ...)
返回,像resume
一样,yield
的返回值是(arg1, ...)
。
调用
resume
函数时,resume
的参数(arg1, ...)
可以理解为:1、让协程用
(arg1, ...)
为形参赋值开始运行;2、
(arg1, ...)
是想传递给yield
的调用者的数据。
当协程结束时,resume
会返回函数的返回值。
#!/usr/local/bin/lua
co = coroutine.create(function (x)
print("co1", x)
v1, v2 = coroutine.yield(111, 222)
print("co2", v1, v2)
end)
print(coroutine.resume(co, 1))
print(coroutine.resume(co, 2, 3))
-------------------------------------
[root@localhost ~]# ./test.lua
co1 1 -- print("co1", x)
true 111 222 -- print(coroutine.resume(co, 1))
co2 2 3 -- print("co2", v1, v2)
true -- print(coroutine.resume(co, 2, 3))
resume的返回值是对应的yield的参数,yield的返回值是对应的resume的参数
创建协程co
Suspended
| 调用resume(co, 1)
| 协程函数“function (x)”,x赋值1,
↓ 协程开始运行。
Running
| 调用yield(111, 222)
| resume(co, 1)得到返回值 111 222
↓ 协程挂起。
Suspended
| 调用resume(co, 2, 3)
| yield(111, 222)得到返回值 2 3
↓ 协程运行
Running
| 协程结束
| resume(co, 2, 3)得到返回值 true
↓
End
2个函数的参数似乎都是“向上”传递的。
协程的生产者-消费者问题
还无法理解,先记着。
代码结构:
-- 生产者
function producer ()
while true do
local x = io.read()
send(x)
end
end
-- 消费者
function consumer (prod)
while true do
local x = receive(prod)
io.write(x, "\n")
end
end
当一个协程调用函数yield
时,它不是进入了一个新的函数,而是返回一个挂起的调用(调用的是函数resume
)。
同样,对函数resume
的调用也不会启动一个新函数,而是返回一个对函数yield
的调用。
似乎“返回对函数的调用”就是得到函数的返回值。
-- receive唤醒生产者的执行使其生成一个新值
function receive (prod)
local status, value = coroutine.resume(prod)
return value
end
-- send让出产生新值的协程的执行权,将生成的值传递给消费者
function send (x)
coroutine.yield(x)
end
-- 生产者必须运行在协程里
producer = coroutine.create(producer)
尝试理解:
生产者运行在协程里,初始状态为挂起。
当消费者函数被调用时,
resume
函数唤醒生产者协程,生产者生产数据,然后通过yield
函数传递数据并将自己挂起,resume
函数返回数据,供消费者函数使用。函数
producer
作为协程函数,不需要手动调用。函数
consumer
需要一个函数类型参数,用于指定“从哪个生产者中获取数据。”
扩展:过滤器
-- 以上3个函数consumer、receive、send不变
-- producer作修改,返回一个协程
function producer ()
return coroutine.create(function ()
while true do
local x = io.read()
send(x)
end
end)
end
function filter (prod)
return coroutine.create(function ()
for line = 1, math.huge do
local x = receive(prod)
x = string.format("%5d %s", line, x)
send(x)
end
end)
end
consumer(filter(producer()))
将协程用作迭代器 ~
理解不了。P.278开始
待补充。。。
事件驱动式编程 ~
待补充。。。
– 反射 – ?
目前还看不懂。
C语言部分
Lua是一种嵌入式语言,意味着Lua不能独立运行,而是一个链接到其他应用程序的库,将Lua的功能融入到应用中。
Lua也是一种可扩展的语言,可以用其他函数在Lua中注册新的函数,从而增加一些无法直接用Lua语言编写的功能。
在C语言和Lua语言的2种交互形式中,Lua被用作嵌入式语言时,C语言拥有控制权,C代码被称为应用代码;Lua作为可扩展语言时,Lua语言拥有控制权,C代码被称为库代码。
应用代码和库代码都使用相同的API与Lua语言通信。
在C语言中使用Lua函数
Lua和C语言之间是通过栈进行数据交换的。
从stdin获取Lua代码
例子1
往stdin输入lua函数,C语言读取并执行。
lua_State
luaL_newstate()
luaL_openlibs()
luaL_loadstring()
lua_pcall()
lua_tostring()
lua_pop()
1、先调用luaL_newstate
获得一个指向lua_State
的指针。
lua_State *L = luaL_newstate();
2、调用luaL_openlibs
luaL_openlibs(L);
3、用luaL_loadstring
编译用户输入,入栈;并用lua_pcall
从栈中弹出编译的函数并以保护模式运行。
while (fgets(buff, sizeof(buff), stdin) != NULL)
{
error = luaL_loadstring(L, buff) || lua_pcall(L, 0, 0, 0);
if (error)
{
fprintf(stderr, "%s \n", lua_tostring(L, -1));
lua_pop(L, 1);
}
}
3.1、如果出错,用lua_tostring
获取错误信息;lua_pop
弹出该元素。
lua_tostring(L, -1); // 将栈顶元素作为以\0结尾的字符串返回
使用Lua解析配置文件
例子2:使用Lua API来指示Lua解析配置文件
lua_getglobal()
lua_tointegerx()
luaL_loadfile()
配置文件格式:
width = 200
height = 300
代码:
int getglobint(lua_State *L, const char *var)
{
int isnum, result;
// lua_getglobal:(似乎是根据索引)将全局变量的值入栈
lua_getglobal(L, var);
// lua_tointegerx:将栈顶(-1位置)的元素转换为整型并取值
result = (int)lua_tointegerx(L, -1, &isnum);
// isnum保存的是错误值?
if (!isnum)
error(L, "'%s' should be a number\n", var);
// 出栈
lua_pop(L, 1);
return result;
}
void load(lua_State *L, const char *fname, int *w, int *h)
{
// 读取文件fname,调用文件中的代码
if (luaL_loadfile(L, fname) || lua_pcall(L, 0, 0, 0))
error(L, "cannot run config. file %s", lua_tostring(L, -1));
*w = getglobint(L, "width");
*h = getglobint(L, "height");
}
以自己的习惯理解,文件
fname
应该是一些Lua代码,而从网站参照的例子看,似乎文件fname
就是配置文件,当然它的格式也可视为Lua代码。
对于简单的例子,直接使用只包含数字的文件更方便。使用Lua的好处有:Lua处理了所有语法细节,连配置文件也能有注释。
操作表
例子3:设置颜色
一些函数的解释:http://www.360doc.com/content/13/1231/18/14605176_341547587.shtml
使用表来保存颜色数据。
background = {red = 0.30, green = 0.10, blue = 0}
获取表的数据:
#define MAX_COLOR 255
// 假设表位于栈顶。key为表中一个字段
int getcolorfield(lua_State *L, const char *key)
{
int result, isnum;
// 键(key)入栈
lua_pushstring(L, key);
// 在表(-2位置)中查找键对应的值t[key] = value,然后key出栈,value入栈
lua_gettable(L, -2);
// 获取值(-1位置,将栈顶元素作为整数返回)
result = (int)(lua_tonumberx(L, -1, &isnum) * MAX_COLOR);
if (!isnum)
error(L, "invalid component '%s' in color", key);
// value出栈,让表回到栈顶
lua_pop(L, 1);
return result;
}
设置表的数据:
void setcolorfield (lua_State *L, const char *index, int value)
{
lua_pushstring(L, index); // key入栈
lua_pushnumber(L, (double)value / MAX_COLOR); // value入栈
lua_settable(L, -3); // t[key] = value,然后key和vale出栈
}
使用:
lua_getglobal(L, "background");
if (!lua_istable(L, -1))
error(L, "'background' is not a table");
red = getcolorfield(L, "red");
green = getcolorfield(L, "green");
blue = getcolorfield(L, "blue");
一些改进
通过字符串类型的键来检索表的操作很常见,针对这种情况,lua_gettable
和lua_settable
都有对应的函数lua_getfield
和lua_setfield
。
都是省略了键入栈操作。
lua_pushstring(L, key)
lua_gettable(L, -2);
// 等价于
lua_getfield(L, -1, key);
lua_pushstring(L, index);
lua_pushnumber(L, (double)value / MAX_COLOR); lua_settable(L, -3);
// 等价于
lua_pushnumber(L, (double)value / MAX_COLOR);
lua_setfield(L, -2, index)
调用Lua函数
Lua语言可以在一个配置文件中定义应用所调用的函数。
调用Lua函数的API规范:
- 将待调用的函数入栈。
- 将函数的参数入栈。
- 使用
lua_pcall
调用函数。 - 从栈中取出结果。
例如,配置文件中有如下函数:
function foo(x, y)
return (x^2 * math.sin(y)) / (1 - x)
end
调用Lua语言中定义的函数foo
double f(lua_State *L, double x, double y)
{
int isnum;
double z;
lua_getglobal(L, "foo"); // 1
lua_pushnumber(L, x); // 2
lua_pushnumber(L, y); // 2
// 3
if (lua_pcall(L, 2, 1, 0) != LUA_OK)
error(L, "error running function 'f': %s", lua_tostring(L, -1));
z = lua_tonumberx(L, -1, &isnum); // 4
if (!isnum)
error(L, "function 'f' should return a number");
lua_pop(L, 1);
return z;
}
lua_pcall
lua_pcall(lua_State*, 函数参数数量, 期望的结果数, 错误处理函数)
lua_pcall
会根据期望的数量来调整返回值的个数,即用nil
补充或丢弃多余结果。
在压入结果前,lua_pcall
会先把函数和参数从栈中移除。返回值按次序入栈,最后一个返回值的索引是-1。
如果lua_pcall
在运行期间出现错误,会返回错误码,并向栈中压入一条错误信息(此时函数和参数已经出栈)。如果有错误处理函数,在压入错误信息前会先调用错误处理函数。
在Lua中调用C语言
P.341