Lua从入门到入了门(2) 基础难点概述

如何组织代码

Lua和JS一样都是解释型语言,没有入口的概念. 但是要实现文章1结尾时候所说的Coding环境

写个入口文件 main.lua 然后每个类一个文件 从入口文件的某个函数开始执行 然后把整个程序Run起来

我能想到的解决方案是

  1. 首先通过某种方法可以用Lua模拟出类的概念

  2. 将每一个类都单独建立一个*.lua文件

  3. 建立一个Load.luaMain.lua文件(Main.lua也是模拟出来的类),Main类里面有一个Start()方法

  4. Load.lua中通过require函数将其余所有的文件都require进来,然后new一个Main的对象执行Start()方法 开始整个程序

伪码如下:

1
2
3
4
5
6
7
8
9
10
11
12

require("ClassA")
require("ClassB")
require("ClassC")
...
require("ClassY")
require("ClassZ")

--都require完了以后执行:

local entry = Main.new();
entry.Start();

实际项目

因为没和其他人交流过 也不知道 这是不是个标准流程 ,不过我自己的项目是准备这么弄的.

会有一些改动 因为有部分代码应该会直接和GameObject做双向绑定,所以不一定入口文件需要将所有文件都require进来,并且入口文件也不一定会只有一个.

另外一个就是Load.lua这个文件到时候应该会根据某种配置用程序生成.

