【Lua篇】《Lua程序设计》全书内容总结

这篇文章是 《Lua程序设计》 的读书笔记和概要。这是关于lua编程最权威的书籍之一。推荐给lua基础不够牢的童鞋。没有看过的可以通过我这篇文章快速浏览书中内容,已经看过的可以也能借助这篇文章复习一遍。另外由于我之前是使用c#的,所以这篇文章也会提到c#和lua的一些不同点。
全书内容分为4个部分:

  • 第1章到第10章,讲了基础数据类型、函数、闭包、协程、错误处理。这一部分的内容属于最基础的语法,只要学过其它语言,看这部分毫无压力。
  • 第11章到第17章,全书最重要的部分。讲了表、元表元方法、环境、模块与包、面向对象、弱引用。这部分内容属于lua的高级功能,其中的表和元表我认为是lua最独特的地方。
  • 第18章到第23章, 讲了数学库、Table库、字符串库、IO库、操作系统库、Debug库。属于lua标准库里的内容,大部分都在讲库函数如何使用,只需要做了解即可。里面最有用的个人认为是数学库、Table库、Debug库。其它的用到再查就可以了。
  • 第24章到第31章,讲了CAPI、从Lua调C、从C调Lua、给Lua扩展userdata、内存管理。这部分属于其它语言和lua之间交互的内容。如果需要给项目接入lua或者想看懂xlua、tolua这种lua插件,那么这部分内容也是必看的。

在这里插入图片描述


目录


第1章 开始

1.1 程序块

程序块,即chunk,由一行或多行lua可执行的代码构成。下面两段代码,一个是程序块一个不是。

-- 是程序块
function f(a,b)
    return a*a - b*b;
end

-- 不是程序块
do 
    return 1;

一般使用dofilerequire来执行程序块。

1.2 词法规范

lua变量不能以数字开头,也要避免以下划线开头。lua中的一些特殊变量就是以下划线开头的。也不要使用and、break、do、else、elseif等关键字做变量。

1.3 全局变量

不加local的变量就是全局变量,不需要全局变量了把它设为nil即可。

1.4解释器程序

讲了在UNIX系统中怎么把lua当成脚本解释器来用。


第2章 类型与值

2.1 nil(空)

nil主要功能在于区别其他任何值。

2.2 boolean(布尔)

注意boolean不是一个条件值的唯一表示方式。false和nil都是假,其他情况都是真。

2.3 number(数字)

lua没有整数类型,都是使用双精度浮点数。

2.4 string(字符串)

字符串机制和c#类型,修改字符串变量时,是创建了一个新的字符串。字符串和数字之间可以自动转换。
lua中使用 两个点来连接字符串。如:

print(10..20)
2.5 table(表)

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)
2.6 function(函数)

lua既可以调用lua编写的函数,也能调用以c编写的函数。第5章、第26章将详细讨论。

2.7 userdata(自定义类型)和thread(线程)

在其他语言编写的数据类型传到lua里面就是自定义类型,如unity里的transform、component等。线程会在第9章解释。


第3章 表达式

3.1 算术操作符

算术操作符和其他编程语言差别不大,除了加减乘除指数 +、-、*、/、^ 外还有 % 取模。当%用于非整数时表示小数点后几位的数据。比如x%1结果就是x的小数部分,x%0.01表示小数点后2位以后的数。x-x%0.01表示x精确到小数点后两位的结果。

3.2 关系操作符

< > <= >= == ~=
对于table,userdata,函数使用引用比较。

3.3 逻辑操作符

逻辑操作符有and、or、not。and和or都有短路求值。即第一个操作数满足了才会评估第二个操作数。

3.4 字符串连接

上面已经提到了字符串相关知识,这里不再赘述。

3.5 优先级

记住操作符优先级并没有什么意义,使用括号指定运算顺序即可。

3.6 table构造式

使用以下语法构造一个table:

a = {
    
     x = 10, y =20}
a = {
    
    }
a = {
    
    10,20}

在没有指定key时,会自动使用数字进行索引,并且以1开头


第4章 语句

4.1 赋值

lua允许多重赋值。例如:

a,b = 10, 20
-- 交换x与y
x,y = y,x

多重赋值还能用来接收函数的多个返回值:

a,b = f()
4.2 局部变量与块

lua中使用local创建局部变量,局部变量会随着作用域的结束而消失。在lua中,有一种推荐写法:

local foo = foo

这句代码创建了局部变量foo,并把全局变量foo赋值给它。这种写法两个好处:

  • 如果后面代码修改了全局foo,也能拿到修改前的值
  • 加速当前作用域对foo的访问
4.3 控制结构
  • if then else end
  • while do
  • repeat until
  • 数字型for
for i=1,10 do print(i) end
  • 泛型for。ipair按照索引升序遍历,当key不是整数时返回,当key不等于索引时返回
for i,v in ipairs(a) do print(v) end
4.4 break与return

break与return用于跳出当前的块。


第5章 函数

函数的冒号操作符调用table的函数时将table隐含地作为函数的第一个参数。o.foo(o,x)另一种写法是o:foo(x)

5.1 多重返回值

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))
5.2 变长参数

lua中的函数可以传入不定数量的实参。使用3个点来表示,这种参数叫作变长参数

function foo(...)
local a,b,c = ...
end

如果有固定参数,固定参数必须放在变长参数之前。

5.3 具名实参

先来回顾一下c#的缺省参数:

private void MyFunc( int a = 0 ,int b = 0int c = 0)
{
    
    
}

c#里可以不按照参数的顺序来使用函数

MyFunc( b : 10, a : 10 )

lua不直接支持这种语法,但是可以通过将函数参数设为table,来实现这种机制。

function rename (arg)
return os.rename(arg.old,arg.new)
end
rename{
    
    old = "temp.lua",new = "temp1.lua"}

当实参只有一个table时,可以省略圆括号


第6章 深入函数

在lua中,函数是一种第一类值。这个概念的意思是函数和其他类型的值具有相同的权利,即可以传参,可以作为返回值。
函数和其他值一样是匿名的,一个函数定义其实是一条赋值语句。如下面两种写法是等价的:

function foo(x) return 2*x end
foo = function(x) return 2*x end

除了第一类值, lua中函数的另一个特征是有词法域。这是指当一个函数嵌套在另一个函数时,内部函数可以访问外部函数的局部变量。这两种特征都是c#没有的,c#中只能使用委托和匿名委托来实现类似的机制。

6.1 closure(闭包)

先来看以下代码:

function newCounter()
local i = 0
retuan function()
		i = i+1
		return 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
6.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 1
	else return n * fact(n-1)
	end
end

这是正确写法

local fact
fact = function(n)
if n == 0 then return 1
	else return n * fact(n-1)
	end
end

这也是正确写法

local function fact (n)
if n == 0 then return 1
	else return n * fact(n-1)
	end
end
6.3 正确的尾调用

尾调用就是指一个函数的最后一步是return另一个函数
如下就是一个尾调用

function f(x) return g(x) end

而下面代码不是尾调用

function f(x) g(x) end

出现尾调用后,程序不会保存尾调用所在的函数的栈信息,因为没有必要。这种现象称为尾调用消除。基于这种机制,尾调用永远不会导致栈溢出
尾调用的典型应用就是状态机。用一个函数来表示一个状态。


第7章 迭代器和泛型for

7.1 迭代器和closure

在讲系统自带的迭代器函数前,先来看一下我们自己应该如何手写迭代器函数:

function values(t)
	local i = 0
	return function() i = i + 1; return t[i] end
end

t = {
    
    10,20,30}
for element in values(t) do
	print(element)
end

for循环会在values(t)返回的nil时停止。迭代器函数就是利用闭包的可以访问非局部变量的特性实现的。

7.2 泛型for的语义

使用for in do 写法的for循环是泛型for。
ipairs、pairs 这两个东西本质上就是一个系统帮我们写好的迭代器函数

for k,v in ipairs(t) do print(k,v) end
7.3 无状态的迭代器

7.1的例子有一个缺陷,就是在函数value里每次都会返回一个新的闭包。泛型for在in后面的表达式实际上可以返回3个值。分别是迭代器函数、一个恒定状态值、一个控制变量。这样的话就能保存迭代器函数了。
先来看ipirs的实现原理:

local function iter(a,i)
	i = i + 1
	local v = a[i]
	if v then 
		return i,v
	end
end

function 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这种,迭代器函数内部不保存状态,每次迭代时根据恒定状态值和控制变量决定下次的元素的的迭代器叫做无状态的迭代器。

7.4 具有复杂状态的迭代器

与上面相对的,就是迭代器内部保存状态的复杂迭代器 。使用lua的闭包特性可以轻易做到这点。

7.5 真正的迭代器

这一节书上展示了不使用for如何进行迭代。这个知识对于for语句的老版本lua有用。对如今的开发意义不大。


第8章 编译、执行与错误

8.1 编译

loadfiledofile都是从一个文件加载lua代码块,但是不会运行它们
区别在于loadfile不会引发错误,如果出错会返回nil。
loadstringloadfile类似,不同在于它是从字符串读取代码。

f = loadstring("i = i + 1")
f = function() i = i + 1 end

这两行代码是等价的,区别在于loadstring开销更大。还有loadstring不涉及词法域。就是说它不能读取局部变量

8.2 C代码

lua可以通过动态链接调用c里面的代码。核心函数是package.loadlib。需要两个参数,动态库的路径和函数名称。案例:

local path = '/usr/local/lib/lua/5.1/socket.so"
local f = package.loadlib(path, "luaopen_socket")
8.3 错误(error)

lua内部提供了assert函数来帮助检查错误。第一个参数是bool,第二个参数是可选的字符串

n = io.read()
assert(tonumber(n),"invalid input:"..n.."is not a number")
8.4 错误处理和异常

如果要在lua处理错误,使用pcall来包装代码。类似c#的try、catch。

if pcall(foo) then
-- 如果没有错误
<常规代码>
else 
-- 如果有错误
<错误处理代码>
end
8.5 错误消息和追溯

在遇到错误时,lua会显示错误信息,可以通过给error函数传入字符串来更改错误信息:

local status,err = pcall(function() a = "a" + 1 end)
print(err)	-- stdin:1:attempt to perform arithmetic on a string value

local status,err = pcall(function() error("my error") end)
print(err)	-- stdin:1:my error 

error函数还可以传入第二个参数表示由调用层级的哪一层来报告错误
pcall有一个不足:在返回错误消息的时候,pcall到错误消息的这部分调用栈已经被销毁了
所以lua提供了xpcall。可以传入第二个参数即一个错误处理函数。在这个函数里面可以使用debug.traceback来得到完整调用栈。


第9章 协同程序(coroutine)

9.1 协同程序基础

使用coroutine.create创建一个协程,返回值为thread类型:

co = coroutine.create(function() print("hi") end)

协程有四种状态:挂起(suspended)、运行(running)、死亡(dead)和正常(normal)
前三个都好理解。协程刚create或者内部调用yield 都会把协程变成挂起状态。代码全部执行完了就是死亡。
当协程A唤醒协程B的时候,A的状态就是正常

  • 使用coroutine.status来获得状态。
  • 使用coroutine.yield可以挂起协程,还能传入值作为协程返回值。
  • 使用coroutine.resume来启动或恢复协程的执行,将其状态由挂起改为运行。 返回2个值,第一个是true或者false表示协程是否运行正常,第2个返回值是yield的值。
co = coroutine.create(function(a,b)
	coroutine.yield(a + b, a - b)
	end)
print(coroutine.resume(co,20,10)) --> true 30 10 

书中还提到了lua的协程是一种非对称协程非对称协程在挂起时会把控制权给它的调用者。而对称式协程在挂起时会把控制权给其它对称式协程

9.2 管道(pipe)与过滤器(filter)

这一节举了一个消费者和生产者的例子来说明协程的使用,并且和UNIX的pipe进行了比较,感兴趣的自己看书。

9.3 以协同程序实现迭代器

这一节用协程实现了一个迭代器,打印全排列。输出全排列是一道经典的递归算法题,所以把代码贴了上来:

-- 用协程和递归实现全排列
function permgen(a,n)
	n = n or #a
	if n <=1 then 
		coroutine.yield(a)
	else
		for i = 1, n do
			a[n],a[i] = a[i], a[n]
			permgen(a,n-1)
			a[n],a[i] = a[i], a[n]
		end
	end
end
-- 迭代器函数
function permutations(a)
	local co = coroutine.create(function() permgen(a) end)
	return function()
		local code,res = coroutine.resume(co)
		return res
	end
end
-- 打印
function printResult(a)
	for i = 1,#a do
		io.write(a[i]," ")
	end
	io.write("\n")
end
-- 开始循环打印结果
for p in permutations{
    
    "a","b","c"} do
	printResult(p)
end

lua中可以使用coroutine.wrap来代替permutations函数里的内容。它也能创建一个协程,不同的是,它返回一个函数,每次调用这个函数,相当于是调用了一次coroutine.resume

function permutations(a)
	return coroutine.wrap(function() permgen(a) end)
end
9.4 非抢先式的(non-preemptive)多线程

虽然书上写了lua的协程是一种多线程。但是它跟真正的多线程又有所不同。所以又叫它非抢先式的多线程。
传统抢先式的多线程下,操作系统会规定每个线程每次的执行时间,到时间了就把cpu给其它线程用。而非抢先式一个线程占用多少时间它自己说了算。
这节剩下篇幅又是一个很多代码的案例,写的是一个http下载多个文件的例子。


第10章 完整的示例

这一章节书中讲解了两个完整的程序案例,一个是将table读成html,一个是马尔可夫链。感兴趣自己看书。


第11章 数据结构

这一章的内容是讲用table实现各种数据结构。

11.1 数组

实现数组的几种写法:

-- 方式1
a = {
    
    }
for i = 1, 100 do
	a[i] = 0
end
-- 方式2
a = {
    
    1, 4, 9, 16}
-- 方式3
a = {
    
    [1] = 1 , [2] = 4}

另外,使用 长度操作符# 可以获得数组大小。
另外注意索引可以是负数

print(#a)
11.2 矩阵和多维数组

方法1:在table里再套一个table:

mt = {
    
    }
for i = 1,N do
	mt[i] = {
    
    }
	for j = 1,M do
		mt[i][j] = 0
	end
end

方法2:把两个索引合并为一个索引:

mt = {
    
    }
for i = 1, N do
	for j = 1,M do
		mt[(i-1)*M+j] = 0
	end
end
11.3 链表

每个节点是一个table,里面存有next和value。

11.4 队列和双向队列

用两个索引,表示首尾的位置。

List =  {
    
    }
function List.new()
	return {
    
    first = 0, last = -1}
end
-- 往队首添加元素
function List.pushfirst(list, value)
	local first = list.first - 1
	list.first = first
	list[first] = value
end

pushfirst函数有点绕,我举个例子:

  • 新建队列 {first = 0, last = -1}
  • 往队首加一个元素后,first得变成-1
  • 然后-1对应的元素是函数里的参数value。即list[-1] = value
  • 最后队列里的元素应该长这样:{first = -1 , last = -1, [-1] = value}

理解了这个,往队尾添加元素,从队首弹出元素,从队尾弹出元素都是同一个道理了。

11.5 集合与无序组

这一节展示了lua可以把字符串当索引的功能,在查找某个字符是否在集合里很方便:

t = {
    
    ["while"] = true , ["end"] = true}
11.6 字符串缓冲

这一节展示了如何读取多行字符串,因为lua字符串和c#一样都是不可变的,如果每读一行都拼接上一次的结果效率会极其低下。
好的做法是把每一行存起来,最后使用table.concat

local t = {
    
    }
for line in io.line() do
	t[#t + 1] = line.."\n"
end
local s = table.concat(t)
11.7 图

每个节点为一个table,table有两个字段:节点的名称和此节点相邻的节点集合。


第12章 数据文件与持久性

这一章展示了如何将lua作为数据文件,以及序列化lua。

12.1 数据文件

将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)
12.2 序列化(Serialization)

需要使用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的名称。


第13章 元表(metatable)与元方法(metamethod)

13.1 算术类的元方法

通过设置元方法可以让表具有加减乘除等算术操作。以加法 __add 为例:

local mt = {
    
    }
Set = {
    
    }
function Set.new(l)
	local set = {
    
    }
	setmetatable(set,mt)
	for _,v in ipairs(l) do set[v] = true end
	return set
end

function Set.union(a,b)
	local res = Set.new()
	for k in pairs(a) do res[k] = true end
	for k in pairs(b) do res[k] = true end
	return res
end

mt.__add = Set.union
s1 = Set.new(10,20,30)
s2 = Set.new(30,1)
s3 = s1 + s2

如果两个表的元表不一样,lua会优先以第一个表的元表查找__add方法。如果没有再找第二个,都没有就报错
元表支持的各种算术操作符如下: __add、__mul、__sub、__div、__unm(相反数)、__mod(取模)、__pow(乘幂)

13.2 关系类的元方法

除了算术运算符,还有关系运算符,元方法为:__eq(等于)、__lt(小于)、__le(小于等于)

13.3 库定义的元方法

这一节介绍了 __tostring__metatable 的用法。
当调用print的时候会调用tostring函数。下面两个print是等价的:

a = 1;
print(a)
print(tostring(a))

而如果tostring里面是一个表,会调用表的 __tostring 元方法。
__metatable 用于保护元表。接上一节的例子

mt.__metatable = "error発生"
s1 = Set.new()
print(getmetatable(s1))  --> error発生
setmetatable(s1,{
    
    }})
-- stdin:1: cannot change protected metatable
13.4 table访问的元方法

