0%

Lua学习笔记

Table

该数据结构有点像Map.

table使用关联型数组(value),你可以使用任意类型的值作为数组的索引(key),但不能是nil.

使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 1. 构造
table = {}
-- ** 构造时,未声明的索引会按照数值自动插入 **
table = {"a","b"} -- 结果 {1="a",2="b"}
-- ** 构造时,未声明的变量在table中自动按1处理 **
table = {a,b} -- 结果 {1=1,2=1}
-- ** 构造时,字符串索引是不能加引号的,如下,加引号会出错 **
-- ** 同时索引的数值是连续递增,不会中断 **
table = {"1",a ="b","2"} -- 结果 {1="1","a"="b",2="2"}

-- 2. 插入和获取等其它操作,此处省略,可百度

-- 3. 获取table长度
--[[当我们获取 table 的长度的时候无论是使用 # 还是 table.getn 其都会在索引中断的地方停止计数,而导致无法正确取得 table 的长度。
可以使用以下方法来代替:]]
function table_leng(t)
local leng=0
for k, v in pairs(t) do-- 该循环原理详见下一章迭代器
leng=leng+1
end
return leng;
end

迭代

树枝for循环

1
2
3
4
5
6
-- var从exp1逐渐变化到exp2,每次变化(循环结束后)以exp3为步长递增var
-- exp3可选,默认为1
-- exp1,exp2,exp3只会在循环开始前执行一次求值,之后不再求值
for var=exp1,exp2,exp3 do
<执行体>
end

泛型for迭代器

1
2
3
4
5
6
7
function 表达式(table)
return 迭代函数,状态常量,控制变量
end

for 变量列表 in 表达式(table) do

end

执行顺序:

  • 第一,初始化,计算 in 后面表达式的值,表达式应该返回泛型 for 需要的三个值:迭代函数、状态常量、控制变量;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用 nil 补足,多出部分会被忽略。
  • 第二,将状态常量和控制变量作为参数调用迭代函数(注意:对于 for 结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)。
  • 第三,将迭代函数返回的值赋给变量列表。
  • 第四,如果返回的第一个值为nil循环结束(nil也无法作为table的索引),否则执行循环体。
  • 第五,回到第二步再次调用迭代函数
pairs

索引可以是任意类型

1
2
3
for k, v in pairs(t) do
print(k, v)
end

无状态迭代器

无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。

每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器只利用这两个值可以获取下一个元素。

这种无状态迭代器的典型的简单的例子是 ipairs

ipairs

索引必须是数值,否则拿不到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 原理
function iter (a, i)
i = i + 1
local v = a[i]
if v then
return i, v
end
end
function ipairs (a)
return iter, a, 0
end
-- 使用
for k, v in ipairs(t) do
print(k, v)
end

多状态迭代器

很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,最简单的方法是使用闭包,还有一种方法就是将所有的状态信息封装到 table 内,将 table 作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在 table 内,所以迭代函数通常不需要第二个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
array = {"Google", "Runoob"}

function elementIterator (collection)
local index = 0
local count = #collection
-- 闭包函数
return function ()
index = index + 1
if index <= count
then
-- 返回迭代器的当前元素
return collection[index]
end
end
end

for element in elementIterator(array)
do
print(element)
end

元表(Metatable)

在 Lua table 中我们可以访问对应的 key 来得到 value 值,但是却无法对两个 table 进行操作(比如相加)。因此 Lua 提供了元表(Metatable),允许我们改变 table 的行为,每个行为关联了对应的元方法。

元表: 可以理解为在table中中增加了一个表,这个被增加的表就称为元表,且只能通过固定的函数设置或者获取,其它以外的都不行(至少pairs迭代获取拿不到)

  • setmetatable(table,metatable): 对指定 table 设置元表(metatable),如果元表(metatable)中存在 __metatable 键值,setmetatable 会失败。
  • getmetatable(table): 返回对象的元表(metatable)

元方法: 元表中以”__”(双下划线)开头的字段即为元方法.

__metatable元方法

当元表中存在该键值/字段,则之后的setmetatable 会失败。

__index元方法

这是 metatable 最常用的键。

