这篇文章是 《Lua程序设计》 的读书笔记和概要。这是关于lua编程最权威的书籍之一。推荐给lua基础不够牢的童鞋。没有看过的可以通过我这篇文章快速浏览书中内容,已经看过的可以也能借助这篇文章复习一遍。另外由于我之前是使用c#的,所以这篇文章也会提到c#和lua的一些不同点。
全书内容分为4个部分:
程序块,即chunk,由一行或多行lua可执行的代码构成。下面两段代码,一个是程序块一个不是。
-- 是程序块
function f(a,b)return a*a - b*b;
end-- 不是程序块
do return 1;
一般使用dofile或require来执行程序块。
lua变量不能以数字开头,也要避免以下划线开头。lua中的一些特殊变量就是以下划线开头的。也不要使用and、break、do、else、elseif等关键字做变量。
不加local的变量就是全局变量,不需要全局变量了把它设为nil即可。
讲了在UNIX系统中怎么把lua当成脚本解释器来用。
nil主要功能在于区别其他任何值。
注意boolean不是一个条件值的唯一表示方式。false和nil都是假,其他情况都是真。
lua没有整数类型,都是使用双精度浮点数。
字符串机制和c#类型,修改字符串变量时,是创建了一个新的字符串。字符串和数字之间可以自动转换。
lua中使用 两个点来连接字符串。如:
print(10..20)
table和c#的字典有点类似,可以通过索引找到对应的值。除了数组和字典,lua的表还可以用来表示队列、对象等数据结构。不仅可以通过整数来索引,还可以通过字符串或者其他类型(除了nil)。可以使用table.key,table[key]来访问值。注意下面写法上的不同:
a = {}
x = "y"
a[x] = 10
-- 结果为10
print(a[x])
-- 结果为nil
print(a.x)
-- 结果为10
print(a.y)
lua既可以调用lua编写的函数,也能调用以c编写的函数。第5章、第26章将详细讨论。
在其他语言编写的数据类型传到lua里面就是自定义类型,如unity里的transform、component等。线程会在第9章解释。
算术操作符和其他编程语言差别不大,除了加减乘除指数 +、-、*、/、^ 外还有 % 取模。当%用于非整数时表示小数点后几位的数据。比如x%1结果就是x的小数部分,x%0.01表示小数点后2位以后的数。x-x%0.01表示x精确到小数点后两位的结果。
< > <= >= == ~=
对于table,userdata,函数使用引用比较。
逻辑操作符有and、or、not。and和or都有短路求值。即第一个操作数满足了才会评估第二个操作数。
上面已经提到了字符串相关知识,这里不再赘述。
记住操作符优先级并没有什么意义,使用括号指定运算顺序即可。
使用以下语法构造一个table:
a = { x = 10, y =20}
a = {}
a = {10,20}
在没有指定key时,会自动使用数字进行索引,并且以1开头。
lua允许多重赋值。例如:
a,b = 10, 20
-- 交换x与y
x,y = y,x
多重赋值还能用来接收函数的多个返回值:
a,b = f()
lua中使用local创建局部变量,局部变量会随着作用域的结束而消失。在lua中,有一种推荐写法:
local foo = foo
这句代码创建了局部变量foo,并把全局变量foo赋值给它。这种写法两个好处:
for i=1,10 do print(i) end
for i,v in ipairs(a) do print(v) end
break与return用于跳出当前的块。
函数的冒号操作符调用table的函数时将table隐含地作为函数的第一个参数。o.foo(o,x)另一种写法是o:foo(x) 。
lua中的函数可以有多个返回值,并且会根据不同的调用情况选择舍弃返回值。
function foo() retuan "a","b" end
-- x = "a" , "b"被舍弃
x = foo()
-- x = "a", y = 20
x,y = foo(),20
关于多重返回值还有一个特殊函数unpack。它接受一个数组并且从索引1开始返回数组的所有元素。
-- 打印10,20,30
print(unpack(10,20,30))
lua中的函数可以传入不定数量的实参。使用3个点来表示,这种参数叫作变长参数。
function foo(...)
local a,b,c = ...
end
如果有固定参数,固定参数必须放在变长参数之前。
先来回顾一下c#的缺省参数:
private void MyFunc( int a = 0 ,int b = 0 ,int c = 0)
{
}
c#里可以不按照参数的顺序来使用函数:
MyFunc( b : 10, a : 10 )
lua不直接支持这种语法,但是可以通过将函数参数设为table,来实现这种机制。
function rename (arg)
ame(arg.w)
end
rename{old = "temp.lua",new = "temp1.lua"}
当实参只有一个table时,可以省略圆括号。
在lua中,函数是一种第一类值。这个概念的意思是函数和其他类型的值具有相同的权利,即可以传参,可以作为返回值。
函数和其他值一样是匿名的,一个函数定义其实是一条赋值语句。如下面两种写法是等价的:
function foo(x) return 2*x end
foo = function(x) return 2*x end
除了第一类值, lua中函数的另一个特征是有词法域。这是指当一个函数嵌套在另一个函数时,内部函数可以访问外部函数的局部变量。这两种特征都是c#没有的,c#中只能使用委托和匿名委托来实现类似的机制。
先来看以下代码:
function newCounter()
local i = 0
retuan function()i = i+1return i end
end
这段代码中展示了访问非局部变量i 的一个匿名函数。这个函数加上这种非局部变量就叫作一个closure。
如果调用多次newCounter,会创建多个新的局部变量i。
c1 = newCounter()
print(c1()) -- 1
print(c1()) --2
c2 = newCounter()
print(c2()) --1
print(c1()) --3
print(c2()) --2
给table定义函数的几种写法如下:
Lib = {}
Lib.foo = function(x,y) return x + y end
Lib = {foo = function(x,y) return x + y end
}
Lib = {}
function Lib.foo(x.y) return x + y end
另外也可以给函数加上local将其定义为局部函数。
还有定义递归的局部函数时需要注意写法:
这是错误写法,这种写法其实是调用了全局的函数fact
local fact = function(n)
if n == 0 then return 1else return n * fact(n-1)end
end
这是正确写法
local fact
fact = function(n)
if n == 0 then return 1else return n * fact(n-1)end
end
这也是正确写法
local function fact (n)
if n == 0 then return 1else return n * fact(n-1)end
end
尾调用就是指一个函数的最后一步是return另一个函数。
如下就是一个尾调用
function f(x) return g(x) end
而下面代码不是尾调用
function f(x) g(x) end
出现尾调用后,程序不会保存尾调用所在的函数的栈信息,因为没有必要。这种现象称为尾调用消除。基于这种机制,尾调用永远不会导致栈溢出。
尾调用的典型应用就是状态机。用一个函数来表示一个状态。
在讲系统自带的迭代器函数前,先来看一下我们自己应该如何手写迭代器函数:
function values(t)local i = 0return function() i = i + 1; return t[i] end
endt = {10,20,30}
for element in values(t) doprint(element)
end
for循环会在values(t)返回的nil时停止。迭代器函数就是利用闭包的可以访问非局部变量的特性实现的。
使用for in do 写法的for循环是泛型for。
ipairs、pairs 这两个东西本质上就是一个系统帮我们写好的迭代器函数。
for k,v in ipairs(t) do print(k,v) end
7.1的例子有一个缺陷,就是在函数value里每次都会返回一个新的闭包。泛型for在in后面的表达式实际上可以返回3个值。分别是迭代器函数、一个恒定状态值、一个控制变量。这样的话就能保存迭代器函数了。
先来看ipirs的实现原理:
local function iter(a,i)i = i + 1local v = a[i]if v then return i,vend
endfunction ipirs(a)return iter,a,0
end
iter是迭代器函数,a是恒定状态值,0是控制变量初值。for循环的执行逻辑是调用迭代器函数,并且把恒定状态值和控制变量传入。 第一次循环是iter(a,0),第二次循环是iter(a,1),以此类推。
而函数pairs的实现原理:
function pairs(t)return next,t,nil
end
而像ipirs,pairs这种,迭代器函数内部不保存状态,每次迭代时根据恒定状态值和控制变量决定下次的元素的的迭代器叫做无状态的迭代器。
与上面相对的,就是迭代器内部保存状态的复杂迭代器 。使用lua的闭包特性可以轻易做到这点。
这一节书上展示了不使用for如何进行迭代。这个知识对于for语句的老版本lua有用。对如今的开发意义不大。
loadfile和dofile都是从一个文件加载lua代码块,但是不会运行它们。
区别在于loadfile不会引发错误,如果出错会返回nil。
loadstring和loadfile类似,不同在于它是从字符串读取代码。
f = loadstring("i = i + 1")
f = function() i = i + 1 end
这两行代码是等价的,区别在于loadstring开销更大。还有loadstring不涉及词法域。就是说它不能读取局部变量。
lua可以通过动态链接调用c里面的代码。核心函数是package.loadlib。需要两个参数,动态库的路径和函数名称。案例:
local path = '/usr/local/lib/lua/5.1/socket.so"
local f = package.loadlib(path, "luaopen_socket")
lua内部提供了assert函数来帮助检查错误。第一个参数是bool,第二个参数是可选的字符串。
n = io.read()
assert(tonumber(n),"invalid input:"..n.."is not a number")
如果要在lua处理错误,使用pcall来包装代码。类似c#的try、catch。
if pcall(foo) then
-- 如果没有错误
<常规代码>
else
-- 如果有错误
<错误处理代码>
end
在遇到错误时,lua会显示错误信息,可以通过给error函数传入字符串来更改错误信息:
local status,err = pcall(function() a = "a" + 1 end)
print(err) -- stdin:1:attempt to perform arithmetic on a string valuelocal status,err = pcall(function() error("my error") end)
print(err) -- stdin:1:my error
error函数还可以传入第二个参数表示由调用层级的哪一层来报告错误。
pcall有一个不足:在返回错误消息的时候,pcall到错误消息的这部分调用栈已经被销毁了。
所以lua提供了xpcall。可以传入第二个参数即一个错误处理函数。在这个函数里面可以使用
使用
co = ate(function() print("hi") end)
协程有四种状态:挂起(suspended)、运行(running)、死亡(dead)和正常(normal)。
前三个都好理解。协程刚create或者内部调用yield 都会把协程变成挂起状态。代码全部执行完了就是死亡。
当协程A唤醒协程B的时候,A的状态就是正常。
co = ate(function(a,b)coroutine.yield(a + b, a - b)end)
sume(co,20,10)) --> true 30 10
书中还提到了lua的协程是一种非对称协程。非对称协程在挂起时会把控制权给它的调用者。而对称式协程在挂起时会把控制权给其它对称式协程。
这一节举了一个消费者和生产者的例子来说明协程的使用,并且和UNIX的pipe进行了比较,感兴趣的自己看书。
这一节用协程实现了一个迭代器,打印全排列。输出全排列是一道经典的递归算法题,所以把代码贴了上来:
-- 用协程和递归实现全排列
function permgen(a,n)n = n or #aif n <=1 then coroutine.yield(a)elsefor i = 1, n doa[n],a[i] = a[i], a[n]permgen(a,n-1)a[n],a[i] = a[i], a[n]endend
end
-- 迭代器函数
function permutations(a)local co = ate(function() permgen(a) end)return function()local code,res = sume(co)return resend
end
-- 打印
function printResult(a)for i = 1,#a doio.write(a[i]," ")endio.write("n")
end
-- 开始循环打印结果
for p in permutations{"a","b","c"} doprintResult(p)
end
lua中可以使用coroutine.wrap来代替permutations函数里的内容。它也能创建一个协程,不同的是,它返回一个函数,每次调用这个函数,相当于是调用了一次
function permutations(a)return coroutine.wrap(function() permgen(a) end)
end
虽然书上写了lua的协程是一种多线程。但是它跟真正的多线程又有所不同。所以又叫它非抢先式的多线程。
传统抢先式的多线程下,操作系统会规定每个线程每次的执行时间,到时间了就把cpu给其它线程用。而非抢先式一个线程占用多少时间它自己说了算。
这节剩下篇幅又是一个很多代码的案例,写的是一个http下载多个文件的例子。
这一章节书中讲解了两个完整的程序案例,一个是将table读成html,一个是马尔可夫链。感兴趣自己看书。
这一章的内容是讲用table实现各种数据结构。
实现数组的几种写法:
-- 方式1
a = {}
for i = 1, 100 doa[i] = 0
end
-- 方式2
a = {1, 4, 9, 16}
-- 方式3
a = {[1] = 1 , [2] = 4}
另外,使用 长度操作符# 可以获得数组大小。
另外注意索引可以是负数。
print(#a)
方法1:在table里再套一个table:
mt = {}
for i = 1,N domt[i] = {}for j = 1,M domt[i][j] = 0end
end
方法2:把两个索引合并为一个索引:
mt = {}
for i = 1, N dofor j = 1,M domt[(i-1)*M+j] = 0end
end
每个节点是一个table,里面存有next和value。
用两个索引,表示首尾的位置。
List = {}
w()return {first = 0, last = -1}
end
-- 往队首添加元素
function List.pushfirst(list, value)local first = list.first - 1list.first = firstlist[first] = value
end
pushfirst函数有点绕,我举个例子:
理解了这个,往队尾添加元素,从队首弹出元素,从队尾弹出元素都是同一个道理了。
这一节展示了lua可以把字符串当索引的功能,在查找某个字符是否在集合里很方便:
t = {["while"] = true , ["end"] = true}
这一节展示了如何读取多行字符串,因为lua字符串和c#一样都是不可变的,如果每读一行都拼接上一次的结果效率会极其低下。
好的做法是把每一行存起来,最后使用at。
local t = {}
for line in io.line() dot[#t + 1] = line.."n"
end
local s = at(t)
每个节点为一个table,table有两个字段:节点的名称和此节点相邻的节点集合。
这一章展示了如何将lua作为数据文件,以及序列化lua。
将lua作为数据文件的案例:
Entry{"Matsuri", "152", "hololive"}
Entry{"Fubuki", "160", "hololive"}
Entry{"zhangjinghua", "156", "overidea"}
其中Entry是一个函数的调用,参数为table类型。还记得之前的内容吗?如果函数只有一个参数,并且类型为table,调用函数时可以省略括号,直接使用func{}的形式 。 通过Entry函数的内容,我们可以实现各种各样的功能。比如想知道上面代码里有几项数据:
local count = 0
function Entry(...) count = count + 1 end
dofile("data")
print(count)
需要使用type来判断类型,如果子元素是table,进行递归。需要使用string.format和 %q 来处理奇葩字符串:
a = 'a"problematic"\string'
print(string.format("%q",a)) --> "a"problematic"\string"
对于环形table和共享子table也要做特殊处理:
a = {x = 1,y = 2;{3,4,5}}
a[2] = a -- 环
a.z = a[1] -- 共享子table
解决办法是使用一个额外的table来保存已经被记录过的table和它的名称,key是table,value是table的名称。
通过设置元方法可以让表具有加减乘除等算术操作。以加法 __add 为例:
local mt = {}
Set = {}
w(l)local set = {}setmetatable(set,mt)for _,v in ipairs(l) do set[v] = true endreturn set
endfunction Set.union(a,b)local res = w()for k in pairs(a) do res[k] = true endfor k in pairs(b) do res[k] = true endreturn res
endmt.__add = Set.union
s1 = w(10,20,30)
s2 = w(30,1)
s3 = s1 + s2
如果两个表的元表不一样,lua会优先以第一个表的元表查找__add方法。如果没有再找第二个,都没有就报错。
元表支持的各种算术操作符如下: __add、__mul、__sub、__div、__unm(相反数)、__mod(取模)、__pow(乘幂)。
除了算术运算符,还有关系运算符,元方法为:__eq(等于)、__lt(小于)、__le(小于等于)。
这一节介绍了 __tostring 和 __metatable 的用法。
当调用print的时候会调用tostring函数。下面两个print是等价的:
a = 1;
print(a)
print(tostring(a))
而如果tostring里面是一个表,会调用表的 __tostring 元方法。
而 __metatable 用于保护元表。接上一节的例子
mt.__metatable = "error発生"
s1 = w()
print(getmetatable(s1)) --> error発生
setmetatable(s1,{}})
-- stdin:1: cannot change protected metatable
1. __index元方法
当访问一个table不存在的元素时,会访问__index的元方法。
2. __newindex 元方法
__ndexindex同理,当对一个table不存在的元素赋值时,会访问__newindex的元方法。
使用rawset和rawget可以绕过元表的__index和__newindex。
local mt = {__newindex = function(table, key, value)......end}
3. 应用__index来设置table的默认值
如下的写法可以让所有需要默认值的table共用一个元表:
local mt = {__index = function(t) return t.___ end}
function setDefault(t,d)t.___ = dsetmetatable(t,mt)
end
tab = {x = 10,y = 20}
setDefault(tab,0)
print(tab.x, tab.z) --> 10 0
4. 应用__index和__newindex来跟踪table的访问
5. 实现只读的table
function readOnly(t)local proxy = {}local mt = {__index = t,__newindex = function(t,k,v)error("attempt to update a read-only table",2)end}setmetatable(proxy , mt)return proxy
end
获取全局变量和赋值全局变量:
value = _G[varname]
_G[varname] = value
如果变量名是类似"d"的情况应该这么写:
function getfield(f)local v = _Gfor w atch(f,"[%w_]+") do v = v[w]endreturn v
end
由于lua将全局变量存放在一个普通的table里,所以可以通过元表来改变访问全局变量的行为 。
setmetatable(_G,{__newindex = function(t,n,v)local w = info(2,"S).whatif w ~="main"error("attempt to write to undeclared variable"..n,2)end rawset(t,n,v)end,__index = function(t,n)error("attempt to read undeclared variable"..n,2)end})
这里面用到了
lua里存放全局变量的表默认也是全局的,修改全局变量将会影响其他函数。但是可以使用setfenv来改变当前函数存放全局变量的表(也叫当前函数的环境)。 setfenv第一个参数为数字或函数,为数字时,表示第几层的函数,第二个函数为表。
a = 1
setfenv(1,{})
print(a) --> 会报错,print为nil
上面这个例子改变了环境,所以之前能使用的全局变量或函数全部不能使用了。
注意每个闭包也有自己的环境:
function factory()return function()return aend
enda = 3
f1 = factory()
f2 = factory()
setfenv(f1, {a = 10})
print(f1()) --> 10
print(f2()) --> 3
require用于加载模块,返回由模块函数组成的table,如果已经加载过,会保存在package.load[name] 里。
local m = require "io"
m.write("hello worldn")
require用于搜索lua文件的路径放在package.path中。
为了后期更改模块名的方便,不建议定义函数的时候使用真正的模块名。
local M = {}
complex = M
M.i = {r =0, i = 1}
w(r.i) return {r = r, i = i} end
return complex
最后一行的return可以用package.loaded[modname] 替代:
local modname = ...
local M = {}
_G[modname] = M
package.loaded[modname] = M
使用环境可以改善写模块经常出现的两个问题:
local modname = ...
local M = {}
_G[modname] = M
package.loaded[modname] = M
setfenv(1, M)
声明函数不再需要前缀时 就会变成:
function add(c1,c2)return new(c1.r + c2.r, c1.i + c2.i)
end
上述的写法会产生无法调用原先的全局变量的缺点,解决方案有三:
local sqrt = math.sqrt
local io = io
setfenv(1,M)
使用module(…) 可以代替下面这几行代码:
local modname = ...
local M = {}
_G[modname] = M
package.loaded[modname] = M
setfenv(1, M)
使用module(…,package.seeall) 相当于上面的代码加上:
setmetatable(M,{_index == _G})
在定义模块名时可以加点,mod.sub 是mod的一个子模块。一个包是一个完整的模块树。执行require搜索文件时,会自动把模块名中的点转为分割符:
./?.lua
-- require "a.b"
./a/b.lua
注意:
在定义函数时,建议使用self:
function Account.withdraw (self, v)self.balance = self.balance - v
end
有两个好处:
调用时使用冒号语法隐藏self :
Account:withdraw(100,100)
使用元表和__index的机制可以实现类实例:
function Account:new(o)o = o or {}setmetatable(o,self)self.__index = selfreturn o
end
继承的实现:
SpecialAccount = Account:new()
s = SpecialAccount:new(limit = 1000)
编写新的方法:
function SpecialAccount:getLimit()return self.limit or 0
end
如果有一个对象需要特殊的行为,可以直接在对象中实现这个行为。
function s:getLimit()return balance * 0.10
end
多重继承实现的关键在于不能将基类作为子类的元表:
local function search(k,plist)for i = 1, #plist dolocal v = plist[i][k]if v then return v endend
endfunction createClass(...)local c = {}local parents = {...}setmetatable(c,{__index = function(t,k)return search(k,parents)end})function c:new(o)o = o or {}setmetatable(o,{__index = c})return oend
return c
end
多重继承由于需要查找父类,性能上不如单一继承,改进的方法之一是将继承的方法复制到子类中, 缺点是子类不能修改方法的定义。
在lua里实现私密性的做法:
使用两个table来表示一个对象,一个table用于存储对象的状态,另一个用于操作 。
当一个对象只有一个方法时,可以不用创建table,而是将这个单独的方法作为对象来表示返回 。
在lua不会回收可访问table中作为key或者value的对象 。除非使用弱引用。 另外像数字和布尔这样的值类型是不会被回收的 。
__mode含有"v"表示表arr里的value是弱引用,含有"k"表示arr里的key是弱引用,含有"kv"则两者兼有。意思是当value或者key为nil时,GC之后表arr里不再拥有引用
t1,t2 = {},{}arr = {}arr[1] = t1arr[2] = t2t1 = nilsetmetatable(arr,{__mode = "v"})collectgarbage()for k,v in pairs(arr) doprint(k,v)end-- 只有1个t2,t1已经被回收
备忘录函数的作用就是把经常调用的数据缓存起来,用空间换时间。这种函数就很适合使用弱引用。这个时候需要把value作为弱引用。
在lua里,table也是可以作为key的 。 当table作为key时,也需要使用弱引用,不然这个table也不会被回收,这种时候就需要 把key作为弱引用。
弱引用还有一个应用就是设置table的默认值。
这是将table作为key,默认值作为value存起来 :
local defaults = {}
setmetatable(defaults,{__mode = "k"})
local mt = {__index = function(t) return defaults[t] end}
function setDefault(t,d)defaults[t] = dsetmetatable(t,mt)
end
当然也可以将默认值作为key,table作为value :
local metas = {}
setmetatable(metas,{__mode = "v"})
function setDefault(t,d)local mt = metas[d]if mt == nil thenmt = {__index = function() return d end}metas[d] = mtendsetmetatable(t,mt)
end
math库 由一组标准的数学函数构成,包括三角函数(sin、cos、tan、asin、acos等)、指数和对数函数(exp、log、log10)、取整函数(floor、ceil)、max和min、生成伪随机数的函数(random、randomseed),以及变量pi和huge。其中huge为lua可以表示的最大数字 。
table.sort(list, [comp]) 第二个参数是一个排序函数。这个函数需要两个参数,如果希望第一个参数排在第二个前面,返回true。注意sort功能只能是对table的value进行排序而不是key 。
注意,字符串索引从1开始,也可以用负数 , -1表示倒数第一个。
s = "hello world"
i, j = string.find(s, "hello")
print(i,j) -->1 5
print(string.match("hello world","hello")) --> hello
s = string.gsub("lua is cute", "cute", "great")
print(s) --> lua is great
s = "hello world from lua"
for w atch(s, "%a+") doprint(w)
end
-- 打印每个单词
模式可以用于匹配字符串,上节提到的函数都能使用模式,诸如下面这种格式:
-- 检查字符串是否以数字开头
if string.find(s, "^[+-]?%d+$") then ...
下面的字符都是模式里经常会出现的:
%a %c %d %l %p %s %u %w %x %z () . % + - ? [] ^ $
捕获功能可根据一个模式从目标字符串中抽出匹配于该模式的内容。
pair = "name = Anna"
key,value = string.match(pair,"(%a+)%s* = %s(%a+)")
print(key,value) --> name Anna
string.gsub 函数的第三个参数可以是一个函数或table。
I/O 库提供了两套不同风格的文件处理接口。 第一种风格使用隐式的文件句柄; 它提供设置默认输入文件及默认输出文件的操作, 所有的输入输出操作都针对这些默认文件。 第二种风格使用显式的文件句柄。
当使用隐式文件句柄时, 所有的操作都由表 io 提供。 若使用显式文件句柄, io.open 会返回一个文件句柄,且所有的操作都由该文件句柄的方法来提供。
表io常用的函数如下:
操作系统库定义在table os中,其中包含了文件操作数、获取当前日期和时间的函数。
调试库由两类函数组成:自省函数(introoective function)和钩子(hook) 。
主要的自省函数是
debug.setlocal则用来改变局部变量的值。
function foo(a,b)local xdo local c = a - b endlocal a = 1while true dolocal name,value = local(1,a)if not name then break endprint(name,value)a =a + 1end
endfoo(10,20)
打印结果:
a 10
b 20
x nil
a 4
第一个a,b 是函数的参数,没有c是因为执行到getlocal时c已经不在作用域了。
所有自省函数都能接受一个协同程序作为参数。
debug.sethook ([thread,] hook, mask [, count])
将一个函数作为钩子函数设入。 字符串 mask 以及数字 count 决定了钩子将在何时调用。 掩码是由下列字符组合成的字符串,每个字符有其含义:
如果不传入参数, debug.sethook 关闭钩子。
这一节展示了使用debug.sethook 来统计函数调用次数的案例。
CAPI是一组能使C与Lua交互的函数。包括读写Lua全局变量、调用Lua函数、运行一段Lua代码,以及注册C函数以供Lua代码调用。
Lua和C通过一个虚拟栈来进行通信。
lua使用索引来引用栈中的元素,栈底为1,往上递增,栈顶为-1,往下递减。
lua使用C的setjmp机制来处理异常。
lua_error:以栈顶的值作为错误对象,抛出一个 Lua 错误
所有注册到lua中的函数都具有相同的原型,这个时候使用的是局部栈,当lua调用一个C函数时,第一个参数总是这个局部栈的索引1:
typedef int (*lua_CFunction) (lua_State *L);
返回值表示它需要向Lua返回几个值。详细参见:lua_CFunction
void lua_pushcfunction (lua_State L, lua_CFunction f)
将一个 C 函数压栈
void lua_register (lua_State L, const char name, lua_CFunction f)
把 C 函数 f 设到全局变量 name 中
对于lua函数来说,有3种地方可以保存非局部的数据:全局变量、函数环境和非局部变量(closure中)。 CAPI中提供了3种地方来保存这类数据:注册表、环境和upvalue。
注册表位于伪索引 上,这个索引由LUA_REGISTRYINDEX定义。
设置环境的代码如下:
lua_newtable(L);
lua_replace(L, LUA_ENVIRONINDEX);
luaL_register(L, <库名>, <函数列表》);
当创建了一个 C 函数后, 你可以给它关联一些值。首先这些值需要先被压入堆栈(如果有多个值,第一个先压)。 接下来调用 lua_pushcclosure 来创建出闭包并把这个 C 函数压到栈上。 参数 n 表示有多少个值需要关联。 lua_pushcclosure 也会把这些值从栈上弹出。
这一章节展示了用C语言编写新的类型来扩展lua。实现了一个在C语言里定义的布尔数组,然后在lua里可以使用这个类型并且可以调用相关的方法的功能。具体实现的代码比较长,感兴趣自己看书。
注意返回值是void*类型。这个类型的指针可以用任意类型的指针给它赋值。
不只是table可以有元表,userdata同样可以有元表
一种辨别不同类型的userdata的方法,为每种类型创建一个唯一的元表
轻量级userdata 表示一个指针 void*。 它是一个像数字一样的值: 你不需要专门创建它,它也没有独立的元表,而且也不会被收集(因为从来不需要创建)。 只要表示的 C 地址相同,两个轻量用户数据就相等。
lua通过元方法__gc来指定终结函数, 这个元方法只对userdata有效。
在回收一个userdata时,会调用这个元方法,并且把自身作为参数传入。
每当创建一个luastate,lua就会自动在这个状态机中创建一个新线程,这个线程称为“主线程”。主线程永远不会被回收。当调用lua_close关闭状态机才会释放。
除了主线程之外,其他线程一样是垃圾回收的对象。
这个函数会从 from 的栈上弹出 n 个值, 然后把它们压入 to 的栈上。
每次调用luaL_newstate 都会创建一个新的Lua状态机。
本章介绍了对lua的内存机制可以做的一些定制功能。
lua_Alloc定义如下:
typedef void* (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize)
第二个参数是准备分配或释放的内存块地址,第三个参数是内存块的原大小,最后一个参数是要求的内存块大小。
lua的垃圾收集周期由4个阶段组成:标记、整理、清扫、收尾 。
lua提供了API可以控制垃圾收集器的某些行为。
这个函数根据其参数 what 发起几种不同的任务,如停止gc、重启gc、单步gc、返回内存等等操作。
详见:lua_gc 以及 collectgarbage 。
关于作者:
CSDN博客:
知乎专栏:[] ()
游戏同行聊天群:891809847
本文发布于:2024-01-28 15:33:05,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/17064271898427.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |