文章目錄
  1. 1. Lua
  2. 2. Lua Study
    1. 2.1. Weak Tables And Finalizers
    2. 2.2. Lua里的位运算
      1. 2.2.1. 取反
      2. 2.2.2. 移位和异或
    3. 2.3. Lua里的OOP
      1. 2.3.1. Class抽象
      2. 2.3.2. Class定义
      3. 2.3.3. Class继承
      4. 2.3.4. Class对象实例化
    4. 2.4. Lua只读Table
      1. 2.4.1. 基础版只读Table
      2. 2.4.2. 进阶版只读Table(支持#和pairs)
      3. 2.4.3. 完善版只读Table(支持递归ReadOnly)
      4. 2.4.4. 优化点
    5. 2.5. Lua回调绑定
    6. 2.6. Lua小知识
  3. 3. Lua常用插件
    1. 3.1. luarocks
    2. 3.2. serpent
    3. 3.3. Lpeg
      1. 3.3.1. Pattern
      2. 3.3.2. Match
      3. 3.3.3. Capture
      4. 3.3.4. 实战
  4. 4. Lua IDE
    1. 4.1. VSCode
      1. 4.1.1. 设置同步
      2. 4.1.2. 快捷键
      3. 4.1.3. Snippets
      4. 4.1.4. Lua调试
        1. 4.1.4.1. EmmyLua
          1. 4.1.4.1.1. VSCode配置
          2. 4.1.4.1.2. XLua
          3. 4.1.4.1.3. 代码提示
          4. 4.1.4.1.4. 查找方法引用
          5. 4.1.4.1.5. 调试
  5. 5. ToLua
  6. 6. Refrence
    1. 6.1. 以前学习笔记
    2. 6.2. 官方网站
    3. 6.3. 参考书籍

Lua

结合以前的初步学习了解,这一次要对Lua进行进一步的深入学习。

首先我们要知道Lua是脚本语言,是有自己的虚拟机,是解析执行而非像C#,C++,Java这些编译执行。同时Lua是弱类型语言。

这里不更多的讲解Lua虚拟机相关概念,详情参考http://blog.sina.com.cn/s/blog_8c7d49f20102uzsx.html
Virtual_Machine_Working_Levels
这里只要知道Lua的虚拟机(程序模拟线程,堆栈等)是运行在程序里而非物理CPU上,因此只要虚拟机把Lua脚本解析成对应平台的机器码就能支持跨平台(好比Java的JVM)。

Lua Study

以前的部分学习参考:
Scripting System & Lua Study
Lua — some important concept and knowledge
Lua - C API
Lua - C call Lua & Lua call C
Lua - Object Oriented Programming
Lua - Userdata
Lua - Managing Resources & Threads and States

Weak Tables And Finalizers

“and Finalizers Lua does automatic memory management. Programs create objects (tables, threads, etc.), but there is no function to delete objects. Lua automatically deletes objects that become garbage, using garbage collection. “

Weak Table
“Weak tables allow the collection of Lua objects that are still accessible to the program”

“A weak reference is a reference to an object hat is not considered by the garbage collector”

“In weak tables,, both keys and values can be weak”(three kinds of week table:1. weak key 2. weak value 3. weak key and value)

Weak Table Using:

  1. 释放使用不再使用的缓存数据(memorizing)
    Auxiliary table(辅助表),Lua里有类似于C#里的internal hash table用于重用使用过的string和访问结果(C#里主要是使用过的string重用。Lua还能将访问过的string的table结果缓存返回)

但Auxiliary table有个缺点就是使用不频繁的string和result会一直被缓存无法释放。weak table正是用于解决这一问题的方案之一。(因为weak table的weak reference一旦不再被使用就会被下一次GC释放)

  1. Object Attributes Implemetation
    Solution: use external table to associate objects with attributes
    Drawback: external table reference prevent objects from being collected
    Final solution: use weak keys for objects in external table

  2. Tables with Default Values
    这里有两种各有优缺点的实现方案:
    方案1:

1
2
3
4
5
6
7
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

方案2:

1
2
3
4
5
6
7
8
9
10
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 -- memorize
end
setmetatable(t, mt)
end

前者针对每一个不同的Table都会分配一个defaults入口,table作为key,default value作为value。这样的缺点就是当table数量很大的时候会需要分配很多内存去存储不同table的default value。

后者针对不同的default value分配了更多的内存(比如mt,entry on metas, closure……),但优点是同样的default value只需要分配一次即可,所以后者更适用于table数量多但default value大都相同的情况。

  1. Ephemeron Tables(声明短暂的table Lua 5.2里提供)
    Ephemeron Table:
    “In Lua 5.2, a table with weak keys and strong values is an ephemeron table. In an ephemeron table, the accessibility of a key controls the accessibility of its corresponding value.(只当有strong
    reference to key时value才是strong的)”
    e.g. constant-function factory

Note:
“Only objects can be collected from a weak table. Values, such as numbers and booleans, are not collectible”(只有Objects在weak table里能被gc回收,number和booleans这种Value类型不能在weak table里被gc回收)

“strings are collectible, but string is not removed from weak table(unless its associated value is collected)”

Finalizers
“Finalizers allow the collection of externa objects that are not directly under control of the garbage collector”

“Finalizer is a function associated with an object that is called when that object is about to be collected.”(相当于C#里的Finalize,在GC object的时候会被调用。但只有一开始设置Metamethod的gc时才能mark该object为可finalization的,否则就算后面在复制gc也不会在调用该object的finalizer)

Lua里是通过Metamethod里的__gc实现。

1
2
3
4
5
-- 测试环境要求至少Lua 5.2
o = {x = "hi"}
setmetatable(o, {__gc = function (o) print(o.x) end})
o = nil
collectgarbage() --> hi

The order of finalization called:
“When the collector finalizes several obejcts in the same cycle, it calls their finalizers in the reverse order that the objects were marked for finalization”(先声明__gc被mark为可finalization的object的finalizer后被调用)

1
2
3
4
5
6
7
8
9
10
11
-- 测试环境要求至少Lua 5.2
mt = {__gc = function (o) print(o[1]) end}
list = nil
for i = 1, 3 do
list = setmetatable({i, link = list}, mt)
end
list = nil
collectgarbage()
--> 1
--> 2
--> 3

对于被finalize的object,Finlization会使被finalize的object处于两个特殊的状态:

  1. transient resurrection(短暂的复苏)
  2. permanent resurrection(永久的复苏)
    前者因为在调用__gc的metamethod时我们会得到finalize的object,这样一来使得该object alive again,然后我们可以通过该object用于访问里面的对象。
    finalizer被调用时,该alive again的object被存储在了global table里,导致该object在finalize后依然存在。要想使用finalize也真正回收obejct,我们需要调用两次gc,第一次用于回收原始object,第二次用于回收alive again的object。
1
-- 因为当前测试环境是Lua 5.1(基于LuaTo#插件学习的,JIT支持到5.1的版本)所以不支持Talbe的__gc Metamethod,这里暂时没有写测试程序。

Note:
“In lua 5.1 the only lua values that work with gc metamethod is userdata. “(Lua 5.1不支持table的gc metamethod,只支持userdata的__gc metamethod)

Lua里的位运算

在了解Lua里的位运算之前我们需要了解原码,反码,补码相关的概念,详情参考:
原码, 反码, 补码 详解

这里直接说结论:
机器的数字存储采用补码的方式来编码存储的。

正数的反码 = 自身
负数的反码 = 符号位不变 + 其他位取反

正数的补码 = 自身
负数的补码 = 反码 + 1

移码 = 2^n + 补码(真值为排开非符号位的值,n为真值的位数) = 补码符号位取反

为什么会需要原码,反码,补码?
个人总结原因有以下几点:

  1. 计算机运算设计为了简化让符号位直接参与运算,同时只有加法没有减法,减法用加一个负数的表示
  2. 直接用原码相加无法解决符号位相加问题,导致结果不正确
  3. 直接用反码相加无法解决0的准确表达,即10和00都表示0但区分了正负之分(有效范围127到-127)
  4. 用补码相加不仅解决了正负0的问题,同时1*0还能表示-128(扩展有效范围为127到-128。这也是为什么我们的int32的有效范围为2^31 - 1到(-2)^31的原因)
  5. 移码解决补码不能快速比较两个数大小的问题

了解了基础的理论知识,让我们再来看看Lua里的位运算。

取反

1
2
3
4
5
6
7
for i = 0, 3, 1 do
print("========================")
print(i)
local number = i
local result = ~number
print(result)
end

让我们看下输出结果:
BitNotOperation
上面的结果出人意料,0取反为-1,1取反为-2,2取反为-3,3取反为-4,并不是我们想象中的0取反为-2^31

那么为什么会有这样的结果了,让我们结合前面学习的原码,反码,补码知识来破解其中的奥妙。
让我们来一步一步看看,0在取反的过程中是怎么一步一步变成-1的。
正向:
0000(原码) 0
0000(反码) 0
0000(补码) 0
1111(补码取反) -2^31 + 1

因为机器是以补码存储数字的,所以0的存储编码为0000
我们通过对0取反,得到1111(补码取反)
得到了取反后的补码,我们如何知道这个补码表达的是什么数值了?这个时候需要结合补码的计算方式逆向推导出原码值。

补码逆向:
1111 - 1 = 1110(反码) -2^31 + 1
1110(反码) = 原码符号位不变 + 原码其他位取反 = 1001(原码) = -1

可以看到通过正向和逆向反推,我们成功得出了0取反后的值为-1而不是我们想象中的-2^31

移位和异或

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
print("========================")
local num1 = 1
print(num1 << 1)
local num2 = -1
print(num2 << 1)

print("========================")
-- 01101(原码)
-- 01101(补码)
local num3 = 13
-- 11011(原码)
-- 10100(反码)
-- 10101(补码)
local num4 = -11
-- 01101(补码) & 10011(补码)
-- 00101(异后补码)
-- 00101(异后原码)
print(num3 & num4)
-- 01101(补码) | 10101(补码)
-- 11101(或后补码)
-- 11100(或后反码)
-- 10011(或后原码)
print(num3 | num4)
-- 01101(补码) ~ 10101(补码)
-- 11000(异或后)
-- 10111(异或后反码)
-- 11000(异或后原码)
print(num3 ~ num4)
print("========================")
-- 11001(原码)
-- 10111(补码)
local num5 = -9
-- 00011(原码)
-- 00011(补码)
local num6 = 3
-- 10111(补码) ~ 00011(补码)
-- 10100(异或后)
-- 10011(异或后反码)
-- 11100(异或后原码)
print(num5 ~ num6)

BitwiseOperation
可以看到num5和num6异或因为补码的影响得到-12的结果。
Note:

1. **有负数的情况,异和或操作都会受补码影响,整数补码等于原码,所以整数异和或操作等价于原码直接异或**
2. **异或操作对符号位会有影响,需要考虑补码影响**
3. **位移操作左移会替换符号位,右移会丢弃低位**

Lua里的OOP

OOP(Object Oriented Programming)
首先Lua里面编写更重要的是Think in Lua,所以这里我不是强调OOP的重要性,而是单纯因为OOP被更多的C#程序员所熟悉。

Class抽象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
-- File Name:    Class.lua
-- Description: 模拟Lua里OOP特性(不支持多重继承)
-- Author: TangHuan
-- Create Date: 2018/11/09

-- 使用方法:
-- 1. 定义类
-- local ClassName = Class("ClassName", nil)
-- ***(自定义类数据方法等)
-- 参数说明:
-- clsname类名,super父类
-- 返回结果:
-- ClassName(含Class基本信息结构的table)

-- 2. 实例化类对象
-- 参数说明:
-- BaseClass含类定义的Class Table
-- ...构造类实例对象的构造函数ctor()的参数
-- 返回结果:
-- 基于BaseClass的实例对象
-- new(BaseClass, ...)

---克隆对象(建议用于克隆Class对象)
---@param any 对象
---@return any 克隆对象
function Clone(object)
local lookup_table = {}
local function _copy(object)
if type(object) ~= "table" then
return object
elseif lookup_table[object] then
return lookup_table[object]
end
local new_table = {}
lookup_table[object] = new_table
for key, value in pairs(object) do
new_table[_copy(key)] = _copy(value)
end
return setmetatable(new_table, getmetatable(object))
end
return _copy(object)
end

---判定是否是指定类或者继承至该类
---@param self Class@类实例对象或者类
---@param cname string | Class@类名或者类定义table
local function IsClass(self, cname)
if type(self) ~= "table" then
return false;
end

if type(cname) == "table" then
if self.class == cname then
return true;
elseif self.super then
return self.super.IsClass(self.super, cname);
end
elseif type(cname) == "string" then
if self.class.className == cname then
return true;
elseif self.super then
return self.super.IsClass(self.super, cname);
end
end
return false;
end

--- 提供Lua的OOP实现,快速定义一个Class(不支持多重继承)
--- 模拟Class封装,继承,多态,类型信息,构造函数等
--- 模拟一个基础的Class需要的信息
---@param clsname string@类名
---@param super super@父类
---@return Class@含Class所需基本信息的Class table
function Class(clsname, super)
local classtable = {}
-- ctor模拟构造函数
classtable.Ctor = false
-- className模拟类型信息,负责存储类名
classtable.className = clsname
-- super模拟父类
-- Note: 外部逻辑层不允许直接._super访问父类
classtable._super = super
-- 自身class类table
classtable.class = classtable;
-- 指定索引元方法__index为自身,模拟类访问
-- 后面实例化对象时会将classtable作为元表,从而实现访问类封装的数据
classtable.__index = classtable
-- 是否是指定类或者继承至某类的方法接口
classtable.IsClass = IsClass;
-- 如果指定了父类,通过设置Class的元表为父类模拟继承
if super then
setmetatable(classtable, super)
else
--print("如果定义的不是最基类,请确认是否require了父类!")
end
return classtable
end

--- 提供实例化对象的方法接口
--- 模拟构造函数的递归调用,从最上层父类构造开始调用
---@param cls cls@类定义
---@param ... ...@构造函数变长参数
---@return cls@cls类的实例对象table
function New(cls, ...)
-- 实例对象表
local instance = {}
-- 设置实例对象元表为cls类模拟类的封装访问
setmetatable(instance, cls)
-- create模拟面向对象里构造函数的递归调用(从父类开始构造)
local create
create = function(cls, ...)
if cls._super then
create(cls._super, ...)
end
if cls.Ctor then
cls.Ctor(instance, ...)
end
end
create(cls, ...)
return instance
end

---静态类
function StaticClass(clsname)
return {}
end

上面的代码注释已经很清楚了,就不一一解释了。

理解上面Lua实现OOP的关键在于通过table模拟Class的抽象以及数据封装,通过metatable模拟继承特性。

Class定义

定义一个类的方式如下:

1
2
3
4
5
6
7
8
9
10
11
类名 = Class("类名")

--构造函数
function 类名:Ctor()
--成员变量定义
end

--方法定义
function 类名:方法名()

end

Class继承

继承一个类的方式如下:

1
2
3
4
5
6
7
8
9
10
11
子类名 = Class("子类名", 父类名)

--构造函数
function 子类名:Ctor()
--成员变量定义
end

--方法定义
function 子类名:方法名()

end

Class对象实例化

Lua OOP对象实例化方式如下:

1
2
3
4
local 变量名 = New(类名)
变量名.成员变量
变量名:成员方法()
变量名.静态方法()

Lua只读Table

在游戏开发里,配置表数据往往是固定不允许修改的,无论是C#还是Lua,对于对象修改限制都只能通过上层封装实现此功能。

而这里我要学习了解的就是在Lua里实现ReadOnly table用于配置表,确保开发人员不会出现对配置表的错误操作。

参考:

Sweet Snippet 之 Lua readonly table

基础版只读Table

在Lua官网上其实已经给出了一版ReadOnly的基础实现思路:

Read-Only Tables

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function readOnly (t)
local proxy = {}
local mt = { -- create metatable
__index = t,
__newindex = function (t,k,v)
error("attempt to update a read-only table", 2)
end
}
setmetatable(proxy, mt)
return proxy
end

days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}

print(days[1]) --> Sunday
days[2] = "Noday" --> stdin:1: attempt to update a read-only table

通过上面的事例可以看出,设计Readonly table的核心思想是通过__index(访问不存在Key时触发)和__newindex(赋值不存在Key时触发)元方法来接手所有的table访问赋值。

为了确保__index__newindex的触发,我们需要通过实现一个空table作为代理table来实现所有的Key访问和赋值都能触发__index__newindex,然后通过实现__index__newindex将数据访问赋值指向原始table来实现数据访问和ReadOnly功能。

进阶版只读Table(支持#和pairs)

上面的事例代码虽然实现了基础的ReadOnly功能,但当我们采用#或pairs访问数据长度和数据时会发现,访问不到数据,原因是因为我们的代理table本来就没有数据并且我们也没有自己实现__len__pairs元方法指向原始数据访问导致的。所以进阶版ReadOnly实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function ReadOnly (t, name)
local proxy = {}
local mt = { -- create metatable
__index = t,
__newindex = function (t,k,v)
error(string.format("attempt to update a read-only table:%s", name))
end,
__len = function()
return #t
end,
__pairs = function()
return next, t, nil
end
}
setmetatable(proxy, mt)
return proxy
end

local originalTable = {}
table.insert(originalTable, "A")
table.insert(originalTable, "C")
table.insert(originalTable, "B")
local readOnlyTable = ReadOnly(originalTable, "readOnlyTable")
print(#readOnlyTable)
print("==========================")
for key, value in pairs(readOnlyTable) do
print(key)
print(value)
end
print("==========================")
for index, value in ipairs(readOnlyTable) do
print(index)
print(value)
end
print("==========================")
readOnlyTable[3] = "BB"
--readOnlyTable[4] = "D"

ReadOnlyTableOutput

可以看到,通过重写__len__pairs我们已经支持了ReadOnly的长度获取和pairs遍历访问。

完善版只读Table(支持递归ReadOnly)

经过上面的努力我们已经支持了#和pairs的遍历操作,但我们的只读Table还只支持了一层,也就是说嵌套的table并没有支持只读设定。

实现思路:

  1. 递归对所有的table调用ReadOnly方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
--- 转换table成只读table
---@param t table @需要变成ReadOnly的table
---@param name string @ReadOnly table的名字(方便报错打印定位)
---@param depth number @只读table深度
---@return table @转换后的只读table
function ReadOnlyTable(t, name, depth)
if type(t) ~= "table" then
error("not support none table to a recusive read-only table!")
return t
end
depth = depth or 1
if depth > 1 then
local nextDepth = depth - 1
for key, value in pairs(t) do
if type(value) == "table" then
t[key] = ReadOnlyTable (t[key], name, nextDepth)
end
end
end
local proxy = {}
local mt = {
__index = t,
__newindex = function (t,k,v)
error(string.format("attempt to update a read-only table:%s", name))
end,
__len = function()
return #t
end,
__pairs = function()
return next, t, nil
end
}
setmetatable(proxy, mt)
return proxy
end

local originalTable = {}
originalTable["A"] = "A"
originalTable["NestTable"] = {
["Nest_A"] = "Nest_A",
["Nest_B"] = "Nest_B",
["NestNestTable"] = {
["Nest_Nest_A"] = "Nest_Nest_A",
["Nest_Nest_B"] = "Nest_Nest_B",
}
}
originalTable["B"] = "B"
local readOnlyTable = ReadOnlyTable(originalTable, "readOnlyTable", 2)
print_table(readOnlyTable)
print(#readOnlyTable)
print("==========================")
for key, value in pairs(readOnlyTable) do
print(key)
print(value)
end
print("==========================")
--readOnlyTable["A"] = "AA" --> ReadOnly Error
--readOnlyTable["NestTable"]["Nest_A"] = "Nest_AA" --> ReadOnly Error
readOnlyTable["NestTable"]["NestNestTable"]["Nest_Nest_A"] = "Nest_Nest_AA"
print_table(readOnlyTable)

RecusiveReadOnlyTableOuput

通过上面的完善,我们成功的实现了支持指定深度的ReadOnly表实现。

优化点

  1. 考虑到原表的性能开销,我们可以在开发期开启ReadOnly表的设计,发包后直接采用原始表访问的方式来优化掉这部分性能开销

Lua回调绑定

在Lua里函数有着第一类值的说法(在Lua中函数和其他值(数值、字符串)一样,函数可以被存放在变量中,也可以存放在表中,可以作为函数的参数,还可以作为函数的返回值。)。

很多时候我们想传递一个类方法作为回调方法,希望回来的时候是自带self,如果我们只按以下写法来使用,我们将丢掉self这个上下文:

1
2
3
4
5
6
7
8
9
10
11
local testHandler = {}

function testHandler:FuncWithoutParams()
print("testHandler:FuncWithoutParams()")
end

function testHandler:TestHandler(cb)
cb()
end

testHandler:TestHandler(testHandler.FuncWithoutParams)

所以为了封装self的,我们需要通过闭包机制来实现一个handler,以下代码实现了带参和不带参的handler.lua版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- File Name:      Handler.lua
-- Description: 带self的回调绑定
-- Author: TonyTang
-- Create Date: 2021/6/5

--- 无参委托绑定
---@param obj table @对象
---@param method func() @方法
---@return func() @委托绑定无参方法
function handler(obj, method)
return function()
method(obj)
end
end

--- 带参委托绑定
---@param obj table @对象
---@param method func() @方法
---@param ... any @参数
---@return func(...) @委托绑定带参方法
function handlerBind(object, method, ...)
local params = table.pack(...)
return function()
method(obj, table.unpack(params))
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
local testHandler = {}

function testHandler:FuncWithoutParams()
print("testHandler:FuncWithoutParams()")
end

function testHandler:FuncWithParams(param1, param2, param3)
print(string.format("testHandler:FuncWithParams(%s, %s, %s)", param1, param2, param3))
end

function testHandler:TestHandler()
local handler = _G.handler(self, self.FuncWithoutParams)
handler()
end

function testHandler:TestHandlerBind(param1, param2, param3)
local handlerBind = _G.handlerBind(self, self.FuncWithParams, param1, param2, param3)
handlerBind()
end

testHandler:TestHandler()
testHandler:TestHandlerBind(1, 2, 3)
testHandler:TestHandlerBind(1, nil, 3)
testHandler:TestHandlerBind(nil, 2, 3)

HandlerOutput

但上面这种设计,还不支持自定义传参的基础上保留自定义函数传参:

1
2
3
4
5
6
7
8
9
10
11
function testHandler:FuncWithParams(...)
print("testHandler:FuncWithParams()")
print(...)
end

function testHandler:TestHandlerBind(...)
local handlerBind = _G.handlerBind(self, self.FuncWithParams, "param1", "param2")
handlerBind(...)
end

testHandler:TestHandlerBind(1, 2, 3)

理论上上面这种情况,我们预期的testHandler:FuncWithParams()最终调用传入的参数为2+N个(“param1”+”param2”+…)

实现上面的需求,核心我们需要利用table.pack和table.unpack的机制(但要注意table.unpack(tb)默认效果等价于table.unpack(tb, 1, #tb)即会被nil打断的,这个不是我们希望看到的)。

解决上述问题,我们只需要通过封装参数pack和unpack过程,并明确指定table.unpack(tb, 1, count)的方式来解决被nil打断的情况,最终handler.lua代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
-- File Name:      Handler.lua
-- Description: 带self的回调绑定
-- Author: TonyTang
-- Create Date: 2021/6/5

--- 无参委托绑定
---@param obj table @对象
---@param method func() @方法
---@return func() @委托绑定无参方法
function handler(obj, method)
return function(...)
method(obj, ...)
end
end

--- 带参委托绑定
---@param obj table @对象
---@param method func() @方法
---@param ... any @可变长参数
---@return func(...) @委托绑定带参方法
function handlerBind(object, method, ...)
local params = SafePack(...)

return function(...)
local params2 = SafePack(...)
local concateParams = ConcatePack(params, params2)
method(obj, SaveUnpack(concateParams))
end
end

--- 安全Pack(解决原生pack的nil截断问题, SafePack与SafeUnpack要成对使用)
---@return table @pack后的参数table
function SafePack(...)
local params = {...}

params.n = select("#", ...)
return params
end

--- 安全Unpack(解决原生unpack被nil阶段问题, SafePack与SafeUnpack要成对使用)
---@return ... @unpack后的参数列表
function SaveUnpack(parakParams)
--- 使用table.unpack()显示指定起始值和数量的方式避免被nil打断
return table.unpack(parakParams, 1, parakParams.n)
end

--- 合并参数
---@param packParams1 table @已经pack的参数table1
---@param packParams2 table @已经pack的参数table2
---@return table @合并pack后table
function ConcatePack(packParams1, packParams2)
--- 基于table.pack和table.unpack机制合并参数
local finalParams = {}

local params1Count = packParams1.n
for i = 1, params1Count, 1 do
finalParams[i] = packParams1[i]
end
local params2Count = packParams2.n
for i = 1, params2Count, 1 do
finalParams[params1Count + i] = packParams2[i]
end
finalParams.n = params1Count + params2Count
return finalParams
end

Note:

  1. table.unpack()默认是table.unpack(tb, 1, #tb),所以如果传递了nil的话是会被打断无法返回所有的

Lua小知识

  1. Lua里没有Class只有通过Table模拟的Class
    参考前面的Lua OOP实现

  2. Lua里没有this的概念,self不等价于this,函数调用传的是谁谁就是self。Lua里.定义和:定义相对于self的用法来说相当于静态和成员定义。
    Lua里通过.调用的话是不会默认传递自身作为self的,不主动传的话self为空
    Lua里通过:调用的话默认第一个参数就是调用者,既self

1
2
3
4
5
6
7
8
9
10
11
12
13
tab = {}
function tab.dotFunc(param)
print("self = ", self)
print(param)
end

function tab:colonFunc(param)
print("self = ", self)
print(param)
end

tab.dotFunc(1)
tab:colonFunc(1)
![LuaDotAndColon](/img/Lua/LuaDotAndColon.png)
Note:
:只是起了省略第一个参数self的作用,该self指向调用者本身,并没有其他特殊的地方。
  1. Lua中的函数是带有词法定界(lexical scoping)的第一类值(first-class values)(在Lua中函数和其他值(数值、字符串)一样,函数可以被存放在变量中,也可以存放在表中,可以作为函数的参数,还可以作为函数的返回值)

    1
    2
    3
    4
    5
    6
    7
    8
    class = {}
    function class:Func(param1, func)
    print(param1)
    func()
    end

    myfunc = function() print("tempFunc()") end
    class:Func(1, myfunc)

    LuaFirstClassValues

  2. 指定Lua搜索文件路径

    1
    package.path = *

    package.path是在Lua触发require时会去搜索的路径,默认是LUA_PATH_5_3或者LUA_PATH,详情参考:Lua Manual

  3. Lua文件热重载
    Lua通过require加载进来的文件都会被记录在package.loaded里,所以Lua热重载的核心就是让pacakge.loaded里的对应Lua文件清除后重新加载。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ---热重载指定Lua脚本
    function reloadLua(filename)
    if(package.loaded[filename] == nil) then
    Log.log(string.format("lua脚本 : %s 未被加载", filename))
    else
    Log.log(string.format("lua脚本 : %s 已加载", filename))
    --重置loaded信息
    package.loaded[filename] = nil
    end
    --重新加载Lua脚本
    require(filename)
    end
  4. Lua里pairs和ipairs有区别。pairs是key,value的形式遍历所有值。ipairs是从1开始按顺序递增遍历。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    testtab = {[1] = "Tony", [4] = "Huan", [2] = "Tang", [5] = nil }

    for k, v in pairs(testtab) do
    print(k)
    print(v)
    end

    print("=================")

    for i, v in ipairs(testtab) do
    print(i)
    print(v)
    end

    LuaPairsAndIpairs

    Note:
    pairs不会被nil打断但ipairs都会被nil打断
    ipairs还会被不是按1递增的打断

  5. Lua参数传递
    Lua里通过arg[?]的形式访问传入的参数,默认第一个参数是arg[1],arg[0]是lua脚本自身

  6. Lua里面可以通过#得到table长度
    Note:
    #会被nil打断

  7. Lua里打印数据类型用type

1
2
print(type(1))
print(type("Tony"))

LuaType

Lua常用插件

luarocks

LuaRocks is the package manager for Lua modules.

从官方的介绍可以看出,LuaRocks相对于Lua,就好比Package Ctronl在Sublime Text里的存在,主要目的就是包管理器,方便Lua快速安装各种第三方Lua库。

根据官方的教程,LuaRocks安装设置相当复杂,单纯下载一个exe还有很多环境问题要处理。

这里本人不想使用官方Installation instructions for WindowsNew Page]的一键安装方式,主要是希望能用Lua5.3,他一键安装的方式好像是Lua5.1,所以最后打算把Lua和LuaRocks都按照以下博主的方式从源码编译走一遍流程:

Windows 平台 Luarocks 3.0.2 编译安装

  • 安装MinGW(用于编译源码)

    添加**/MinGW/bin到环境变量,确保gcc编译器能找到.

    WhereMinGW

    关于MinGW和Cygwin的区别这里暂时不深入,详情参考:

    MinGw与Cygwin的区别

    MinGW 的主要方向是让GCC的Windows移植版能使用Win32API来编程。 Cygwin 的目标是能让Unix-like下的程序代码在Windows下直接被编译。

    这里考虑到这里的主要目的是编译Windows版的Lua5.3,用哪一个都行,暂时是用MinGW了。

  • 编译Lua5.3

    下载Lua5.3源码

    MinGW安装好后,编译Lua5.3就很容易了,因为Lua5.3里面自带了MakeFile(指定make如何编译的文件,这里暂时不深入学习),通过命令:

    1
    mingw32-make mingw

    就能触发编译Lua5.3,但为了LuaSocks后续会用到的相关目录准备工作,这里还是采用前面博主提供的build.bat:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    @echo off
    :: ========================
    :: build.bat
    ::
    :: build lua to dist folder
    :: tested with lua-5.3.5
    :: based on:
    :: https://medium.com/@CassiusBenard/lua-basics-windows-7-installation-and-running-lua-files-from-the-command-line-e8196e988d71
    :: ========================
    setlocal
    :: you may change the following variable’s value
    :: to suit the downloaded version
    set work_dir=%~dp0
    :: Removes trailing backslash
    :: to enhance readability in the following steps
    set work_dir=%work_dir:~0,-1%
    set lua_install_dir=%work_dir%\dist
    set compiler_bin_dir=%work_dir%\tdm-gcc\bin
    set lua_build_dir=%work_dir%
    set path=%compiler_bin_dir%;%path%
    cd /D %lua_build_dir%
    mingw32-make PLAT=mingw
    echo.
    echo **** COMPILATION TERMINATED ****
    echo.
    echo **** BUILDING BINARY DISTRIBUTION ****
    echo.
    :: create a clean “binary” installation
    mkdir %lua_install_dir%
    mkdir %lua_install_dir%\doc
    mkdir %lua_install_dir%\bin
    mkdir %lua_install_dir%\include
    mkdir %lua_install_dir%\lib
    copy %lua_build_dir%\doc\*.* %lua_install_dir%\doc\*.*
    copy %lua_build_dir%\src\*.exe %lua_install_dir%\bin\*.*
    copy %lua_build_dir%\src\*.dll %lua_install_dir%\bin\*.*
    copy %lua_build_dir%\src\luaconf.h %lua_install_dir%\include\*.*
    copy %lua_build_dir%\src\lua.h %lua_install_dir%\include\*.*
    copy %lua_build_dir%\src\lualib.h %lua_install_dir%\include\*.*
    copy %lua_build_dir%\src\lauxlib.h %lua_install_dir%\include\*.*
    copy %lua_build_dir%\src\lua.hpp %lua_install_dir%\include\*.*
    copy %lua_build_dir%\src\liblua.a %lua_install_dir%\lib\liblua.a
    echo.
    echo **** BINARY DISTRIBUTION BUILT ****
    echo.
    %lua_install_dir%\bin\lua.exe -e "print [[Hello!]];print[[Simple Lua test successful!!!]]"
    echo.

    :: configure environment variable
    :: https://stackoverflow.com/a/21606502/4394850
    :: http://lua-users.org/wiki/LuaRocksConfig
    :: SETX - Set an environment variable permanently.
    :: /m Set the variable in the system environment HKLM.
    setx LUA "%lua_install_dir%\bin\lua.exe" /m
    setx LUA_BINDIR "%lua_install_dir%\bin" /m
    setx LUA_INCDIR "%lua_install_dir%\include" /m
    setx LUA_LIBDIR "%lua_install_dir%\lib" /m

    pause

    这样一来Lua编译以及Lua环境变量设置就搞定了:

    LuaBuildOutput

    LuaEnvSetting

    测试以下Lua使用:

    MakeSureLuaExits

    Note:

    1. 因为我们使用的是MinGW,所以博客上的make命令是找不到的需要用mingw32-make
    2. 因为涉及到设置环境变量等操作所以启动cmd或者powershell运行bat时需要以管理员身份
    3. 上文的Lua环境变量设置并未设置Lua到Path中,所以还需要单独添加Lua/bin到能确保Lua能被找到使用
  • 编译LuaRocks

    下载LuaRocks源码(下一键带Install.bat的)

    执行Install.bat触发编译流程

    LuaRocksInstallCommand

    详细参数介绍参考:

    Installation instructions for Windows

    最后看到LuaRocks安装成功:

    LuaRocksInstallSuccess

    最后将LuaRocks/systree/bin所在目录添加到环境变量Path里,这样通过LuaRocks安装的第三方库就可以使用了(经测试单独只加到Path里还不行(好像是因为Lua和LuaRocks默认分开安装的),package.path里默认是不会去LuaRocks/systree/bin下找的),需要执行以下命令来查看手动设置LUA_PATH所需的位置信息:

    1
    luarocks path --bin

    LuaRocksPathAndCPath

    原本的LUA_PATH和LUACPATH信息:

    OrignalLuaPathAndCPath

    把上面两个手动设置到环境变量LUA_PATH和LUA_CPATH里:

    LuaEnviromentSetting

接下来测试LuaRocks的使用:

我们尝试安装serpent:

1
luarocks install serpent

InstallSerpent

SerpentInstallatio

后续会讲到serpent的使用学习,到这里Lua5.3和LuaRocks以及serpent的LuaRocks快速安装就全部结束了。

serpent

Lua serializer and pretty printer.

从Git上的介绍可以看出,Serpent是一个支持序列化反序列化以及格式化打印的第三方库。

比如我们希望把内存里的某个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
28
print("SerpentStudy!")

require "DIYLog"

local serpent = require "serpent"

local serializeDataTable = {
["Name"] = "TonyTang",
detailInfo = {
["Age"] = 18,
["BirthDate"] = "2000/01/01",
["Adrress"] = "SiChuan"
}
}

print("=================")
local serializeString = serpent.dump(serializeDataTable)
print(serializeString)

print("=================")

local formatSerializeString = serpent.block(serializeDataTable)
print(formatSerializeString)

print("=================")

local result, deserializeDataTable = serpent.load(formatSerializeString)
print_table(deserializeDataTable)

输出:

SerpentOutput

从上面测试用例结果可以看出,通过Serpent我们轻易的做到了Lua序列化反序列化以及格式化输出

Lpeg

LPeg is a new pattern-matching library for Lua, based on Parsing Expression Grammars (PEGs).

从官方的介绍可以看出Lpeg是一个类似正则的但并不是正则的新一代(基于Parsing Expression Grammars)的模式匹配Lua库。

那么什么是Parsing Expression Grammars了?

a parsing expression grammar (PEG), is a type of analytic formal grammar, i.e. it describes a formal language in terms of a set of rules for recognizing strings in the language.

根据Wiki的介绍可以看出Parsing Expression Grammars就是我们编程语言里的解析表达式语法。

为了进一步的深入学习和使用,让我们先通过Luasocks安装Lpeg库。

LpegInstallation

接下来文档Lpeg和下面这篇文章通过 LPeg 介绍解析表达式语法(Parsing Expression Grammars)来深入学习Lpeg的设计和使用:

Pattern

Pattern差不多可以理解成正则里的匹配表达式。

为了方便理解,这里先截图下Lpeg Pattern相关定义的基础介绍:

LpegPatternsDoc

Match

Match也和正则里的概念差不多,就是触发指定字符串使用指定Pattern进行匹配的一个过程,同时返回匹配相关结果(比如返回匹配到的字符位置的下一个字符位置。或者通过Capture函数将匹配结果转换成指定值)

了解了Lpeg里面的一些基础Pattern定义后,让我们看看Lpeg里匹配函数match的介绍:

lpeg.match (pattern, subject [, init])

The matching function. It attempts to match the given pattern against the subject string. If the match succeeds, returns the index in the subject of the first character after the match, or the captured values(if the pattern captured any value).

Unlike typical pattern-matching functions, match works only in anchored mode; that is, it tries to match the pattern with a prefix of the given subject string (at position init), not with an arbitrary substring of the subject. So, if we want to find a pattern anywhere in a string, we must either write a loop in Lua or write a pattern that matches anywhere.

从上面的介绍可以看出,match匹配成功会返回匹配位置的下一个位置或者是捕获到的值(后续会讲到捕获函数相关)。同时match不想正则可以把所有匹配都找出来而是需要通过自行通过match的init方式来loop所有捕获后的子字符串来找出所有匹配的结果。

Note:

  1. 默认情况下,成功匹配时,LPeg 将返回字符消耗数(译者注: 也就是成功匹配子串之后的下一个字符的位置). 如果你只是想看看是否匹配这就足够好了,但如果你试图解析出字符串的结构来,你必须用一些 LPeg 的捕获(capturing)函数.

Capture

A capture is a pattern that produces values (the so called semantic information) according to what it matches. LPeg offers several kinds of captures, which produces values based on matches and combine these values to produce new values. Each capture may produce zero or more values.

从介绍大概理解Capture也是一种Pattern,但它是一种基于Pattern匹配结果转换出结果值的Pattern而不是常规的匹配Pattern(个人理解就好比Pattern+转换函数)。

为了方便理解,这里先截图下Capture相关定义的基础介绍:

LpegCaptureDoc

Note:

  1. A capture pattern produces its values only when it succeeds.(Capture Pattern只会在成功时计算值)

实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
print("LpegStudy")

require "DIYLog"

--- 最终目标:
--- 1. {***}占位替换
--- 2. {num string1 | string2 | string3 }根据num选择对应字符串

local lpeg = require("lpeg")
local match = lpeg.match
local P = lpeg.P
local R = lpeg.R

--- 普通匹配
print("普通匹配:")
--- 不匹配,返回nil
print(match(P("hello"), "world"))
--- 一个匹配,返回6
print(match(P("hello"), "hello"))
--- 一个匹配,返回6
print(match(P("hello"), "helloworld"))

--- 模式组合
print("模式组合:")
local helloPattern = P("hello")
local worldPattern = P("world")
--- *表示匹配pattern1后面跟随pattern2
local helloAndworldPattern = helloPattern * worldPattern
--- +表示匹配pattern1或者pattern2
local helloOrworldPattern = helloPattern + worldPattern
--- 一个匹配,返回11
print(match(helloAndworldPattern, "helloworld"))
--- 不匹配,返回nil
print(match(helloAndworldPattern, "worldhello"))
--- 1个匹配,返回6
print(match(helloOrworldPattern, "helloworld"))
--- 1个匹配,返回6
print(match(helloOrworldPattern, "worldhello"))

--- 解析数字
print("解析数字:")
local integerPattern = R("09")^1
--- 解析数字带捕获函数
local integerCapturePattern = R("09")^1 / tonumber
--- 匹配,返回5
print(match(integerPattern, "2923 2924"))
--- 匹配,返回2923
print(match(integerCapturePattern, "2923 2924"))

--- 解析数字或者字符
print("解析数字或者字符:")
local charPattern = P(1)
--- 匹配,返回2
print(match(charPattern, "hello 123"))
local integerOrchaPattern = integerCapturePattern-- + charPattern
--- 利用Capture.Ct函数存储所有匹配结果到表中
local integerOrcharCapture = lpeg.Ct(integerOrchaPattern^0)
--- 未匹配输出 {}
print_table(match(integerOrcharCapture, "hello!"))
--- 匹配,输出:{123}
print_table(match(integerOrcharCapture, "hello 123"))
--- 匹配,输出:{5,5,5,7,7,7}
print_table(match(integerOrcharCapture, "5 5 5 yeah 7 7 7"))
--- 匹配,输出:{5,5,5,7,7,7}
print_table(match(integerOrcharCapture, "5 5 5 a b c d 7 7 7"))

--- 计算器语法解析器
print("计算器语法解析器:")
local white = lpeg.S(" \t\r\n") ^ 0

local integer = white * lpeg.R("09") ^ 1 / tonumber
local muldiv = white * lpeg.C(lpeg.S("/*"))
local addsub = white * lpeg.C(lpeg.S("+-"))

local factor = integer * muldiv * integer + integer

--- 匹配,输出:{5}
print_table(match(lpeg.Ct(factor), "5"))
--- 匹配,输出:{2,"*",1}
print_table(match(lpeg.Ct(factor), "2*1"))

--- 字符串匹配替换
--- 实现类似gsub的方法的效果
print("字符串匹配替换:")
--- lpeg.Ct -- 仅仅捕获到table
function find(s, patt)
patt = P(patt) / tostring
local p = lpeg.Ct((patt + 1)^0)
return match(p, s)
end

--- lpeg.Cs -- 捕获并替换
function gsub(s, patt, repl)
patt = P(patt)
local p = lpeg.Cs((patt / repl + 1)^0)
return match(p, s)
end

--- 匹配,输出:{"dog","dog"}
print_table(find("hello dog, dog!", "dog"))
--- 匹配,输出:{"hello cat, cat!"}
print_table(gsub("hello dog, dog!", "dog", "cat"))

--- 最终目标1: {***}占位替换
--- 指定字符串,采用table里的key替换{key}成key对应的value
function tableGsub(s, tb)
local result = s
local replaceKey = nil
local replaceValue = nil
for key, value in pairs(tb) do
replaceKey = string.format("{%s}", key)
replaceValue = tostring(value)
print(string.format("字符串:%s 替换:%s为%s", result, replaceKey, replaceValue))
result = gsub(result, replaceKey, replaceValue)
print(string.format("替换后结果:%s", result))
end
return result
end

local content = "测试table替换,名字:{name} 年龄:{age} 性别:{sex} 名字2:{name} 名字3:{name}"
local replaceTable = {
name = "TonyTang",
sex = "Man",
age = 18,
luckyNumber = 7,
}
local timeCounter = New(TimeCounter, "tableGsub")
timeCounter:Start()
content = tableGsub(content, replaceTable)
timeCounter:Stop()
print("替换后结果:replaceValue")
print(content)
print(string.format("替换耗时:%s", timeCounter.EllapseTime))

Lua IDE

首先这里决定选一个相对轻量级的Lua IDE,VS因为太重量级了,这里不考虑。
其次考虑到跨平台和前端常用的(平时主要会用到C#和Lua),最初决定选择VSCode作为Lua代码编写的IDE,VSCode丰富的插件库也是选择VSCode很重要的一个原因之一。经过调研发现VSCode第三方插件Luaide在VSCode 1.33.1以上版本有严重的内存暴涨问题,所以最后笔者转向了VSCode + EmmyLua 插件的方式。

VSCode

Visual Studio Code(简称VS Code)是一个由微软开发的,同时支持Windows、Linux、和macOS系统且开放源代码的代码编辑器[4],它支持测试,并内置了Git 版本控制功能,同时也具有开发环境功能,例如代码补全(类似于 IntelliSense)、代码片段、和代码重构等,该编辑器支持用户个性化配置,例如改变主题颜色、键盘快捷方式等各种属性和参数,还在编辑器中内置了扩展程序管理的功能。

设置同步

VSCode支持高度自由的自定义插件和自定义设置,我们如何在不同的工作环境快速同步IDE信息(插件,自定义设置等),避免每一次在不同工作环境下都重复下载插件设置等操作是一个需要解决的问题。

借助于第三方插件:
SettingsSync(使用了Github Gist)可以同步保存VSCode配置和扩展。

详细配置流程参考:
Settings Sync

上传快捷键:
Shift + Alt + U
下载同步快捷键:
Shift + Alt + D

快捷键

当拿到新的IDE时,为了工作效率,快捷键不熟是一个很影响开发效率的问题。
VSCode丰富的插件库和高度的可自定义可以轻松解决这个问题:

  1. Visual Studio Keymap(第三方插件 — 快速导入VS的快捷键)
  2. Custom Shortcup Keymap(自定义设置快捷键File -> Preference -> Keyboard Shortcuts — 用于满足自定义需求)

Snippets

Snippets又名代码片段,主要目的是为了通过自定义关键词和模板,快速触发代码片段生成。
这里主要针对自定义代码片段的功能使用来学习。
File -> Preference -> User Snippets
e.g.个人文件署名的代码片段:
lua.json

1
2
3
4
5
6
7
8
9
10
11
12
{
"Lua File Title Sneppits": {
"prefix": "LuaFileTitle",
"body": [
"-- File Name: $TM_FILENAME",
"-- Description: This file is used to ${1:Staff}",
"-- Author: ${2:TangHuan}",
"-- Create Date: ${3:Year}/${4:Month}/${5:Day}",
],
"description": "Lua File Title Sneppits"
}
}

这样一来当我们输入LuaFileTitle关键词时,VSCode就会自动触发提示,直接table键选择Snippets就能触发代码生成。
效果如下:

1
2
3
4
-- File Name:    Base.lua
-- Description: This file is used to Staff
-- Author: TangHuan
-- Create Date: Year/Month/Day

详细使用Snippets参考:
Creating your own snippets

Lua调试

Lua作为解析执行的语言,调试相比其他高级语言比较麻烦一点,这里选择借助第三方(Luaide)插件作为辅助工具。

经过调研发现VSCode第三方插件Luaide在VSCode 1.33.1以上版本有严重的内存暴涨问题,所以最后笔者转向了VSCode + EmmyLua 插件的方式。
这里就没有详细了解VSCode + LuaIde的调试方式了。

EmmyLua

EmmyLua
VSCode安装EmmyLua很简单,直接插件里搜索安装即可,这里就不多做说明。

接下来主要结合XLua+VSCode+EmmyLua实战使用EmmyLua提示和调试两大重要功能。

VSCode配置

安装EmmyLua插件后,VSCode基本就已经完成配置了,但要使用VSCode开始编写Lua代码,我们还需要把Lua代码导入到VSCode里来。

VSCode导入Lua代码是以目录的形式,我们导入的我们加载Lua文件所在的最上层目录即可:
LuaFolderStructure

XLua

XLua官网
XLua的集成很简单,从Git下下来,把关键的几个目录放到项目里即可。
主要是如下几个目录:

  1. Tools(打包时XLua代码注入需要的工具在这里 Note: 放Assets同一层不要放到Assets里了)
  2. Assets/XLua/Src(XLua需要的CS源代码)
  3. Assets/XLua/Editor/ExampleConfig.cs(XLua默认给的一套纯Lua开发基于反射生成需要标记CSharpCallLua || LuaCallCSharp || BlackList || Hotfix的一套模板)
  4. Assets/Plugins(XLua编译多平台后的库文件)

Unity加载Lua文件默认不认.lua后缀的文件,我们需要自定义CustomLoader去加载到我们想要加载的.lua文件。
LuaManager.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using XLua;

/// <summary>
/// Lua单例管理者
/// </summary>
public class LuaManager : SingletonMonoBehaviourTemplate<LuaManager> {

/// <summary>
/// Lua环境(全局唯一)
/// </summary>
public LuaEnv LuaEnv
{
get;
private set;
}

/// <summary>
/// Lua脚本默认创建目录路径
/// </summary>
private string LuaScriptFolderPath;

/// <summary>
/// Lua脚本文件后缀
/// </summary>
private const string LuaFilePostFix = ".lua";

/// <summary>
/// Lua脚本文件路径映射Map
/// Key为文件名(不含.lua后缀),Value为该文件的全路径(含.lua后缀)
/// </summary>
private Dictionary<string, string> mLuaScriptFilePathMap;

/// <summary>
/// Lua初始化
/// </summary>
public override void init()
{
Debug.Log("LuaManager:init()");
if (LuaEnv == null)
{
LuaEnv = new LuaEnv();
}
else
{
Debug.Log("Lua环境已经初始化!");
return;
}

LuaScriptFolderPath = Application.dataPath + "/Scripts/XLua/Lua/";
mLuaScriptFilePathMap = new Dictionary<string, string>();

//自定义CustomLoader
LuaEnv.AddLoader(LuaCustomLoader);

//读取所有Lua文件
readAllLuaFiles();

//加载LuaMain.lua
LuaEnv.DoString("require 'LuaMain'");
}

private void readAllLuaFiles()
{
var allluafiles = Directory.GetFiles(LuaScriptFolderPath, "*.lua", SearchOption.AllDirectories);
foreach(var luafile in allluafiles)
{
var luafilename = Path.GetFileNameWithoutExtension(luafile);
if(!mLuaScriptFilePathMap.ContainsKey(luafilename))
{
mLuaScriptFilePathMap.Add(luafilename, luafile);
}
else
{
Debug.LogError(string.Format("有重名的Lua文件,文件路径:{0}文件路径:{1}",luafile, mLuaScriptFilePathMap[luafilename]));
}
}
}

/// <summary>
/// Lua自定义Loader
/// </summary>
/// <param name="filename"></param>
/// <returns></returns>
private byte[] LuaCustomLoader(ref string filename)
{
Debug.Log(string.Format("加载Lua文件 : {0}", filename));
var scriptfullpath = string.Empty;
#if UNITY_EDITOR
//Editor走File加载
//Unity不认Lua后缀的情况
var luascriptfolderpath = string.Empty;
byte[] luafile = null;
if (mLuaScriptFilePathMap.TryGetValue(filename, out scriptfullpath))
{
luafile = File.ReadAllBytes(scriptfullpath);
}
#else
//TODO
//真机走其他方式加载
return null;
#endif
if (luafile != null)
{
return luafile;
}
else
{
Debug.LogError(string.Format("找不到Lua文件 : {0}", filename));
return null;
}
}
}

可以看到为了成功加载.lua后缀的Lua文件我做了以下几件事:

  1. 通过Directory.GetFiles()获取到所有*.lua文件的路径映射,用于加载时消除目录的概念
  2. 自定义CustomLoader,支持加载(通过File.ReadAllBytes).lua后缀文件
代码提示

Test.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- File Name:    Test.lua
-- Description: This file is used to Test EmmyLua
-- Author: TangHuan
-- Create Date: 2019/04/29

--- 自定义table类型,测试Emmylua注释提示
---@class diytable
---@field field1 number 字段1
---@field field2 string 字段2
local diytable = {
field1 = 321,
field2 = "field2"
}

---@param para1 number 参数1
---@return diytable 返回diytable
function Test(para1)
print("Test(" .. para1 .. ")")
return diytable
end

local table = Test(123)
print(table.field1)
print(table.field2)

输入table.时会看到成员变量提示:
EmmyLuaNotation

EmmyLua提示详细使用参考:
@class类声明注解

查找方法引用

EmmyLua查找方法引用很简单,直接在方法上右键查找所有引用即可:
LuaMethodReference

调试

EmmyLua插件装好后,VSCode导入了Lua加载最上层目录后,调试Lua就很简单了:
F5 -> EmmyLua Attach Debug -> 选择对应Unity进程
然后通过F9下断点就可以像VS一样给Lua下断点了。
VSCodeEmmyLuaDebug

ToLua

待续……

Refrence

通过 LPeg 介绍解析表达式语法(Parsing Expression Grammars)

lpeg

原码, 反码, 补码 详解

谈谈二进制(四)——原码、补码、反码、移码

以前学习笔记

http://blog.sina.com.cn/s/blog_8c7d49f20102uzsx.html
http://blog.sina.com.cn/s/blog_8c7d49f20102v0rk.html
http://blog.sina.com.cn/s/blog_8c7d49f20102v0s0.html
http://blog.sina.com.cn/s/blog_8c7d49f20102v0s5.html
http://blog.sina.com.cn/s/blog_8c7d49f20102v0s9.html
http://blog.sina.com.cn/s/blog_8c7d49f20102v0zp.html
http://blog.sina.com.cn/s/blog_8c7d49f20102v0zs.html
http://blog.sina.com.cn/s/blog_8c7d49f20102v1qg.html
http://blog.sina.com.cn/s/blog_8c7d49f20102v383.html

官方网站

XLua
EmmyLua

参考书籍

《Programming in Lua third edition》 — Roberto Ierusalimschy
《Lua用户手册》
《Game Scripting Mastery》 — Alex Varanese

文章目錄
  1. 1. Lua
  2. 2. Lua Study
    1. 2.1. Weak Tables And Finalizers
    2. 2.2. Lua里的位运算
      1. 2.2.1. 取反
      2. 2.2.2. 移位和异或
    3. 2.3. Lua里的OOP
      1. 2.3.1. Class抽象
      2. 2.3.2. Class定义
      3. 2.3.3. Class继承
      4. 2.3.4. Class对象实例化
    4. 2.4. Lua只读Table
      1. 2.4.1. 基础版只读Table
      2. 2.4.2. 进阶版只读Table(支持#和pairs)
      3. 2.4.3. 完善版只读Table(支持递归ReadOnly)
      4. 2.4.4. 优化点
    5. 2.5. Lua回调绑定
    6. 2.6. Lua小知识
  3. 3. Lua常用插件
    1. 3.1. luarocks
    2. 3.2. serpent
    3. 3.3. Lpeg
      1. 3.3.1. Pattern
      2. 3.3.2. Match
      3. 3.3.3. Capture
      4. 3.3.4. 实战
  4. 4. Lua IDE
    1. 4.1. VSCode
      1. 4.1.1. 设置同步
      2. 4.1.2. 快捷键
      3. 4.1.3. Snippets
      4. 4.1.4. Lua调试
        1. 4.1.4.1. EmmyLua
          1. 4.1.4.1.1. VSCode配置
          2. 4.1.4.1.2. XLua
          3. 4.1.4.1.3. 代码提示
          4. 4.1.4.1.4. 查找方法引用
          5. 4.1.4.1.5. 调试
  5. 5. ToLua
  6. 6. Refrence
    1. 6.1. 以前学习笔记
    2. 6.2. 官方网站
    3. 6.3. 参考书籍