比如Lua/InitLoad/*.lua下面的文件都初始时候加载,然后Lua/Manual/*.lua下面的文件都手动加载.

或者直接用前缀做区分 Init_$ClassName.lua,Manual_$ClassName.lua

但是大体上来说,结构会按照上面所说的那样.

require 中 local的作用域

这是我当时有些没搞懂的地方,就是local 变量作用域的问题. 一个local变量 放在一个for循环里面,或者放在一个函数里面 都很好理解. 因为和其他语言都相同嘛. 就是局部变量的概念.

但是这个require是怎么个意思? 我当时就是以为require就是横向拆分

比如我就一个Main.lua

1
2
3
local A = "A"

print("A is: "..A)

输出结果肯定就是

1
A is: A

此时我把local A = "A"放到另外一个文件ClassA.lua中 然后执行

1
2
require("ClassA")
print("A is: "..A)

运行会报错

1
lua: main.lua:4: attempt to concatenate a nil value (global 'A')

我大概看了一下require的伪实现代码,应该是内部通过一个Loader进行加载,然后缓存到package.loaded中来.所以是否可以理解为

整个require到的”文件”都是在这个Load函数的作用域中(closure闭包)?

不过 无论具体实现如何 我目前就把require当做放在了一个do end的代码块中来理解了

伪码如下:

1
2
3
4
5
6
7
8
9
--require("ClassA")转意为:
do
local A = "A"
end

--Main.lua
do
print("A is: "..A)
end

看懵圈的语法

Lua内有一些语法糖(Syntactic sugar),还有一些内置的关键key. 所以初接触时候比较晕, 虽然都有所了解 但是开始时理解的还是不够深入. 就像上学时候学习新公式似的. 讲完都会 一放到具体习题里面又都懵圈了.

function

function 中的 name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Fun1=function()
print("Fun1 called")
end

function Fun2()
print("Fun2 called")
end

Fun1();
Fun2();

Wrap={}
function Wrap.WFun1()
print("Wrap Fun1 called")
end

Wrap.WFun2=function()
print("Wrap Fun2 called")
end

Wrap["WFun1"]()
Wrap.WFun2();

Lua中函数均为匿名函数,所以Fun2就可以理解为在全局声明了一个叫Fun2的变量,其引用了一个匿名函数,

同理的,Wrap.WFun1 就是在Wrap这个table中用一个WFun1作为一个Key 引用了一个匿名函数.

function 中的 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Wrap={}
function Wrap:WFun3()
print("Self is a ",self);
end

Wrap.WFun4=function(_firstArg)
print("First also is a ",_firstArg);
end


Wrap.WFun3(Wrap)
Wrap:WFun3()

Wrap.WFun4(Wrap)
Wrap:WFun4(Wrap)

local AnotherWrap = {}
AnotherWrap.HoldKey=Wrap;

Wrap.WFun3(AnotherWrap)
AnotherWrap.HoldKey:WFun3()

上面函数调用的输出结果为:

1
2
3
4
5
6
7
Self is a 	table: 0x7f8353c068f0
Self is a table: 0x7f8353c068f0
First also is a table: 0x7f8353c068f0
First also is a table: 0x7f8353c068f0

Self is a table: 0x7f8353c02690
Self is a table: 0x7f8353c068f0
  • : 在函数定义阶段则表示函数会有一个参数,然后其参数名称为self.

  • : 在函数调用阶段则表示将调用的Table作为第一个参数传入该函数

self: 的意义也仅此而已,所以用时候不要想当然 尤其在底层已经封装为伪类结构后. 我懵圈时候Google到的好些人提的问题 都是因为直接使得现成架构 然后妖魔化了 这两个关键字☹️

function 中的 …

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Fun1( ... )
local a,b,c = ...
print(string.format("Arg a=%s b=%s c=%s",a,b,c))
end

Wrap={}
function Wrap.WFun1( ... )
local arg = table.pack(...)
for k,v in pairs(arg) do
print(k,v)
end
end

Fun1("A","B","C","D")
Wrap.WFun1("A","B","C","D")
Wrap:WFun1("A","B","C","D")

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Arg a=A b=B c=C

1 A
2 B
3 C
4 D
n 4

1 table: 0x7fab78406320
2 A
3 B
4 C
5 D
n 5

...就是可变参数,必须用到函数尾部的声明 这个写匿名函数时候一般都会接触到,只不过我接触的语言有些会让你声明时候指定个名字 比如...yourArgName,然后直接函数内使用该名字就行了,Lua是直接用... 就作为名字了.

可以直接local a,b,c = ... 这样把所有参数取到, 也可以用table.pack组装成一个table(Lua5.2+),或者直接{...}装进一个匿名的table中.

教科书第四版 p62 页 说的很清楚了,就不重复了

function 中的 {}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Fun1(_arg)
for k, v in pairs(_arg) do
print(k, v)
end
end

Fun1({x = 10, y = 15, w = 1280, h = 720, title = "Hello"})

Fun1{
title = "Hello2",
x = 10,
y = 15,
w = 1280,
h = 720
}

当函数只接收一个参数,且这个参数是一个table时候 Fun1({})可以简写成 Fun1{}

metatable

metatable aka 元表 算是Lua的灵魂了吧?😓 ,这块其实只能多看 看各种人写的 从多角度去理解 我觉得是最快上手渠道.

重复制轮子无意义,我就说下我个人对其的理解吧

首先抛弃所有OOP思想,不要想那堆 就当自己刚学编程. 面向对象是什么? 完全不知道呢.

将所有的table都理解为一张A4白纸,恩,就是复印用的那个! 普通table是一张,metatable是另外一张

区分好 metatable,setmetatable:

  • metatable : 就是一个table表, 也就是一张普通的 A4白纸
  • setmetatable : 这是一个函数

setmetatable(白纸1号,白纸1号_Mt) 这个就翻译为 白纸1号 你大哥是 白纸1号_Mt 那张普通白纸. 当你有什么不懂的时候 都问他!

比如:

你说 白纸1号(PaperOne) 你和 白纸2号(PaperTwo) 加加看 我看看能弄出个啥出来! 翻译为机器码就是

1
2
3
4
5
6
7
PaperOne={}
PaperOne_Mt={}
setmetatable(PaperOne,PaperOne_Mt)

PaperTwo={}

local result = PaperOne+PaperTwo

这时候 白纸1号就不知道这个table+table是应该怎么整了,就问白纸1号_Mt,大哥这个table+table怎么搞?

这也就是元表的 算数运算关系运算的作用. 相关详细实现请看 教科书😑

index 和 newindex

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

RedPaperOne = {}
RedPaperTwo = {}
RedPaperTwo.Name = "PaperTow"

RedPaper_Mt = {}
setmetatable(RedPaperOne, RedPaper_Mt)
setmetatable(RedPaperTwo, RedPaper_Mt)

function RedPaper_Mt.__index(_table, _key)
print("红纸大哥说 : 你", _table, "没有这个Key", _key, "给你个默认值 66666 好了")
return "66666"
end

print("RedPaperOne 我的名字是: ", RedPaperOne.Name)
print("RedPaperTwo 我的名字是: ", RedPaperTwo.Name)

GreenPaperOne = {}
GreenPaperTwo = {}
GreenPaperTwo.Name = "绿2号"

GreenPaper_Mt = {}
GreenPaper_Mt.Name = "小绿"
GreenPaper_Mt.__index = GreenPaper_Mt

setmetatable(GreenPaperOne, GreenPaper_Mt)
setmetatable(GreenPaperTwo, GreenPaper_Mt)

print("GreenPaperOne 我的名字是: ", GreenPaperOne.Name)
print("GreenPaperTwo 我的名字是: ", GreenPaperTwo.Name)

BluePaperOne = {}
BluePaper_Mt = {}
BluePaper_Mt.__newindex = function (_table, _key, _value)
print("蓝纸大哥说: 你就是小蓝 别闹")
rawset(_table, _key, "小蓝")
return
end

setmetatable(BluePaperOne, BluePaper_Mt)

BluePaperOne.Name = "我是小绿"

print("BluePaperOne 我的名字是: ", BluePaperOne.Name)

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12

红纸大哥说 : 你 table: 0x7fa05a406930 没有这个Key Name 给你个默认值 66666 好了

RedPaperOne 我的名字是: 66666
RedPaperTwo 我的名字是: PaperTow

GreenPaperOne 我的名字是: 小绿
GreenPaperTwo 我的名字是: 绿2


蓝纸大哥说: 你就是小蓝 别闹
BluePaperOne 我的名字是: 小蓝

__index__newindex 都是 大哥身上两个特有的属性,一个负责小弟身上没有这个属性时候去get,另一个负责去set

小弟身上有这个属性了,小弟是根本都不会鸟大哥的,自己就处理掉了.

有些时候大哥会把这两个key定义为两个函数,大哥自己真做点事儿. 但有的时候大哥就懒,直接就 大哥.__index=大哥

此时小弟当处理不了时候问大哥: 大哥,那哥们儿要输出我身上的Name属性,我没那玩意儿啊,大哥闭着眼睑 指了指自己身上的Name=小绿那条记录. 小弟就心领神会的把”小绿” 返回了.

以上设置了一个很2B的情景,但是有些时候 碰到一些不是很容易看懂的代码 你可以是这把那段代码带入 情景,没准会更容易帮你理解.

有个补充就是__newindex内部实现使用了rawset,就是让小弟忽略__newindex实现,进行直接赋值,如果不这么写就变成自己调自己,然后无限循环下去了.

留意 setmetatable(x,{__index=y})

setmetatable(x,{__index=y}) 这行代码其实有个坑,刚开始被绕进去了. 就是这里面其实是有3张表的,就是三个白纸. 还是套用上面的例子 一个是白纸1号,另一个是白纸一号_Mt. 但是里面还有一个匿名table就是{__index=y}

翻译为2B情景就是

1
2
3
4
5
6
7
8
9
10
11
12

PaperOne={}
PaperOne_Fake_Brother={}
PaperOne_Fake_Brother.Name="我是你假大哥"

PaperReal_Brother={}
PaperReal_Brother.Name="我是你真大哥"
PaperReal_Brother.__index=PaperOne_Fake_Brother

setmetatable(PaperOne,PaperReal_Brother)

print(PaperOne.Name)

输出结果:

1
我是你假大哥
坚持原创技术分享,您的支持将鼓励我继续创作!