当你通过键来访问 table 的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的index 键。如果index对应的是一个表格,Lua会在表格中查找相应的键。如果_index对应的是一个函数的话,Lua就会调用那个函数,table和键会作为参数传递给函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 1.__index对应表
other = { foo = 3 }
t = setmetatable({}, { __index = other })
t.foo -- 结果为3

-- 2.__index对应函数
mytable = setmetatable({key1 = "value1"}, {
__index = function(mytable, key)
if key == "key2" then
return "metatablevalue"
else
return nil
end
end
})
print(mytable.key1,mytable.key2) -- 结果: value1 metatablevalue

__newindex 元方法

newindex 元方法用来对表更新,index则用来对表访问 。

当你给表的一个缺少的索引赋值,解释器就会查找__newindex 元方法:如果存在则调用这个函数而不进行赋值操作。

1
2
3
4
5
6
7
8
9
10
mymetatable = {}
mytable = setmetatable({key1 = "value1"}, { __newindex = mymetatable })

print(mytable.key1) -- 当前mytable{key1= "value1"} mymetatable{}

mytable.newkey = "新值2" -- 当前mytable{key1= "value1"} mymetatable{newkey = "新值2"}
print(mytable.newkey,mymetatable.newkey)-- nil, 新值2

mytable.key1 = "新值1" -- 当前mytable{key1= "新值1"} mymetatable{newkey = "新值2"}
print(mytable.key1,mymetatable.key1) -- 新值1 nil

__call 元方法

__call 元方法在 Lua 调用一个值时调用(将table作为函数进行调用)。以下实例演示了计算表中元素的和:

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
-- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用
-- 自定义计算表中最大键值函数 table_maxn,即计算表的元素个数
function table_maxn(t)
local mn = 0
for k, v in pairs(t) do
if mn < k then
mn = k
end
end
return mn
end

-- 定义元方法__call
mytable = setmetatable({10}, { -- mytable总和为10
__call = function(mytable, newtable)
sum = 0
for i = 1, table_maxn(mytable) do
sum = sum + mytable[i]
end
for i = 1, table_maxn(newtable) do
sum = sum + newtable[i]
end
return sum
end
})
newtable = {10,20,30} -- newtable总和为60
print(mytable(newtable)) --70

__tostring 元方法

__tostring 元方法用于修改表的输出行为。以下实例我们自定义了表的输出内容:

1
2
3
4
5
6
7
8
9
10
mytable = setmetatable({ 10, 20, 30 }, {
__tostring = function(mytable)
sum = 0
for k, v in pairs(mytable) do
sum = sum + v
end
return "表所有元素的和为 " .. sum
end
})
print(mytable) -- 表所有元素的和为 60

为表添加操作符

元方法 对应的运算符
__add 对应的运算符 ‘+’.
__sub 对应的运算符 ‘-‘.
__mul 对应的运算符 ‘*’.
__div 对应的运算符 ‘/‘.
__mod 对应的运算符 ‘%’.
__unm 对应的运算符 ‘-‘.
__concat 对应的运算符 ‘..’.
__eq 对应的运算符 ‘==’.
__lt 对应的运算符 ‘<’.
__le 对应的运算符 ‘<=’.

upvalue和localvalue

upvalue

转载 Lua的function、closure和upvalue

Lua 中的函数是一阶类型值(first-class value),定义函数就象创建普通类型值一样(只不过函数类型值的数据主要是一条条指令而已),所以在函数体中仍然可以定义函数。

  1. 内嵌函数: 函数fn2定义在函数fn1中,则称fn2为fn1的内嵌函数
  2. 外包函数: 函数fn2定义在函数fn1中,则称fn1为fn2的外包(enclosing)函数
  3. upvalue: 内嵌函数可以访问外包函数已经创建的所有局部变量,这些局部变量则称为: 该内嵌函数的外部局部变量(external local variable)或upvalue

另外,内嵌和外包具有传递性,即fn2的内嵌必然是fn1的内嵌,而fn1的外包也一定是fn2的外包

upvalue的存储特性1
1
2
3
4
5
6
7
8
9
10
function fn1(n) -- 函数参数也是局部变量
local function fn2()
print(n) -- 引用外包函数的局部变量
end
return fn2
end
g1 = fn1(1979)
g1() -- 打印出1979
g2 = fn1(500)
g2() -- 打印出500

当执行完g1 = fn1(1979)后,局部变量n的生命本该结束,但因为它已经成了内嵌函数fn2(它又被赋给了变量g1)的upvalue,所以它仍然能以某种形式继续“存活”下来,从而令g1()打印出正确的值。

可为什么g2与g1的函数体一样(都是fn1的内嵌函数fn2的函数体),但打印值不同?

这就涉及到一个相当重要的概念——闭包(closure)。事实上,Lua编译一个函数时,会为它生成一个原型(prototype),其中包含了函数体对应的虚拟机指令、函数用到的常量值(数,文本字符串等等)和一些调试信息。在运行时,每当Lua执行一个形如function…end 这样的表达式时,它就会创建一个新的数据对象,其中包含了相应函数原型的引用、环境(environment,用来查找全局变量的表)的引用以及一个由所有upvalue引用组成的数组,而这个数据对象就称为闭包。由此可见,函数是编译期概念,是静态的,而闭包是运行期概念,是动态的。g1和g2的值严格来说不是函数而是闭包,并且是两个不相同的闭包,而每个闭包可以保有自己的upvalue值,所以g1和g2打印出的结果当然就不一样了。虽然闭包和函数是本质不同的概念,但为了方便,且在不引起混淆的情况下,我们对它们不做区分。

upvalue的存储特性2

使用upvalue很方便,但它们的语义也很微妙,需要引起注意。比如将fn1函数改成:

1
2
3
4
5
6
7
8
9
function fn1(n)
local function fn2()
print(n)
end
n = n + 10
return fn2
end
g1 = f1(1979)
g1() -- 打印出1989

内嵌函数定义在n = n + 10这条语句之前,可为什么g1()打印出的却是1989?

upvalue实际是局部变量,而局部变量是保存在函数堆栈框架上(stack frame)的,所以只要upvalue还没有离开自己的作用域,它就一直生存在函数堆栈上。这种情况下,闭包将通过指向堆栈上的upvalue的引用来访问它们,一旦upvalue即将离开自己的作用域(这也意味着它马上要从堆栈中消失),闭包就会为它分配空间并保存当前的值,以后便可通过指向新分配空间的引用来访问该upvalue。当执行到fn1(1979)的n = n + 10时,闭包已经创建了,但是n并没有离开作用域,所以闭包仍然引用堆栈上的n,当return fn2完成时,n即将结束生命,此时闭包便将n(已经是1989了)复制到自己管理的空间中以便将来访问。弄清楚了内部的秘密后,运行结果就不难解释了。

upvalue的存储特性3-共享
1
2
3
4
5
6
7
8
9
10
11
12
13
function Create(n)
local function foo1()
print(n)
end
local function foo2()
n = n + 10
end
return foo1,foo2
end
f1,f2 = Create(1979)
f1() -- 打印1979
f2() f1() -- 打印1989
f2() f1() -- 打印1999

f1, f2这两个闭包的原型分别是Create中的内嵌函数foo1和foo2,而foo1和foo2引用的upvalue是同一个,即Create的局部变量 n。前面已说过,执行完Create调用后,闭包会把堆栈上n的值复制出来,那么是否f1和f2就分别拥有一个n的拷贝呢?其实不然,当Lua发现两个闭包的upvalue指向的是当前堆栈上的相同变量时,会聪明地只生成一个拷贝,然后让这两个闭包共享该拷贝,这样任一个闭包对该upvalue进行修改都会被另一个探知。上述例子很清楚地说明了这点:每次调用f2都将upvalue的值增加了10,随后f1将更新后的值打印出来。upvalue的这种语义很有价值,它使得闭包之间可以不依赖全局变量进行通讯,从而使代码的可靠性大大提高。

upvalue的获取
1
2
3
--此函数返回函数 f 的第 up 个上值的名字和值。 如果该函数没有那个上值,返回 nil
--以 '(' (开括号)打头的变量名表示没有名字的变量 (去除了调试信息的代码块)。
getupvalue (f, up)

