Lua, 从入门到接着入门
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
- 其他运算符 .. #
优先级:
- ^
- not, - (unary)
- *, /
- +, -
- ..
- <, >, <=, >=, ~=, ==
- and
- or
2. 变量
- 全局变量 全是全局的,不管在哪里
- 局部变量 用 local 显示声明,作用域,声明位置开始到语句块结束,对局部变量的访问速度更快
- 表中的域
多变量依次赋值 a, b = b, a 交换变量 a 和 b。
- 变量个数 > 值的个数 // 按变量个数补足nil
变量个数 < 值的个数 // 多余的值会被忽略
语句
控制语句
- 循环
1 | while () |
1 | for ... do |
1 | repeat...until |
1 | break |
4. 函数
格式:
1 | optional_function_scope function ( argument1, argument2, argument3..., argumentn) |
如果要局部变量,显式使用 local 关键字创建变量。
支持返回多个返回值,类似 python。
5. 表
表(table)可以说是 Lua 中最重要的数据类型,可以用来构建其他数据类型,如 数组,字典;用来解决模块 module,包 package,对象 object
Lua的垃圾回收机制,在没有变量指向 table 时,会清理相对应的内存
对 table 的索引使用方括号“[]”。Lua 也提供了“.”操作。
1 | t[i] |
6. 元表
问题:table 中可以通过访问 key 得到对应 value,但是无法对两个 table 进行操作(什么操作?两个 table 相加)
类似,操作符重载哟?
元表可以设置在表中,通过方法:
1 | setmetatable(table, metatable) |
元表中具有元方法,实现如相加等功能:元方法 __add 字段
- __index
- __newindex
- __add
- __sub
- __mul
- …
- __call
- __tostring
0x02
这是进阶
- 模块与包
模块类似于封装库,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 | local path = "/usr/local/lua/lib/libluasocket.so" |
- 面向对象
封装:指能够把一个实体的信息、功能、响应都装入一个单独的对象中的特性。
继承:继承的方法允许在不改动原程序的基础上对其进行扩充,这样使得原功能得以保存,而新功能也得以扩展。这有利于减少重复编码,提高软件的开发效率。
多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。
抽象:抽象(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 | -- Meta class |
这里将基类实现了 new 方法,把自己作为元表,添加到派生类中,并返回派生类的对象。
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
最后,当我完整完成我再说两句。