Lua, 从入门到接着入门

5.4k 词

0x00

之前,有介绍过如何使用 Moonsharp 在 c# 工程中加载 Lua 脚本,而这一篇,打算关注于 Lua 脚本本身,介绍 Lua 的基础,入门教程(下面就是毫不严谨的介绍与分类)。

Lua 是种被广泛应用的嵌入式脚本语言,使用脚本语言可以显著缩短传统的“编写,编译,链接,运行”(edit-compile-link-run)的程序开发过程,通常,脚本是解释运行而非编译,以易学易用的姿态解决一些简单任务。如今,脚本语言更是可以在计算机系统的各个层级都能见到,并且在许多方面,高级语言与脚本语言的界限也变得模糊,比如我们在 Unity 使用的 C# 就是一例。

今天的主角 Lua 是真的牛*,它的设计目的就是为了嵌入应用程序,为其提供一种灵活的扩展和定制功能,可以很容易的与 c/c++ 的代码相互调用。可以作为扩展脚本或者配置文件(代替 xml,ini),应用场景如我们所熟悉的爱啪啪的热更新,游戏中常见的游戏模组(mod, modification)。

0x01

这是基础

1. 数据类型

  • nil 无效值,如没有赋值的变量,也可以用来对全局变量和表(里的变量)进行删除(赋值 nil),注意一点是使用 nil 进行比较判断时要加引号,如 type(x) 为 nil,判断 type(x) == nil 为 false,tpye(x) == “nil” 为 true
  • boolean 布尔,只有 false 和 nil 为假 (没有 0 啊啊啊啊)
  • number 双精度类型实浮点数,就 double,不管是 2,2.2,2e+1,等等
  • string 字符串,单双引号,也可以用”[[]]”来表示有换行的一段字符串,字符串用“+”会尝试进行数值计算,字符串连接使用“..”,计算字符串长度,使用“#字符串”如 print(#len)
  • function c lua 编写的函数,lua 中函数被看作是“第一类值 First-Class Value”,函数可以存放在变量中,也可以以匿名函数 anonymous function 的方式作为参数传递
  • userdata 存储在变量中的 c 数据结构 以及指针
  • thread 执行的独立线路,用于执行协同程序。lua 中的协同程序 coroutine 协程,与线程 thread 差不多,拥有自己独立的栈,局部变量,指令指针,并于其他协程共享全局变量等,但最主要的区别是不能同时运行,任意时刻只有一个协程运行,处于运行状态的协程只有被挂起 suspend 时才会暂停。协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。
  • table 表,实际是关联数组 associative arrays,数组的索引可以是数字字符串表,使用构造表达式创建 table,{}表示创建一个空表,lua 表的默认初始索引不是 0 而是 1

运算符:

  • 算数运算符 + - * / % ^ -
  • 关系运算符 == ~= > < >= <=
  • 逻辑运算符 and or not
  • 其他运算符 .. #

优先级:

  1. ^
  2. not, - (unary)
  3. *, /
  4. +, -
  5. ..
  6. <, >, <=, >=, ~=, ==
  7. and
  8. or

2. 变量

  • 全局变量 全是全局的,不管在哪里
  • 局部变量 用 local 显示声明,作用域,声明位置开始到语句块结束,对局部变量的访问速度更快
  • 表中的域

多变量依次赋值 a, b = b, a 交换变量 a 和 b。

  1. 变量个数 > 值的个数 // 按变量个数补足nil
  2. 变量个数 < 值的个数 // 多余的值会被忽略

  3. 语句

控制语句

  • 循环
1
2
3
while ()
do
end
1
2
for ... do
end
1
repeat...until
1
break

4. 函数

格式:

1
2
3
4
optional_function_scope function ( argument1, argument2, argument3..., argumentn)
function_body
return result_params_comma_separated
end

如果要局部变量,显式使用 local 关键字创建变量。

支持返回多个返回值,类似 python。

5. 表

表(table)可以说是 Lua 中最重要的数据类型,可以用来构建其他数据类型,如 数组,字典;用来解决模块 module,包 package,对象 object

Lua的垃圾回收机制,在没有变量指向 table 时,会清理相对应的内存

对 table 的索引使用方括号“[]”。Lua 也提供了“.”操作。

1
2
3
t[i]
t.i
gettable_event(t,i) -- 采用索引访问本质上是一个类似这样的函数调用

6. 元表

问题:table 中可以通过访问 key 得到对应 value,但是无法对两个 table 进行操作(什么操作?两个 table 相加)

类似,操作符重载哟?

元表可以设置在表中,通过方法:

1
2
setmetatable(table, metatable)
getmetatable(table)

元表中具有元方法,实现如相加等功能:元方法 __add 字段

  • __index
  • __newindex
  • __add
  • __sub
  • __mul
  • __call
  • __tostring

0x02

这是进阶

  1. 模块与包

模块类似于封装库,Lua5.1 开始加入了标准的模块管理机制,在文件中放入公用的代码,以 API 接口的形式在其他地方调用。

模块就是一个 table,需要导出的常量和函数放到里面,返回这个 table 即可。像调用 talbe 里的元素一样调用模块里的常量和函数。

require 函数,用来加载模块, require(“<模块名>”) 或 require “<模块名>”

lua 5.2 版本之后,require不再定义全局变量,需要保存返回值。

会发生啥:返回一个由模块常量或函数组成的 table,并且还会定义一个包含该 table 的全局变量,名叫“<模块名>”。

当然,也可以加一个别名 local m = require “module”

加载存在加载机制,在 package.loadfile 中的路径来加载模块,否则就去找 c 程序库