localvalue

暂时略

1
2
3
4
5
6
7
8
9
10
11
12
13
function Create(n)
local function foo1()
print(n)
end
local function foo2()
n = n + 10
end
return foo1,foo2
end
f1,f2 = Create(1979)
f1() -- 打印1979
f2() f1() -- 打印1989
f2() f1() -- 打印1999

f1, f2这两个闭包的原型分别是Create中的内嵌函数foo1和foo2,而foo1和foo2引用的upvalue是同一个,即Create的局部变量 n。前面已说过,执行完Create调用后,闭包会把堆栈上n的值复制出来,那么是否f1和f2就分别拥有一个n的拷贝呢?其实不然,当Lua发现两个闭包的upvalue指向的是当前堆栈上的相同变量时,会聪明地只生成一个拷贝,然后让这两个闭包共享该拷贝,这样任一个闭包对该upvalue进行修改都会被另一个探知。上述例子很清楚地说明了这点:每次调用f2都将upvalue的值增加了10,随后f1将更新后的值打印出来。upvalue的这种语义很有价值,它使得闭包之间可以不依赖全局变量进行通讯,从而使代码的可靠性大大提高。

全局函数

rawget

1
rawget(table,key) -- 获取table中字段key对应的value,忽略table中元表的__index方法

rawset

1
rawset(table,key,value) -- 在table中添加字段key和对应的value,忽略table中元表的__newindex元方法

lua 5.1 setfenv/getfenv

Lua5.1版本存在该函数 , 给每个chunk设置函数环境 . 5.2后移除 , 取消了环境表的概念,增加了_Env来管理.

setfenv(function/number,table)

  • 当第一个参数为一个函数时,表示设置该函数的环境
  • 当第一个参数为一个数字时,为1代表当前函数,2代表调用自己的函数,3代表调用自己的函数的函数,以此类推

所谓函数的环境,其实一个环境就是一个表,该函数被限定为只能访问该表中的域,或在函数体内自己定义的变量。下面这个例子,设定当前函数的环境为一个空表,那么在设定执行以后,来自全局的print函数将不可见,所以调用会失败。

getfenv(f)

  • 该函数未找到准确定义,以下代码供参考
  • 该函数必须在先设置了函数环境之后才能生效
    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
    f = 4
    function a()
    f = 3
    print(getfenv(0).f, getfenv(1).f, getfenv(2).f, getfenv(3).f)
    end
    A = {}
    setmetatable(A, {__index = _G})
    setfenv(a, A)

    function b()
    f = 2
    A.a()
    end
    B = {}
    setmetatable(B, {__index = _G})
    setfenv(b, B)

    function c()
    f = 1
    B.b()
    end
    C = {}
    setmetatable(C, {__index = _G})
    setfenv(c, C)
    c()
_ENV
  • _Env作为chunk‘闭包的第一个upvalue,从 load 开始(初始化为_G),第一个 chunk 就被加上了 _ENV 这个 upvalue ,然后依次传递下去。
  • 如果在某个chunk’中定义 local _ENV={…}其实就相当于修改这个chunk下面的环境。
  • Lua在编译时会给变量名var变为_ENV.var
1
2
3
4
5
6
7
8
9
10
 -- Lua 5.1
function foobar()
setfenv(1, {})
-- code here
end
-- Lua 5.2
function foobar()
local _ENV = {}
-- code here
end
_G 和 _Env

G 是放在注册表LUA_RIDX_GLOBALS中,初始化时核心的库都放在_G中;_Env 是chunk闭包的第一个upvalue,load时默认为_G, 然后后面定义的变量都会在编译时加上_ENV.前缀,以此传递下去,当然也可以修改。lua的注册表,_ENV,_G 底层实现从源代码层级对二者之间的区别进行了讨论。如果想要修改环境的同时还能访问全局变量

1
2
3
4
5
6
7
8
9
10
a = 1  
local newgt = ()
setmetatable(newgt, {__index = _G})
_ENV = newgt
print(a) --> 1
a = 10
print(a) --> 10
print(_G.a) --> 1
_G.a = 20
print(_G.a) --> 20
您的支持是对我最大的动力 (●'◡'●)