1. __index元方法
当访问一个table不存在的元素时,会访问__index的元方法。

  • 如果__index的元方法是一个函数,会以table和访问的key调用这个函数。
  • 如果__index的元方法是一个表,会以访问的key去查找这个表里。

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.___ = d
	setmetatable(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

第14章 环境

14.1 具有动态名字的全局变量

获取全局变量和赋值全局变量:

value = _G[varname]
_G[varname] = value

如果变量名是类似"a.b.c.d"的情况应该这么写:

function getfield(f)
	local v = _G
	for w in string.gmatch(f,"[%w_]+") do 
		v = v[w]
	end
	return v
end

string.gmatch 里面的参数用到了字符串模式,功能类似正则表达式,用来匹配字符串,有着复杂的规则。平时用不着,需要的时候再看:lua中的字符串操作模式

14.2 全局变量声明

由于lua将全局变量存放在一个普通的table里,所以可以通过元表来改变访问全局变量的行为

setmetatable(_G,{
    
    
	__newindex = function(t,n,v)
		local w = debug.getinfo(2,"S).what
		if 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
	})

这里面用到了debug.getinfo,__newindex里面几行代码的作用是判断是不是主线程调用,如果不是则报错,如果是则设置全局变量。 关于getinfo里的参数,第23章会详细说明。

14.3 非全局的环境

lua里存放全局变量的表默认也是全局的,修改全局变量将会影响其他函数。但是可以使用setfenv来改变当前函数存放全局变量的表(也叫当前函数的环境)。 setfenv第一个参数为数字或函数,为数字时,表示第几层的函数,第二个函数为表。

a = 1
setfenv(1,{
    
    })
print(a)	--> 会报错,print为nil

上面这个例子改变了环境,所以之前能使用的全局变量或函数全部不能使用了。
注意每个闭包也有自己的环境

function factory()
	return function()
		return a
	end
end

a = 3
f1 = factory()
f2 = factory()
setfenv(f1, {
    
    a = 10})
print(f1())		--> 10
print(f2())		--> 3

第15章 模块与包

15.1 require函数

require用于加载模块,返回由模块函数组成的table,如果已经加载过,会保存在package.load[name] 里。

local m = require "io"
m.write("hello world\n")

require用于搜索lua文件的路径放在package.path中。

15.2 编写模块的基本方法

为了后期更改模块名的方便,不建议定义函数的时候使用真正的模块名。

local M = {
    
    }
complex = M
M.i = {
    
    r =0,  i = 1}
function M.new(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
15.3 使用环境

使用环境可以改善写模块经常出现的两个问题:

  • 忘记加local导致局部函数变成全局函数
  • 调用模块内的公有函数时需要写前缀 比较麻烦
local modname = ...
local M = {
    
    }
_G[modname] = M
package.loaded[modname] = M
setfenv(1, M)

声明函数不再需要前缀时 就会变成modname.xxx:

function add(c1,c2)
	return new(c1.r + c2.r, c1.i + c2.i)
end

上述的写法会产生无法调用原先的全局变量的缺点,解决方案有三:

  • 使用元表setmetatable(M, {__index = _G})
  • _G 保留起来,调用全局变量时增加 _G 前缀
  • 提前声明需要调用的函数或者模块
local sqrt = math.sqrt
local io = io
setfenv(1,M)
15.4 module函数

使用module(…) 可以代替下面这几行代码:

local modname = ...
local M = {
    
    }
_G[modname] = M
package.loaded[modname] = M
setfenv(1, M)

使用module(…,package.seeall) 相当于上面的代码加上:

setmetatable(M,{
    
    _index == _G})
15.5 子模块和包

在定义模块名时可以加点,mod.submod的一个子模块。一个包是一个完整的模块树。执行require搜索文件时,会自动把模块名中的点转为分割符:

./?.lua
-- require "a.b"
./a/b.lua

注意:

  • 加载子模块a.b时不会自动加载a
  • 加载模块a时也不会自动加载子模块a.b

第16章 面向对象编程

在定义函数时,建议使用self

function Account.withdraw (self, v)
	self.balance = self.balance - v
end

有两个好处:

  • 模块名发生变动时函数不受影响
  • 可以调用有相同方法的其他对象

调用时使用冒号语法隐藏self

Account:withdraw(100,100)
16.1 类

使用元表和__index的机制可以实现类实例:

function Account:new(o)
	o = o or {
    
    }
	setmetatable(o,self)
	self.__index = self
	return o
end
16.2 继承

继承的实现:

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
16.3 多重继承

多重继承实现的关键在于不能将基类作为子类的元表

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 = {
    
    ...}
	setmetatable(c,{
    
    __index = function(t,k)
		return search(k,parents)
	end})
	
	function c:new(o)
		o = o or {
    
    }
		setmetatable(o,{
    
    __index = c})
		return o
	end
return c
end

多重继承由于需要查找父类,性能上不如单一继承,改进的方法之一是将继承的方法复制到子类中, 缺点是子类不能修改方法的定义。

16.4 私密性

在lua里实现私密性的做法:
使用两个table来表示一个对象,一个table用于存储对象的状态,另一个用于操作

16.5 单一方法(single-method)做法

当一个对象只有一个方法时,可以不用创建table,而是将这个单独的方法作为对象来表示返回


第17章 弱引用table

在lua不会回收可访问table中作为key或者value的对象 。除非使用弱引用。 另外像数字和布尔这样的值类型是不会被回收的
__mode含有"v"表示表arr里的value是弱引用,含有"k"表示arr里的key是弱引用,含有"kv"则两者兼有。意思是当value或者key为nil时,GC之后表arr里不再拥有引用

  t1,t2 = {
    
    },{
    
    }
  arr = {
    
    }
  arr[1] = t1
  arr[2] = t2
  t1 = nil
  
  setmetatable(arr,{
    
    __mode = "v"})
  collectgarbage()
  for k,v in pairs(arr) do
  	print(k,v)
  end
  -- 只有1个t2,t1已经被回收
17.1 备忘录(memoize)函数

备忘录函数的作用就是把经常调用的数据缓存起来,用空间换时间。这种函数就很适合使用弱引用。这个时候需要把value作为弱引用

17.2 对象属性

在lua里,table也是可以作为key的 。 当table作为key时,也需要使用弱引用,不然这个table也不会被回收,这种时候就需要 把key作为弱引用

17.3 回顾table的默认值

弱引用还有一个应用就是设置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] = d
	setmetatable(t,mt)
end

当然也可以将默认值作为key,table作为value :

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

第18章 数学库

math库 由一组标准的数学函数构成,包括三角函数(sin、cos、tan、asin、acos等)、指数和对数函数(exp、log、log10)、取整函数(floor、ceil)、max和min、生成伪随机数的函数(random、randomseed),以及变量pi和huge。其中huge为lua可以表示的最大数字


第19章 table库

19.1 插入和删除
  • table.insert(list, [pos], value) 中间的pos如果不填就是插入到table末尾
  • table.remove(list, [pos])
19.2 排序

table.sort(list, [comp]) 第二个参数是一个排序函数。这个函数需要两个参数,如果希望第一个参数排在第二个前面,返回true。注意sort功能只能是对table的value进行排序而不是key

19.3 连接

table.concat(list) 接受一个字符串数组,进行连接并且返回结果。


第20章 字符串库

20.1 基础字符串函数
  • string.len(s) 返回s的长度
  • strnig.rep(s,n) 返回字符串s重复n次的结果
  • string.lower(s) 和 string.upper(s) 大小写转换
  • string.sub(s,i,j) 截取s的第i个到第j个字符
  • strnig.char和strnig.byte 转换字符和内部数值表示
  • strnig.format 格式化字符

注意,字符串索引从1开始,也可以用负数 , -1表示倒数第一个。

20.2 模式匹配函数
  • string.find 搜索字符串返回索引
s = "hello world"
i, j = string.find(s, "hello")
print(i,j)	-->1  5
  • string.match 搜索字符串返回匹配的子字符串
print(string.match("hello world","hello"))	--> hello
  • string,gsub 替换字符串
s = string.gsub("lua is cute", "cute", "great")
print(s)	--> lua is great
  • string.gmatch(s, pattern) 返回一个迭代器函数,以模式对s做匹配
s = "hello world from lua"
for w in string.gmatch(s, "%a+") do
	print(w)
end
-- 打印每个单词
20.3 模式

模式可以用于匹配字符串,上节提到的函数都能使用模式,诸如下面这种格式:

-- 检查字符串是否以数字开头
if string.find(s, "^[+-]?%d+$") then ...

下面的字符都是模式里经常会出现的:
%a %c %d %l %p %s %u %w %x %z () . % + - ? [] ^ $

20.4 捕获

捕获功能可根据一个模式从目标字符串中抽出匹配于该模式的内容。

pair = "name = Anna"
key,value = string.match(pair,"(%a+)%s* = %s(%a+)")
print(key,value)	--> name Anna
20.5 替换

string.gsub 函数的第三个参数可以是一个函数或table。

  • 是函数时,调用的参数就是捕获到的内容,函数的返回值作为替换的字符串
  • 是table时,将捕获到的内容作为key,查看table,将对应的value作为替换的字符串

第21章 I/O库

I/O 库提供了两套不同风格的文件处理接口。 第一种风格使用隐式的文件句柄; 它提供设置默认输入文件及默认输出文件的操作, 所有的输入输出操作都针对这些默认文件。 第二种风格使用显式的文件句柄。

当使用隐式文件句柄时, 所有的操作都由表 io 提供。 若使用显式文件句柄, io.open 会返回一个文件句柄,且所有的操作都由该文件句柄的方法来提供。

表io常用的函数如下:

  • io.close
  • io.read
  • io.closet
  • io.open

第22章 操作系统库

操作系统库定义在table os中,其中包含了文件操作数、获取当前日期和时间的函数。


第23章 调试库

调试库由两类函数组成:自省函数(introoective function)和钩子(hook)

23.1 自省机制

主要的自省函数是debug.getinfo 。第一个参数是调用层数或者函数,第二个参数用来设置要获得的信息,可以设置如下几种字符:

  • ’n’ : 填充 name 及 namewhat 域;
  • ’S’: 填充 source , short_src , linedefined , lastlinedefined ,以及 what 域;
  • ’l’: 填充 currentline 域;
  • ’t’: 填充 istailcall 域;
  • ’u’: 填充 nups, nparams,及 isvararg 域;
  • ’f’: 把正在运行中指定层次处函数压栈;
  • ’L’: 将一张表压栈,这张表中的整数索引用于描述函数中哪些行是有效行。 (有效行指有实际代码的行,即你可以置入断点的行。 无效行包括空行和只有注释的行。)
23.1.1 访问局部变量

debug.getlocal 用来检查任意活动函数的局部变量。第一次参数是层数,第二个参数是索引。
debug.setlocal则用来改变局部变量的值。

function foo(a,b)
	local x
	do local c = a - b end
	local a = 1
	while true do
		local name,value = debug.getlocal(1,a)
		if not name then break end
		print(name,value)
		a =a + 1
	end
end
		
foo(10,20)

打印结果:
a 10
b 20
x nil
a 4

第一个a,b 是函数的参数,没有c是因为执行到getlocal时c已经不在作用域了。

23.1.2 访问非局部的变量
  • debug.getupvalue (f, up)
    此函数返回函数 f 的第 up 个上值的名字和值
  • debug.setupvalue (f, up, value)
    这个函数将 value 设为函数 f 的第 up 个上值
23.1.3 访问其他协同程序

所有自省函数都能接受一个协同程序作为参数。

23.2 钩子

debug.sethook ([thread,] hook, mask [, count])
将一个函数作为钩子函数设入。 字符串 mask 以及数字 count 决定了钩子将在何时调用。 掩码是由下列字符组合成的字符串,每个字符有其含义:

  • ‘c’: 每当 Lua 调用一个函数时,调用钩子;
  • ‘r’: 每当 Lua 从一个函数内返回时,调用钩子;
  • ‘l’: 每当 Lua 进入新的一行时,调用钩子。
    此外, 传入一个不为零的 count , 钩子将在每运行 count 条指令时调用。

如果不传入参数, debug.sethook 关闭钩子。

23.3 性能剖析

这一节展示了使用debug.sethook 来统计函数调用次数的案例。


第24章 CAPI概述

CAPI是一组能使C与Lua交互的函数。包括读写Lua全局变量、调用Lua函数、运行一段Lua代码,以及注册C函数以供Lua代码调用。
Lua和C通过一个虚拟栈来进行通信。

24.1 第一个示例
  • luaL_newstate 创建状态机
  • luaL_openlibs 打开标准库
  • lua_pcall 调用代码块
  • luaL_loadbuffer 加载代码块
  • lua_pop 弹出元素
24.2 栈
  • 压入元素:lua_push*
  • 检查栈空间:lua_checkstack
  • 检查元素:lua_is* 。注意lua_isnumber是检查值能否转为数字类型,lua_isstring同理。
  • 获取元素:lua_to*
  • 其他栈操作:
    lua_gettop、lua_settop、lua_pushvalue、lua_remove、lua_insert、lua_replace

lua使用索引来引用栈中的元素,栈底为1,往上递增,栈顶为-1,往下递减。

24.3 CAPI中的错误处理

lua使用C的setjmp机制来处理异常。

24.3.1 应用程序代码中的错误处理
  • 使用panic 函数,只要不返回就可以不退出程序
  • 让代码在“保护模式”下运行
24.4.2 库代码中的错误处理

lua_error:以栈顶的值作为错误对象,抛出一个 Lua 错误


第25章 扩展应用程序

25.1 基础
  • lua_getglobal 获取全局变量,入栈
25.2 table操作
  • lua_gettable 获得索引处的table
  • lua_getfield 获得索引处的table的某个key对应的值
  • lua_settable
  • lua_setfield
25.3 调用Lua函数
  • lua_getglobal 压入函数
  • lua_push* 压入参数
  • lua_pcall 完成调用
  • lua_is* 检查结果

第26章 从Lua调用C

26.1 C函数

所有注册到lua中的函数都具有相同的原型,这个时候使用的是局部栈,当lua调用一个C函数时,第一个参数总是这个局部栈的索引1

typedef int (*lua_CFunction) (lua_State *L);

返回值表示它需要向Lua返回几个值。详细参见:lua_CFunction

void lua_pushcfunction (lua_State L, lua_CFunction f)
将一个 C 函数压栈

26.2 C模块

void lua_register (lua_State L, const char name, lua_CFunction f)
把 C 函数 f 设到全局变量 name 中


第27章 编写C函数的技术

27.1 数组操作
  • int lua_rawgeti (lua_State *L, int index, int key);
    把 t[key] 的值压栈, 这里的 t 是指给定索引处的表,不触发元方法
  • void lua_rawseti (lua_State L, int index, int key) ;
    等价于 t[key] = v , 这里的 t 是指给定索引处的表, 而 v 是栈顶的值。
    这个函数会将值弹出栈。 赋值是直接的;即不会触发元方法。
27.2 字符串操作
  • const char *lua_pushlstring (lua_State *L, const char *s, size_t len);
    把指针 s 指向的长度为 len 的字符串压栈
  • const char *lua_pushfstring (lua_State *L, const char *fmt, …);
    把一个格式化过的字符串压栈, 然后返回这个字符串的指针。
  • void lua_concat (lua_State *L, int n);
    连接栈顶的 n 个值, 然后将这些值出栈,并把结果放在栈顶
27,3 在C函数中保存状态

对于lua函数来说,有3种地方可以保存非局部的数据:全局变量、函数环境和非局部变量(closure中)。 CAPI中提供了3种地方来保存这类数据:注册表、环境和upvalue。

27.3.1 注册表(registry)

注册表位于伪索引 上,这个索引由LUA_REGISTRYINDEX定义。

  • int luaL_ref (lua_State L, int t)
    L传入luaState的指针,t传入LUA_REGISTRYINDEX。这个函数的作用是弹出栈顶的值,并且用一个新分配的整数key把这个值注册到注册表里,然后返回这个整数key。这个key被称为"引用"。
  • int lua_rawgeti (lua_State L, int index, lua_Integer n)
    L传入luaState的指针,index传入LUA_REGISTRYINDEX,n传入上一个函数返回的整数key。作用是把注册表里的key对应的值压栈
  • void luaL_unref (lua_State L, int t, int ref)
    L传入luaState的指针,t传入LUA_REGISTRYINDEX,ref传入luaL_ref返回的整数key。
27.3.2 C函数的环境

设置环境的代码如下:

lua_newtable(L);
lua_replace(L, LUA_ENVIRONINDEX);
luaL_register(L, <库名><函数列表》);
  • void lua_replace (lua_State L, int index);
    把栈顶元素放置到给定位置而不移动其它元素 (因此覆盖了那个位置处的值),然后将栈顶元素弹出。
27.3.3 upvalue
  • void lua_pushcclosure (lua_State L, lua_CFunction fn, int n);
    把一个新的 C 闭包压栈。

当创建了一个 C 函数后, 你可以给它关联一些值。首先这些值需要先被压入堆栈(如果有多个值,第一个先压)。 接下来调用 lua_pushcclosure 来创建出闭包并把这个 C 函数压到栈上。 参数 n 表示有多少个值需要关联。 lua_pushcclosure 也会把这些值从栈上弹出。


第28章 用户自定义类型

这一章节展示了用C语言编写新的类型来扩展lua。实现了一个在C语言里定义的布尔数组,然后在lua里可以使用这个类型并且可以调用相关的方法的功能。具体实现的代码比较长,感兴趣自己看书。

28.1 userdata
  • void* lua_newuserdata (lua_State *L, size_t size);
    这个函数分配一块指定大小的内存块, 把内存块地址作为一个完全用户数据压栈, 并返回这个地址
  • void* lua_touserdata (lua_State *L, int index);
    如果给定索引处的值是一个完全用户数据, 函数返回其内存块的地址。

注意返回值是void*类型。这个类型的指针可以用任意类型的指针给它赋值。

28.2 元表

不只是table可以有元表,userdata同样可以有元表
一种辨别不同类型的userdata的方法,为每种类型创建一个唯一的元表

  • int luaL_newmetatable (lua_State *L, const char *tname);
    创建一个新的table用作元表,并将其压入栈顶,然后将这个table与注册表中的指定名称关联。
  • int luaL_getmetatable (lua_State *L, const char *tname);
    将注册表中 tname 对应的元表 压栈
  • void *luaL_checkudata (lua_State *L, int arg, const char *tname);
    检查栈中指定位置上是否为一个userdata
28.3 面向对象的访问
28.4 数组访问
28.5 轻量级userdata
  • void lua_pushlightuserdata (lua_State L, void p);
    把一个轻量级userdata压栈。

轻量级userdata 表示一个指针 void*。 它是一个像数字一样的值: 你不需要专门创建它,它也没有独立的元表,而且也不会被收集(因为从来不需要创建)。 只要表示的 C 地址相同,两个轻量用户数据就相等。


第29章 管理资源

lua通过元方法__gc来指定终结函数, 这个元方法只对userdata有效。
在回收一个userdata时,会调用这个元方法,并且把自身作为参数传入。


第30章 线程和状态

30.1 多个线程

每当创建一个luastate,lua就会自动在这个状态机中创建一个新线程,这个线程称为“主线程”。主线程永远不会被回收。当调用lua_close关闭状态机才会释放。

  • lua_State lua_newthread (lua_State L) ;
    创建一条新线程,并将其压栈, 并返回维护这个线程的 lua_State 指针。 这个函数返回的新线程共享原线程的全局环境, 但是它有独立的运行栈。

除了主线程之外,其他线程一样是垃圾回收的对象。

  • void lua_xmove (lua_State from, lua_State to, int n)
    交换同一个状态机下不同线程中的值。

这个函数会从 from 的栈上弹出 n 个值, 然后把它们压入 to 的栈上。

30.2 Lua状态机

每次调用luaL_newstate 都会创建一个新的Lua状态机。

  • 不同状态机完全独立,不共享数据
  • 所有交换的数据必须经由C代码中转,所以只能交换可以在C语言中表示的类型

第31章 内存管理

本章介绍了对lua的内存机制可以做的一些定制功能。

31.1 分配函数
  • lua_State lua_newstate (lua_Alloc f, void ud);
    参数 f 是一个分配器函数; Lua 将通过这个函数做状态机内所有的内存分配操作。 第二个参数 ud ,这个指针将在每次调用分配器时被转入。

lua_Alloc定义如下:

typedef void* (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize)

第二个参数是准备分配或释放的内存块地址,第三个参数是内存块的原大小,最后一个参数是要求的内存块大小。

31.2 垃圾收集器

lua的垃圾收集周期由4个阶段组成:标记、整理、清扫、收尾
lua提供了API可以控制垃圾收集器的某些行为。

  • int lua_gc (lua_State L, int what, int data);
    控制垃圾收集器。

这个函数根据其参数 what 发起几种不同的任务,如停止gc、重启gc、单步gc、返回内存等等操作。
详见:lua_gc 以及 collectgarbage


关于作者:

  • 水曜日鸡,简称水鸡,ACG宅。曾参与索尼中国之星项目研发,具有2D联网多人动作游戏开发经验。

CSDN博客:https://blog.csdn.net/j756915370
知乎专栏:[https://zhuanlan.zhihu.com/c_1241442143220363264] (https://zhuanlan.zhihu.com/c_1241442143220363264)
游戏同行聊天群:891809847

猜你喜欢

转载自blog.csdn.net/j756915370/article/details/106970587