c 包:

使用 c 为 lua 写包,在使用之前必须进行加载,连接。最方便的实现方式是通过动态链接库机制。

loadlib(path, “”)

加载指定的库并且连接到 lua,返回一个初始化函数作为一个 Lua 的函数,用以在 Lua 中直接调用。

1
2
3
4
local path = "/usr/local/lua/lib/libluasocket.so"
-- 或者 path = "C:\windows\luasocket.dll",这是 Window 平台下
local f = assert(loadlib(path, "luaopen_socket"))
f() -- 真正打开库
  1. 面向对象

封装:指能够把一个实体的信息、功能、响应都装入一个单独的对象中的特性。

继承:继承的方法允许在不改动原程序的基础上对其进行扩充,这样使得原功能得以保存,而新功能也得以扩展。这有利于减少重复编码,提高软件的开发效率。

多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。

抽象:抽象(Abstraction)是简化复杂的现实问题的途径,它可以为具体问题找到最恰当的类定义,并且可以在最恰当的继承级别解释问题。

lua 中使用 table 描述对象的属性,使用 function 表示方法,因此 table + function 模拟类

创建对象是为类的实例分配内存的过程,每个类都有属于自己的内存并共享公共数据。

访问属性 “.”

访问成员函数 “:”

有一定的区别的!

语法糖(Syntactic sugar)是由英国计算机科学家彼得·蘭丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。 语法糖让程序更加简洁,有更高的可读性。

“:” 即一种语法糖,在程序调用时,使用的 “.” 的方法第一个参数总是 self,而 “:” 可以自动将 self 作为第一个参数。

function 前置也是一个语法糖,正常流程是 变量 = function()…

继承可以通过 metatable 来模拟,(但不推荐)

__index 键(元方法,查找表中没有的键时执行的操作,这个在现在做的项目中,就是利用这个键值,到 c# 中查询暴露出来的功能。)

lua 中表在查找键对应的值时,现在表中查找,如果找到则返回值,如果没有找到键,则查看 metatable 中是否有 index 键,如果有就去 index 键对应的表中查找,找到则返回 getmetatable(p).__index.

这样 __index 中的表就有类似父类的表现。

所以在 lua 中函数重写(function override)只需要在派生类中重新定义即可,在 table 中找到,就不需要到 __index 表中查找了。

既然不推荐(原因是什么?)

“菜鸟”中使用的方法是:

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
-- Meta class
Shape = {area = 0}
-- 基础类方法 new
function Shape:new (o,side)
o = o or {}
setmetatable(o, self)
self.__index = self
side = side or 0
self.area = side*side;
return o
end
-- 基础类方法 printArea
function Shape:printArea ()
print("面积为 ",self.area)
end

-- 创建对象
myshape = Shape:new(nil,10)
myshape:printArea()

Square = Shape:new()
-- 派生类方法 new
function Square:new (o,side)
o = o or Shape:new(o,side)
setmetatable(o, self)
self.__index = self
return o
end

-- 派生类方法 printArea
function Square:printArea ()
print("正方形面积为 ",self.area)
end

-- 创建对象
mysquare = Square:new(nil,10)
mysquare:printArea()

Rectangle = Shape:new()
-- 派生类方法 new
function Rectangle:new (o,length,breadth)
o = o or Shape:new(o)
setmetatable(o, self)
self.__index = self
self.area = length * breadth
return o
end

-- 派生类方法 printArea
function Rectangle:printArea ()
print("矩形面积为 ",self.area)
end

-- 创建对象
myrectangle = Rectangle:new(nil,10,20)
myrectangle:printArea()

这里将基类实现了 new 方法,把自己作为元表,添加到派生类中,并返回派生类的对象。

1
2
3
4
5
6
7
8
9
--创建对象
--创建对象是为类的实例分配内存的过程。每个类都有属于自己的内存并共享公共数据。
r = Rectangle:new(nil,10,20)
--访问属性
--我们可以使用点号(.)来访问类的属性:
print(r.length)
--访问成员函数
--我们可以使用冒号 : 来访问类的成员函数:
r:printArea()

菜鸟教程

  1. 协程

协同程序(coroutine),简称协程。

  • coroutine.create()

    创建 coroutine,返回 coroutine, 参数是一个函数,当和 resume 配合使用的时候就唤醒函数调用

  • coroutine.resume()

    重启 coroutine,和 create 配合使用

  • coroutine.yield()

    挂起 coroutine,将 coroutine 设置为挂起状态,这个和 resume 配合使用能有很多有用的效果

  • coroutine.status()

    查看 coroutine 的状态

    注:coroutine 的状态有三种:dead,suspended,running,具体什么时候有这样的状态请参考下面的程序

  • coroutine.wrap()

    创建 coroutine,返回一个函数,一旦你调用这个函数,就进入 coroutine,和 create 功能重复

  • coroutine.running()

    返回正在跑的 coroutine,一个 coroutine 就是一个线程,当使用running的时候,就是返回一个 corouting 的线程号

协程 create 和 wrap 的区别是 create 需要调用 resume 而 wrap 可以直接调用返回的函数。

当协程执行结束,状态为 dead,当协程 yield 返回,状态为 suspended,需要调用 resume 方法让协程继续执行,当协程运行中,状态为 running

协程底层是由一个线程实现,create 方法是在线程中注册了一个事件,resume 触发事件,线程中的 coroutine 就被执行。

0x03

现在这篇文章还只能自己看,我会在后面使用经验增加之后,来增加用例和解释,就酱。

0x04

最后,当我完整完成我再说两句。