Lua 学习笔记

741 阅读47分钟

参考资料

The Programming Language Lua

Lua 教程 | 菜鸟教程 (runoob.com)

LuatOS 文档

Lua 简介

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua 是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组于 1993 年开发的,该小组成员有:Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo。

设计目的

其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

一般来说,我们可以使用 C/C++ 来实现功能,而用 Lua 来实现业务逻辑,这样就可以让我们的程序保持高效的同时,又能灵活快速地实现我们的业务。

Lua 特性

  • 轻量级: 它用标准C语言编写并以源代码形式开放,编译后仅仅一百余K,可以很方便的嵌入别的程序里。
  • 可扩展: Lua提供了非常易于使用的扩展接口和机制:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样。
  • 其它特性:
    • 支持面向过程(procedure-oriented)编程和函数式编程(functional programming);
    • 自动内存管理;只提供了一种通用类型的表(table),用它可以实现数组,哈希表,集合,对象;
    • 语言内置模式匹配;闭包(closure);函数也可以看做一个值;提供多线程(协同进程,并非操作系统所支持的线程)支持;
    • 通过闭包和table可以很方便地支持面向对象编程所需要的一些关键机制,比如数据抽象,虚函数,继承和重载等。

应用场景

  • 游戏开发
  • 独立应用开发
  • Web 应用开发
  • 扩展和数据库插件如:MySQL Proxy 和 MySQL WorkBench
  • 安全系统,如入侵检测系统

环境安装及运行

Windows

Github 下载 github.com/rjpcomputin…

麻瓜式安装即可,不过只有最新只有 5.1 版本的,有些新特性会不支持。

自行编译安装:

  1. www.lua.org/download.ht… 下载最新版本的 Source,我下的就是 lua-5.4.6.tar.gz
  2. github.com/skeeto/w64d… 下载 MinGW 用于编译 Lua 的源码
  3. 找一个目录比如 D:\lua_build\lua-5.4.6 创建 build.bat 内容见后文
  4. 修改一下 set lua_version=5.4.6 为你自己的版本
  5. 修改 set compiler_bin_dir=C:\MinGW-w64\mingw64\bin 为你自己的 MinGWbin目录
  6. 以管理员身份运行 build.bat
  7. 正常情况下会出现 lua 目录,就是成功了,然后把 luabin目录配置到环境变量 PATH 里面,就可以使用了
@echo off
:: ========================
:: file build.cmd
:: ========================
setlocal
:: you may change the following variable's value
:: to suit the downloaded version
set lua_version=5.4.6
 
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%\lua
set compiler_bin_dir=C:\MinGW-w64\mingw64\bin
set lua_build_dir=%work_dir%\lua-%lua_version%
 
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
 
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\*.*
 
echo.
echo **** BINARY DISTRIBUTION BUILT ****
echo.
 
%lua_install_dir%\bin\lua.exe -e"print [[Hello!]];print[[Simple Lua test successful!!!]]"
 
echo.
 
pause

Mac

curl -R -O http://www.lua.org/ftp/lua-5.4.6.tar.gz
tar zxf lua-5.4.6.tar.gz
cd lua-5.4.6
make macos test
make install

Linux

curl -R -O http://www.lua.org/ftp/lua-5.4.6.tar.gz
tar zxf lua-5.4.6.tar.gz
cd lua-5.4.6
make linux test
make install

在线环境

LuatOS 在线模拟 - lua在线测试

基本语法

交互式编程

Lua 交互式编程模式可以通过命令 lua -ilua 来启用:

lua -i
Lua 5.4.6  Copyright (C) 1994-2023 Lua.org, PUC-Rio

脚本式编程

我们可以创建一个文件 test.lua,输入:

print("Hello World!")

然后执行下面脚本运行:

lua test.lua

打印中文的时候如果出现乱码,VSCode 里面 Save with Encoding 可以改为 GBK。

编译

使用 luac.lua 编译成字节码,这些字节码可以直接在 Lua 虚拟机上运行,执行效率也更高。

基本语法:

luac [ options ] [ file ... ]
luac test.lua

执行之后将会生成一个 luac.out(这是输出文件的默认名称) 文件。

luac 的常用选项:

  • -o file:指定编译输出的文件名
  • -c:只进行预处理而不进行编译
  • -s:对源代码进行优化
  • -p:预处理源代码但不进行编译优化
luac -o test.luac teat.lua

执行之后将会生成一个 test.luac 文件。

编译之后执行方法还是一样的,用 lua 命令即可。

注释

-- 单行注释用两个减号

--[[
多行注释
多行注释
--]]

变量

变量在使用前,需要在代码中进行声明,即创建该变量。

编译程序执行代码之前编译器需要知道如何给语句变量开辟存储区,用于存储变量的值。

变量类型

Lua 变量有三种类型:全局变量、局部变量、表中的域。

Lua 中的变量全是全局变量,哪怕是语句块或是函数里,除非用 local 显式声明为局部变量。

全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是:nil。

print(b)
nil
b = 10
print(b)
10

如果想要删除全局变量,将其赋值为 nil 即可。

b = nil

局部变量的作用域为从声明位置开始到所在语句块结束。

变量的默认值均为 nil。

a = 5       -- 全局变量
local b = 5 -- 局部变量

function joke()
    c = 5       -- 全局变量
    local d = 6 -- 局部变量
end

joke()
print(c, d) --> 5 nil

do
    local a = 6 -- 局部变量
    b = 6       -- 对局部变量重新赋值
    print(a, b); --> 6 6
end

print(a, b) --> 5 6

赋值语句

Lua 可以对多个变量同时赋值,变量列表和值列表的各个元素用逗号分开,赋值语句右边的值会依次赋给左边的变量。

a, b = 10, 2

print(a, b) -- 10      2

利用上述特性进行值交换,或将函数调用返回给变量:

x, y = y, x                     -- swap 'x' for 'y'
a[i], a[j] = a[j], a[i]         -- swap 'a[i]' for 'a[j]'
a, b = f()                      -- f()返回两个值,第一个赋给a,第二个赋给b

应该尽可能的使用局部变量,有两个好处:

  1. 避免命名冲突。

  2. 访问局部变量的速度比全局变量更快。

索引

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

local myArr = {}
myArr["key1"] = "value1"
myArr["key2"] = "value2"
print(myArr["key1"])
print(myArr.key2)

输出:

value1
value2

数据类型

Lua 是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作为参数传递或结果返回。

8种基础类型如下表所示:

数据类型描述
nil空,表示无效值,在条件表达式中相当于false
boolean布尔值:true和false
number数字类型,表示双精度类型的浮点数
string字符串,单引号或双引号来表示
function由C或Lua编写的函数
userdata表示任意存储在变量中的C数据结构
thread表示执行的独立线路,用于执行协同程序
table表,其实是一个"关联数组"(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过"构造表达式"来完成,最简单构造表达式是{},用来创建一个空表。

Lua 提供一个 type 函数来检测变量或值的类型:

print(type("Hello world"))
print(type(10.4 * 3))
print(type(print))
print(type(type))
print(type(true))
print(type(nil))
print(type(type(X)))

输出:

string
number
function
function
boolean
nil
string

nil

nil 类型表示空值,给全局变量赋值 nil 等同于把它们删除。

nil 作比较时应该加上引号:

if (type(nil)==nil) then
    print(true)
else
    print(false)
end

if (type(nil)=="nil") then
    print(true)
else
    print(false)
end

输出:

false
true

boolean

布尔类型,Lua 中只把 falsenil 看作假,数字的 0 是真,这与 Javascript 等语言是不一样的。

number

默认只有一种数字类型,double 类型。

string

字符串,可以用单引号,也可以用双引号来表示。

string1 = "this is string1"
string2 = 'this is string2'

字符串块的表示可以使用 [[]]

html = [[
<html>
<head></head>
<body>
    <p>Hello World</p>
</body>
</html>
]]
print(html)

输出:

<html>
<head></head>
<body>
    <p>Hello World</p>
</body>
</html>

字符串拼接使用 ..,而不是 ++ 是用于数字类型的:

print("a" .. 'b')
print(157 .. 428)

print("2" + 6)
print("2" + "6")

输出:

ab
157428
8
8
print("abc" + 1) -- 会报错

使用 # 来获取字符串的长度:

len = "string len"
print(#len)
print(#"string len")

输出:

10
10

table

表,也是关联数组,索引可以是数字或者字符串。

-- 创建一个空的 table
local tbl1 = {}
 
-- 直接初始表
local tbl2 = {"apple", "pear", "orange", "grape"}

print(tbl1)
print(tbl2[1]) -- 注意不同于其他语言的数组把 0 作为数组的初始索引,在 Lua 里表的默认初始索引一般以 1 开始。

输出:

table: 000002109d63a0c0
apple
local a = {}
a["key"] = "value"
a[10] = 11
for k, v in pairs(a) do
    print(k .. " : " .. v)
end

输出:

key : value
10 : 11

table 不会固定长度大小,有新数据添加时 table 长度会自动增长,没初始的 table 元素 都是 nil

local a3 = {}
for i = 1, 10 do
    a3[i] = i
end
a3["key"] = "val"
print(a3["key"])
print(a3[1])
print(a3[10])
print(a3["none"])

输出:

val
1
10
nil

function

函数是被看作是"第一类值(First-Class Value)",即函数和变量之间没有区别,函数可以存在变量里。

local function factorial1(n)
    if n == 0 then
        return 1
    else
        return n * factorial1(n - 1)
    end
end
print(factorial1(5))
local factorial2 = factorial1
print(factorial2(5))

输出:

120
120

function 可以以匿名函数(anonymous function)的方式通过参数传递:

local function testFun(tab, fun)
    for k, v in pairs(tab) do
        print(fun(k, v));
    end
end


local tab = { key1 = "val1", key2 = "val2" };
testFun(tab,
    function(key, val) --匿名函数
        return key .. "=" .. val;
    end
);

输出:

key1=val1
key2=val2

thread

Lua 里,最主要的线程是协同程序(coroutine)。它跟线程(thread)差不多,拥有自己独立的栈、局部变量和指令指针,可以跟其他协同程序共享全局变量和其他大部分东西。

线程跟协程的区别:线程可以同时多个运行,而协程任意时刻只能运行一个,并且处于运行状态的协程只有被挂起(suspend)时才会暂停。

userdata

userdata 是一种用户自定义数据,用于表示一种由应用程序或 C/C++ 语言库所创建的类型,可以将任意 C/C++ 的任意数据类型的数据(通常是 struct 和 指针)存储到 Lua 变量中调用。

流程控制

if 语句

--[ 定义变量 --]
local a = 10;
--[ 使用 if 语句 --]
if (a < 20)
then
    --[ if 条件为 true 时打印以下信息 --]
    print("a 小于 20");
end
print("a 的值为:", a);

输出:

a 小于 20
a 的值为:       10

if...else 语句

--[ 定义变量 --]
local a = 100;
--[ 检查条件 --]
if (a < 20)
then
    --[ if 条件为 true 时执行该语句块 --]
    print("a 小于 20")
else
    --[ if 条件为 false 时执行该语句块 --]
    print("a 大于 20")
end
print("a 的值为 :", a)

输出:

a 大于 20
a 的值为 :      100

if...elseif...else 语句

--[ 定义变量 --]
local a = 100

--[ 检查布尔条件 --]
if (a == 10)
then
    --[ 如果条件为 true 打印以下信息 --]
    print("a 的值为 10")
elseif (a == 20)
then
    --[ if else if 条件为 true 时打印以下信息 --]
    print("a 的值为 20")
elseif (a == 30)
then
    --[ if else if condition 条件为 true 时打印以下信息 --]
    print("a 的值为 30")
else
    --[ 以上条件语句没有一个为 true 时打印以下信息 --]
    print("没有匹配 a 的值")
end
print("a 的真实值为: ", a)

输出:

没有匹配 a 的值
a 的真实值为:   100

循环

while 循环

local a = 10
while (a < 20)
do
    print("value of a:", a)
    a = a + 1
end

输出:

value of a:     10
value of a:     11
value of a:     12
value of a:     13
value of a:     14
value of a:     15
value of a:     16
value of a:     17
value of a:     18
value of a:     19

for 循环

数值for循环

for var=exp1,exp2,exp3 do  
    <执行体>  
end

varexp1 变化到 exp2,每次变化以 exp3 为步长递增,并执行一次 "执行体"。exp3 是可选的,如果不指定,默认为1。

for i = 10, 1, -1 do
    print(i)
end

输出:

10
9
8
7
6
5
4
3
2
1

泛型for循环

local a = { "one", "two", "three" }
for i, v in ipairs(a) do
    print(i, v)
end

输出:

1       one
2       two
3       three

ipairsLua 提供的一个迭代器函数,用了迭代数组

repeat...until循环

repeat
   statements
until( condition )

先执行循环体的语句,再进行条件判断,如果条件为 flase 则继续执行循环体,否则跳出循环。

local a = 10

repeat
    print("value of a:", a)
    a = a + 1
until (a > 15)

输出:

value of a:     10
value of a:     11
value of a:     12
value of a:     13
value of a:     14
value of a:     15

循环控制语句

break语句

local a = 10

while(a < 20)
do
    print("value of a:", a)
    a = a + 1
    if(a > 15) then
        break
    end
end

执行到 break 处会跳出循环。

goto语句(5.2+的特性)

语法:

goto Label

定义 Label:

::Label::
local a = 1
::label::
print("--- goto label ---")

a = a + 1
if a < 3 then
    goto label -- a 小于 3 的时候跳转到标签 label
end

输出:

--- goto label ---
--- goto label ---

如果 Label 后面需要接多条语句可以用 do...end

local a = 1
::label::
do
    print("--- goto label1 ---")
    print("--- goto label2 ---")
end

a = a + 1
if a < 3 then
    goto label -- a 小于 3 的时候跳转到标签 label
end

输出:

--- goto label1 ---
--- goto label2 ---
--- goto label1 ---
--- goto label2 ---

goto 语句经常被用来实现 continue 功能:

for i = 1, 3 do
    if i <= 2 then
        print(i, "yes continue")
        goto continue
    end
    print(i, " no continue")
    ::continue::
    print([[i'm end]])
end

输出:

1       yes continue
i'm end
2       yes continue
i'm end
3        no continue
i'm end

函数

在变量类型的 function 那里已经讲了很多函数相关的内容了,比如:函数定义、匿名函数、多返回值,这里是对其它内容再进行一些补充。

可变参数

Lua 函数可以接受可变数目的参数,和 C 语言类似,在函数参数列表中使用三点 ... 表示函数有可变的参数。

local function add(...)
    local s = 0
    for i, v in ipairs { ... } do --> {...} 表示一个由所有变长参数构成的数组
        s = s + v
    end
    return s
end

print(add(3, 4, 5, 6, 7)) --> 25

我们可以将可变参数赋值给一个变量,#argselect("#", ...)select(n, ...) 三种方式都可以获取可变参数的数量:

local function average(...)
    local result = 0
    local arg = { ... } --> arg 为一个表,局部变量
    for i, v in ipairs(arg) do
        result = result + v
    end
    print(#arg) --> 6
    print(select("#", ...)) --> 6
    -- 用于返回从起点 n 开始到结束位置的所有参数列表
    print(select(3, ...)) --> 3       4       5       6
    return result / #arg
end

print(average(10, 5, 3, 4, 5, 6)) --> 5.5

有时候我们可能需要几个固定参数加上可变参数,固定参数必须放在变长参数之前:

local function fwrite(fmt, ...)  ---> 固定的参数fmt
    return io.write(string.format(fmt, ...))
end

fwrite("abcdefg\n")    -->fmt = "abcdefg", 没有变长参数。  
fwrite("%d%d\n", 1, 2) -->fmt = "%d%d", 变长参数为 1 和 2

内置函数

Lua 也有很多内置函数,我们可以通过官方手册进行查询:Lua 5.4 Reference Manual - contents

我这里是用的 5.4 版本的:

www.lua.org_manual_5.4_.png

迭代器

迭代器(iterator)是一种对象,它能够用来遍历标准模板库容器中的部分或全部元素,每个迭代器对象代表容器中的确定的地址。在 Lua 中迭代器是一种支持指针类型的结构,它可以遍历集合的每一个元素。

在泛型 for 循环中就用到了迭代器,ipairs 就是 Lua 默认提供的迭代函数。

下面是一个实例:

local array = { "Google", "Runoob" }

for key, value in ipairs(array)
do
    print(key, value)
end

输出:

1  Google
2  Runoob

下面我们来分析一下泛型 for 的执行过程:

  1. 初始化,计算 in 后面表达式的值,表达式应该返回泛型 for 需要的三个值:迭代函数、状态常量、控制变量(ipairs(array) 会返回迭代函数、array、0);与多值赋值一样,如果表达式返回的结果个数不足三个会自动用 nil 补足,多出部分会被忽略。
  2. 将状态常量和控制变量作为参数调用迭代函数(注意:对于 for 结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)。
  3. 将迭代函数返回的值赋给变量列表。
  4. 如果返回的第一个值为 nil 循环结束,否则执行循环体。
  5. 回到第二步再次调用迭代函数。

Lua 中我们常常使用函数来描述迭代器,每次调用该函数就返回集合的下一个元素。Lua 的迭代器包含以下两种类型:

  • 无状态的迭代器
  • 多状态的迭代器

无状态的迭代器

无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。

每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器只利用这两个值可以获取下一个元素。

这种无状态迭代器的典型的简单的例子是 ipairs,它遍历数组的每一个元素((1, t[1], (2, t[2]) ...),元素的索引需要是数值。

以下实例我们使用了一个简单的函数来实现迭代器,实现 数字 n 的平方:

local function square(iteratorMaxCount, currentNumber)
    if currentNumber < iteratorMaxCount
    then
        currentNumber = currentNumber + 1
        return currentNumber, currentNumber * currentNumber
    end
end

-- 3 就是状态常量,0就是控制变量
for i, n in square, 3, 0
do
    print(i, n)
end

输出:

1       1
2       4
3       9

迭代的状态包括被遍历的表(循环过程中不会改变的状态常量)和当前的索引下标(控制变量),ipairs 和迭代函数都很简单,我们在 Lua 中可以这样实现:

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

function ipairs(a)
    return iter, a, 0
end

Lua 调用 ipairs(a) 开始循环时,他获取三个值:迭代函数 iter、状态常量 a、控制变量初始值 0;然后 Lua 调用 iter(a, 0) 返回 1, a[1](除非 a[1] = nil);第二次迭代调用 iter(a,1) 返回 2, a[2] …… 直到第一个 nil 元素。

多状态的迭代器

很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,最简单的方法是使用闭包,还有一种方法就是将所有的状态信息封装到 table 内,将 table 作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在 table 内,所以迭代函数通常不需要第二个参数。

local array = { "Google", "Runoob" }

local function elementIterator(collection)
    local index = 0
    local count = #collection
    -- 闭包函数
    return function()
        index = index + 1
        if index <= count
        then
            -- 返回迭代器的当前元素
            return collection[index]
        end
    end
end

for element in elementIterator(array)
do
    print(element)
end

输出:

Google
Runoob

运算符

Lua 的运算符与其他语言的区别不大,主要分为以下几类:

  • 算术运算符
  • 关系运算符
  • 逻辑运算符
  • 其他运算符

算术运算符

设定 A 的值为10,B 的值为 20

操作符描述实例
+加法A + B 输出结果 30
-减法A - B 输出结果 -10
*乘法A * B 输出结果 200
/除法B / A 输出结果 2
%取余B % A 输出结果 0
乘幂A^2 输出结果 100
-负号-A 输出结果 -10
//整除运算符(lua版本>=5.3)5//2 输出结果 2

关系运算符

设定 A 的值为10,B 的值为 20

操作符描述实例
==等于,检测两个值是否相等,相等返回 true,否则返回 false(A == B) 为 false。
~=不等于,检测两个值是否相等,不相等返回 true,否则返回 false(A ~= B) 为 true。
大于,如果左边的值大于右边的值,返回 true,否则返回 false(A > B) 为 false。
<小于,如果左边的值大于右边的值,返回 false,否则返回 true(A < B) 为 true。
>=大于等于,如果左边的值大于等于右边的值,返回 true,否则返回 false(A >= B) 返回 false。
<=小于等于, 如果左边的值小于等于右边的值,返回 true,否则返回 false(A <= B) 返回 true。

逻辑运算符

设定 A 的值为 true,B 的值为 false

操作符描述实例
and逻辑与操作符。 若 A 为 false,则返回 A,否则返回 B。(A and B) 为 false。
or逻辑或操作符。 若 A 为 true,则返回 A,否则返回 B。(A or B) 为 true。
not逻辑非操作符。与逻辑运算结果相反,如果条件为 true,逻辑非为 false。not(A and B) 为 true。

其他运算符

操作符描述实例
..连接两个字符串a..b ,其中 a 为 "Hello " , b 为 "World", 输出结果为 "Hello World"。
#一元运算符,返回字符串或表的长度。#"Hello" 返回 5

运算符优先级

从高到低依次为:

^
not    - (unary)
*      /       %
+      -
..
<      >      <=     >=     ~=     ==
and
or

模块与包

模块类似于一个封装库,从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。

Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。以下为创建自定义模块 module.lua,文件代码格式如下:

-- 文件名为 module.lua
-- 定义一个名为 module 的模块
module = {}

-- 定义一个常量
module.constant = "这是一个常量"

-- 定义一个函数
function module.func1()
    io.write("这是一个公有函数!\n")
end

local function func2()
    print("这是一个私有函数!")
end

function module.func3()
    func2()
end

return module

由上可知,模块的结构就是一个 table 的结构,因此可以像操作调用 table 里的元素那样来操作调用模块里的常量或函数。

上面的 func2 声明为程序块的局部变量,即表示一个私有函数,因此是不能从外部访问模块里的这个私有函数,必须通过模块里的公有函数来调用。

require 函数

上面定义的模块只需 require 函数引用进来就可以完成调用。

require("<模块名>")
或
require("<模块名>")

调用上面模块的实例:

-- 如果把 module.lua 放在 myMudule 文件夹下则可以使用 require("myMudule/module") 来引用
require("module")
print(module.constant)
module.func3()
-- 起别名使用
local m = require("module")
print(m.constant)
m.func3()

输出:

这是一个常量
这是一个私有函数!
这是一个常量
这是一个私有函数!

加载机制

对于自定义的模块,模块文件不是放在哪个文件目录都行,函数 require 有它自己的文件路径加载策略,它会尝试从 Lua 文件或 C 程序库中加载模块。

require 用于搜索 Lua 文件的路径是存放在全局变量 package.path 中,当 Lua 启动后,会以环境变量 LUA_PATH 的值来初始这个环境变量。如果没有找到该环境变量,则使用一个编译时定义的默认路径来初始化。

我们可以打印一下 package.path 的内容:

print(package.path)

以 windows 为例,我当前环境变量 LUA_PATH 配置为 ;;D:\mycode\lua?.luac

文件路径以 ";" 号分隔,最后的 2 个 ";;" 表示新加的路径后面加上原来的默认路径。

输出:

D:\mycode\lua\bin\lua\?.lua;D:\mycode\lua\bin\lua\?\init.lua;D:\mycode\lua\bin\?.lua;D:\mycode\lua\bin\?\init.lua;D:\mycode\lua\bin\..\share\lua\5.4\?.lua;D:\mycode\lua\bin\..\share\lua\5.4\?\init.lua;.\?.lua;.\?\init.lua;D:\mycode\lua?.luac

那么调用 require("module") 时就会尝试打开以下文件目录去搜索目标:

D:\mycode\lua\bin\lua\?.lua;
D:\mycode\lua\bin\lua\?\init.lua;
D:\mycode\lua\bin\?.lua;
D:\mycode\lua\bin\?\init.lua;
D:\mycode\lua\bin\..\share\lua\5.4\?.lua;
D:\mycode\lua\bin\..\share\lua\5.4\?\init.lua;
.\?.lua;
.\?\init.lua;
D:\mycode\lua?.luac

如果找过目标文件,则会调用 package.loadfile 来加载模块。否则,就会去找 C 程序库。

搜索的文件路径是从全局变量 package.cpath 获取,而这个变量则是通过环境变量 LUA_CPATH 来初始。

搜索的策略跟上面的一样,只不过现在换成搜索的是 sodll 类型的文件。如果找得到,那么 require 就会通过 package.loadlib 来加载它。

C 包

Lua和C是很容易结合的,使用 C 为 Lua 写包。

与Lua中写包不同,C包在使用以前必须首先加载并连接,在大多数系统中最容易的实现方式是通过动态连接库机制。

Lua在一个叫 package.loadlib 的函数内提供了所有的动态连接的功能。这个函数有两个参数:库的绝对路径和初始化函数。所以典型的调用的例子如下:

local path = "/usr/local/lua/lib/libluasocket.so"
local f = package.loadlib(path, "luaopen_socket")

package.loadlib 函数加载指定的库并且连接到 Lua,然而它并不打开库(也就是说没有调用初始化函数),反之他返回初始化函数作为 Lua 的一个函数,这样我们就可以直接在Lua中调用他。

如果加载动态库或者查找初始化函数时出错,package.loadlib 将返回 nil 和错误信息。我们可以修改前面一段代码,使其检测错误然后调用初始化函数:

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

一般情况下我们期望二进制的发布库包含一个与前面代码段相似的 stub 文件,安装二进制库的时候可以随便放在某个目录,只需要修改 stub 文件对应二进制库的实际路径即可。

将 stub 文件所在的目录加入到 LUA_PATH,这样设定后就可以使用 require 函数加载 C 库了。

元表(Metatable)

在 Lua table 中我们可以访问对应的 key 来得到 value 值,但是却无法对两个 table 进行操作(比如相加)。

因此 Lua 提供了元表(Metatable),允许我们改变 table 的行为,每个行为关联了对应的元方法。

例如,使用元表我们可以定义 Lua 如何计算两个 table 的相加操作 a+b。

当 Lua 试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫 __add 的字段,若找到,则调用对应的值。 __add 等即时字段,其对应的值(往往是一个函数或是 table)就是"元方法"。

有两个很重要的函数来处理元表:

  • setmetatable(table, metatable):对指定 table 设置元表(metatable),如果元表(metatable)中存在 __metatable 键值,setmetatable 会失败。
  • getmetatable(table): 返回对象的元表(metatable)。

元表的本质其实就是给我们提供了一些方法来改变表的默认行为,通过修改元方法就可以达到修改这些默认行为的目的,下面是一些典型的元方法示例。

__index 元方法

最常用的元方法,定义键值访问的指向

local other = { foo = 3 }

local t1 = setmetatable({}, { __index = other })
print(t1.foo, t1.bar) --> 3       nil


local t2 = setmetatable({ bar = 1 }, { __index = other })
print(t2.foo, t2.bar) --> 3       1
local mytable = setmetatable({ key1 = "value1" }, {
    __index = function(mytable, key)
        if key == "key2" then
            return "metatablevalue"
        else
            return nil
        end
    end
})
print(mytable.key1, mytable.key2) --> value1  metatablevalue

__newindex 元方法

__newindex 元方法用来对表更新,__index 则用来对表访问 。

当你给表的一个缺少的索引赋值,解释器就会查找 __newindex 元方法:如果存在则调用这个函数而不进行赋值操作。

以下实例演示了 __newindex 元方法的应用

local mymetatable = {}
local mytable = setmetatable({ key1 = "value1" }, { __newindex = mymetatable })

print(mytable.key1) --> value1

mytable.newkey = "新值2"
print(mytable.newkey, mymetatable.newkey) --> nil    新值2

mytable.key1 = "新值1"
print(mytable.key1, mymetatable.key1) --> 新值1    nil

正常情况下,给 newkey 赋值是能正常赋值进去的,但是这里却没有赋值进去,原因就是被 __newindex 拦截了,对已存在的 key1 还是可以正常赋值。

local mytable = setmetatable({ key1 = "value1" }, {
    __newindex = function(mytable, key, value)
        rawset(mytable, key, "\"" .. value .. "\"")
    end
})

mytable.key1 = "new value"
mytable.key2 = 4

print(mytable.key1, mytable.key2) --> new value    "4"

操作符相关元方法

两表相加操作:

-- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用
-- 自定义计算表中最大键值函数 table_maxn,即返回表最大键值
local function table_maxn(t)
    local mn = 0
    for k, v in pairs(t) do
        if mn < k then
            mn = k
        end
    end
    return mn
end

-- 两表相加操作
local mytable = setmetatable({ 1, 2, 3 }, {
    __add = function(mytable, newtable)
        for i = 1, table_maxn(newtable) do
            table.insert(mytable, table_maxn(mytable) + 1, newtable[i])
        end
        return mytable
    end
})

local secondtable = { 4, 5, 6 }

mytable = mytable + secondtable
for k, v in ipairs(mytable) do
    print(k, v)
end

其它操作符对应的元方法:

模式描述
__add对应的运算符 '+'.
__sub对应的运算符 '-'.
__mul对应的运算符 '*'.
__div对应的运算符 '/'.
__mod对应的运算符 '%'.
__unm对应的运算符 '-'.
__concat对应的运算符 '..'.
__eq对应的运算符 '=='.
__lt对应的运算符 '<'.
__le对应的运算符 '<='.

__call 元方法

__call 元方法在 Lua 调用一个值时调用。以下实例演示了计算表中元素的和:

-- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用
-- 自定义计算表中最大键值函数 table_maxn,即计算表的元素个数
local function table_maxn(t)
    local mn = 0
    for k, v in pairs(t) do
        if mn < k then
            mn = k
        end
    end
    return mn
end

-- 定义元方法__call
local mytable = setmetatable({ 10 }, {
    __call = function(mytable, newtable)
        local sum = 0
        for i = 1, table_maxn(mytable) do
            sum = sum + mytable[i]
        end
        for i = 1, table_maxn(newtable) do
            sum = sum + newtable[i]
        end
        return sum
    end
})
local newtable = { 10, 20, 30 }
print(mytable(newtable)) --> 70

__tostring 元方法

__tostring 元方法用于修改表的输出行为。以下实例我们自定义了表的输出内容:

local mytable = setmetatable({ 10, 20, 30 }, {
    __tostring = function(mytable)
        local sum = 0
        for k, v in pairs(mytable) do
            sum = sum + v
        end
        return "表所有元素的和为 " .. sum
    end
})
print(mytable) --> 表所有元素的和为 60

协同程序(coroutine)

什么是协同(coroutine)

Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。

协同程序可以理解为一种特殊的线程,可以暂停和恢复其执行,从而允许非抢占式的多任务处理。

协同是非常强大的功能,但是用起来也很复杂。

基本语法

同程序由 coroutine 模块提供支持。

使用协同程序,你可以在函数中使用 coroutine.create 创建一个新的协同程序对象,并使用 coroutine.resume 启动它的执行。协同程序可以通过调用 coroutine.yield 来主动暂停自己的执行,并将控制权交还给调用者。

方法描述
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的时候,就是返回一个 coroutine 的线程号

下面是一个基础实例:

local function foo()
    print("协同程序 foo 开始执行")
    local value = coroutine.yield("暂停 foo 的执行")
    print("协同程序 foo 恢复执行,传入的值为: " .. tostring(value))
    print("协同程序 foo 结束执行")
end

-- 创建协同程序
local co = coroutine.create(foo)

-- 启动协同程序
local status, result = coroutine.resume(co) -- resume 其实就是开始执行,没这句 foo 不会执行
print(result)     -- 输出: 暂停 foo 的执行

-- 恢复协同程序的执行,并传入一个值
status, result = coroutine.resume(co, 42)
print(result)     -- 输出: 协同程序 foo 恢复执行,传入的值为: 42

输出:

协同程序 foo 开始执行
暂停 foo 的执行
协同程序 foo 恢复执行,传入的值为: 42
协同程序 foo 结束执行
nil

另一个实例:

-- 创建了一个新的协同程序对象 co,其中协同程序函数打印传入的参数 i
local co = coroutine.create(
    function(i)
        print(i);
    end
)
-- 使用 coroutine.resume 启动协同程序 co 的执行,并传入参数 1。协同程序开始执行,打印输出为 1
coroutine.resume(co, 1) -- 1

-- 通过 coroutine.status 检查协同程序 co 的状态,输出为 dead,表示协同程序已经执行完毕
print(coroutine.status(co)) -- dead

print("----------")

-- 使用 coroutine.wrap 创建了一个协同程序包装器,将协同程序函数转换为一个可直接调用的函数对象
local co = coroutine.wrap(
    function(i)
        print(i);
    end
)

co(1)

print("----------")
-- 创建了另一个协同程序对象 co2,其中的协同程序函数通过循环打印数字 1 到 10,在循环到 3 的时候输出当前协同程序的状态和正在运行的线程
co2 = coroutine.create(
    function()
        for i = 1, 10 do
            print(i)
            if i == 3 then
                print(coroutine.status(co2)) --running
                print(coroutine.running())   --thread:XXXXXX
            end
            coroutine.yield()
        end
    end
)

-- 连续调用 coroutine.resume 启动协同程序 co2 的执行
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3

-- 通过 coroutine.status 检查协同程序 co2 的状态,输出为 suspended,表示协同程序暂停执行
print(coroutine.status(co2)) -- suspended
print(coroutine.running())

print("----------")

输出:

1
dead
----------
1
----------
1
2
3
running
thread: 0x7fa560f0a0d8  false
suspended
thread: 0x7fa561008808  true
----------

线程和协同程序区别

线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。

在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。

协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。

主要区别归纳如下:

  • 调度方式:线程通常由操作系统的调度器进行抢占式调度,操作系统会在不同线程之间切换执行权。而协同程序是非抢占式调度的,它们由程序员显式地控制执行权的转移。
  • 并发性:线程是并发执行的,多个线程可以同时运行在多个处理器核心上,或者通过时间片轮转在单个核心上切换执行。协同程序则是协作式的,只有一个协同程序处于运行状态,其他协同程序必须等待当前协同程序主动放弃执行权。
  • 内存占用:线程通常需要独立的堆栈和上下文环境,因此线程的创建和销毁会带来额外的开销。而协同程序可以共享相同的堆栈和上下文,因此创建和销毁协同程序的开销较小。
  • 数据共享:线程之间可以共享内存空间,但需要注意线程安全性和同步问题。协同程序通常通过参数传递和返回值来进行数据共享,不同协同程序之间的数据隔离性较好。
  • 调试和错误处理:线程通常在调试和错误处理方面更复杂,因为多个线程之间的交互和并发执行可能导致难以调试的问题。协同程序则在调试和错误处理方面相对简单,因为它们是由程序员显式地控制执行流程的。

总体而言,线程适用于需要并发执行的场景,例如在多核处理器上利用并行性加快任务的执行速度。而协同程序适用于需要协作和协调的场景,例如状态机、事件驱动编程或协作式任务处理。选择使用线程还是协同程序取决于具体的应用需求和编程模型。

Lua 是单线程的。

文件 I/O

I/O 库用于读取和处理文件,分为简单模式和完全模式:

  • 简单模式(simple model)拥有一个当前输入文件和一个当前输出文件,并且提供针对这些文件相关的操作。
  • 完全模式(complete model)使用外部的文件句柄来实现。它以一种面对对象的形式,将所有的文件操作定义为文件句柄的方法。简单模式在做一些简单的文件操作时较为合适。但是在进行一些高级的文件操作的时候,简单模式就显得力不从心。例如同时读取多个文件这样的操作,使用完全模式则较为合适。

基本语法:

file = io.open (filename [, mode])

mode 的值如下表所示:

模式描述
r以只读方式打开文件,该文件必须存在。
w打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。
a以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)
r+以可读写方式打开文件,该文件必须存在。
w+打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
a+与a类似,但此文件可读可写
b二进制模式,如果文件是二进制文件,可以加上b
+号表示对文件既可以读也可以写

简单模式

简单模式使用标准的 I/O 或使用一个当前输入文件和一个当前输出文件。

-- 以只读方式打开文件
local file = io.open("test.lua", "r")

-- 设置默认输入文件为 test.lua
io.input(file)

-- 输出文件第一行
print(io.read())

-- 关闭打开的文件
io.close(file)

-- 以附加的方式打开只写文件
file = io.open("test.lua", "a")

-- 设置默认输出文件为 test.lua
io.output(file)

-- 在文件最后一行添加 Lua 注释
io.write("-- ?test.lua 文件末尾注释")

-- 关闭打开的文件
io.close(file)

io.read 的参数:

模式描述
"n"读取一个数字并返回它。例:file.read("n")
"a"从当前位置读取整个文件。例:file.read("a")
"l"(默认)读取下一行,会跳过行尾,在文件尾 (EOF) 处返回 nil。例:file.read("l")
"L"读取下一行,不会跳过行尾字符,在文件尾 (EOF) 处返回 nil。例:file.read("L")
number返回一个指定字符个数的字符串,或在 EOF 时返回 nil。例:file.read(5)

io 的其它常用方法还有:

  • io.tmpfile():返回一个临时文件句柄,该文件以更新模式打开,程序结束时自动删除
  • io.type(file):检测obj是否一个可用的文件句柄
  • io.flush():向文件写入缓冲中的所有数据
  • io.lines(optional file name):返回一个迭代函数,每次调用将获得文件中的一行内容,当到文件尾时,将返回 nil,但不关闭文件。

完全模式

通常我们需要在同一时间处理多个文件。我们需要使用 file:function_name 来代替 io.function_name 方法。以下实例演示了如何同时处理同一个文件:

-- 以只读方式打开文件
local file1 = io.open("test.lua", "r")
local file2 = io.open("test.md", "r")

-- 输出文件第一行
print(file1:read())
print(file2:read())

-- 关闭打开的文件
file1:close()
file2:close()

其它常用方法:

  • file:seek(optional whence, optional offset):设置和获取当前文件位置,成功则返回最终的文件位置(按字节),失败则返回nil加错误信息。参数 whence 值可以是:

    • "set":从文件头开始
    • "cur":从当前位置开始[默认]
    • "end":从文件尾开始
    • offset:默认为0

    不带参数 file:seek() 则返回当前位置,file:seek("set")则定位到文件头,file:seek("end") 则定位到文件尾并返回文件大小

  • file:flush():向文件写入缓冲中的所有数据

  • io.lines(optional file name):打开指定的文件 filename 为读模式并返回一个迭代函数,每次调用将获得文件中的一行内容,当到文件尾时,将返回 nil,并自动关闭文件。

若不带参数时 io.lines() <=> io.input():lines(); 读取默认输入设备的内容,但结束时不关闭文件,如:

for line in io.lines("test.lua") do
    print(line)
end

以下实例使用了 seek 方法,定位到文件倒数第 10 个位置并使用 read 方法的 a 参数,即从当前位置(倒数第 10 个位置)读取整个文件。

file = io.open("test.lua", "r")

file:seek("end", -10)
print(file:read("*a"))

-- 关闭打开的文件
file:close()

错误处理

程序运行中错误处理是必要的,在我们进行文件操作,数据转移及web service 调用过程中都会出现不可预期的错误。如果不注重错误信息的处理,就会造成信息泄露,程序无法运行等情况。

任何程序语言中,都需要错误处理。错误类型有:

  • 语法错误
  • 运行错误

语法错误

语法错误比较简单,在运行时就会直接输出错误,Lua 的错误提示还是比较清晰的,比较好排查,开发调试阶段就可以解决这些问题。

比如:

for a= 1, 10
    print(a)
end

缺少了 do

test.lua:2: 'do' expected near 'print'

运行错误

运行错误是程序可以正常执行,但是会输出报错信息。如下实例由于参数输入错误,程序执行时报错:

local function add(a, b)
    return a + b
end

add(10)

输出:

test.lua:2: attempt to perform arithmetic on a nil value (local 'b')
stack traceback:
        test.lua:2: in local 'add'
        test.lua:5: in main chunk
        [C]: in ?

lua 里调用函数时,即使实参列表和形参列表不一致也能成功调用,多余的参数会被舍弃,缺少的参数会被补为 nil。

以上报错信息是由于参数 b 被补为 nil 后,nil 参与了 + 运算。

假如 add 函数内不是 return a+b 而是 print(a,b) 的话,结果会变成 10 nil 不会报错。

错误处理

主要用到两个函数 asserterror

assert 函数

local function add(a, b)
    assert(type(a) == "number", "a 不是一个数字")
    assert(type(b) == "number", "b 不是一个数字")
    return a + b
end
add(10)

输出:

lua: test.lua:3: b 不是一个数字
stack traceback:
        [C]: in function 'assert'
        test.lua:3: in local 'add'
        test.lua:6: in main chunk
        [C]: in ?

实例中 assert 首先检查第一个参数,若没问题,assert 不做任何事情;否则,assert 以第二个参数作为错误信息抛出。

error 函数

功能:终止正在执行的函数,并返回 message 的内容作为错误信息(error 函数永远都不会返回)

通常情况下,error 会附加一些错误位置的信息到 message 头部。

Level参数指示获得错误的位置:

  • Level = 1[默认]:为调用 error 位置(文件 + 行号)
  • Level = 2:指出哪个调用 error 的函数的函数
  • Level = 0:不添加错误位置信息
local function errorLv1()
    error("Level1 错误")
end

errorLv1()

输出:

lua: test.lua:2: Level1 错误
stack traceback:
        [C]: in function 'error'
        test.lua:2: in local 'errorLv1'
        test.lua:5: in main chunk
        [C]: in ?
local function errorLv2()
    error("Level2 错误", 2)
end

errorLv2()

输出:

lua: test.lua:5: Level2 错误
stack traceback:
        [C]: in function 'error'
        test.lua:2: in local 'errorLv2'
        test.lua:5: in main chunk
        [C]: in ?
local function errorLv0()
    error("Level0 错误", 0)
end

errorLv0()

输出:

lua: Level0 错误
stack traceback:
        [C]: in function 'error'
        test.lua:2: in local 'errorLv0'
        test.lua:5: in main chunk
        [C]: in ?

pcall 和 xpcall、debug

函数 pcall(protected call)来包装需要执行的代码。

pcall 接收一个函数和要传递给后者的参数,并执行,执行结果:有错误、无错误;返回值 true 或者或 false, errorinfo。

语法如下:

if pcall(function_name, ….) then
-- 没有错误
else
-- 一些错误
end
print(pcall(function(i) print(i) end, 33))
-- 33
-- true
print(pcall(function(i) print(i) error('error..') end, 33))
-- 33
-- false   test.lua:1: error..

pcall 以一种"保护模式"来调用第一个参数,因此 pcall 可以捕获函数执行中的任何错误。

通常在错误发生时,希望获得更多的调试信息,而不只是发生错误的位置。但 pcall 返回时,它已经销毁了调用桟的部分内容。

Lua 提供了 xpcall 函数,xpcall 接收第二个参数——一个错误处理函数,当错误发生时,Lua 会在调用桟展开(unwind)前调用错误处理函数,于是就可以在这个函数中使用 debug 库来获取关于错误的额外信息了。

debug 提供了两个通用的错误处理函数:

  • debug.debug:提供一个Lua提示符,让用户来检查错误的原因,会出现一个 lua_debug 交互环境
  • debug.traceback:根据调用桟来构建一个扩展的错误消息
print(
    xpcall(function(i) print(i) error('error..') end, function() print(debug.traceback()) end, 33)
)

输出:

33
stack traceback:
        test.lua:2: in function <test.lua:2>
        [C]: in function 'error'
        test.lua:2: in function <test.lua:2>
        [C]: in function 'xpcall'
        test.lua:2: in main chunk
        [C]: in ?
false   nil
function myfunction()
    n = n / nil
end

function myerrorhandler(err)
    print("ERROR:", err)
end

status = xpcall(myfunction, myerrorhandler)
print(status)

输出:

ERROR:  test.lua:2: attempt to perform arithmetic on a nil value (global 'n')
false

调试 debug

Lua 提供了 debug 库用于提供创建我们自定义调试器的功能。Lua 本身并未有内置的调试器,但很多开发者共享了他们的 Lua 调试器代码。

debug 库包含以下函数:

  • debug(): 进入一个用户交互模式,运行用户输入的每个字符串。 使用简单的命令以及其它调试设置,用户可以检阅全局变量和局部变量, 改变变量的值,计算一些表达式,等等。
  • getfenv(object):返回对象的环境变量。
  • gethook(optional thread):返回三个表示线程钩子设置的值: 当前钩子函数,当前钩子掩码,当前钩子计数。
  • getinfo([thread,] f [, what]):返回关于一个函数信息的表。 你可以直接提供该函数, 也可以用一个数字 f 表示该函数。 数字 f 表示运行在指定线程的调用栈对应层次上的函数: 0 层表示当前函数(getinfo 自身); 1 层表示调用 getinfo 的函数 (除非是尾调用,这种情况不计入栈);等等。 如果 f 是一个比活动函数数量还大的数字, getinfo 返回 nil。
  • debug.getlocal ([thread,] f, local):此函数返回在栈的 f 层处函数的索引为 local 的局部变量 的名字和值。 这个函数不仅用于访问显式定义的局部变量,也包括形参、临时变量等。
  • getmetatable(value):把给定索引指向的值的元表压入堆栈。如果索引无效,或是这个值没有元表,函数将返回 0 并且不会向栈上压任何东西。
  • getregistry():返回注册表表,这是一个预定义出来的表, 可以用来保存任何 C 代码想保存的 Lua 值。
  • getupvalue (f, up):此函数返回函数 f 的第 up 个上值的名字和值。 如果该函数没有那个上值,返回 nil 。以 '(' (开括号)打头的变量名表示没有名字的变量 (去除了调试信息的代码块)。
  • sethook ([thread,] hook, mask [, count]):将一个函数作为钩子函数设入。 字符串 mask 以及数字 count 决定了钩子将在何时调用。 掩码是由下列字符组合成的字符串,每个字符有其含义:
    • 'c':每当 Lua 调用一个函数时,调用钩子;
    • 'r':每当 Lua 从一个函数内返回时,调用钩子;
    • 'l':每当 Lua 进入新的一行时,调用钩子。
  • setlocal ([thread,] level, local, value):这个函数将 value 赋给 栈上第 level 层函数的第 local 个局部变量。 如果没有那个变量,函数返回 nil 。 如果 level 越界,抛出一个错误。
  • setmetatable (value, table):将 value 的元表设为 table (可以是 nil)。 返回 value。
  • setupvalue (f, up, value):这个函数将 value 设为函数 f 的第 up 个上值。 如果函数没有那个上值,返回 nil 否则,返回该上值的名字。
  • traceback ([thread,] [message [, level]]):如果 message 有,且不是字符串或 nil, 函数不做任何处理直接返回 message。 否则,它返回调用栈的栈回溯信息。 字符串可选项 message 被添加在栈回溯信息的开头。 数字可选项 level 指明从栈的哪一层开始回溯 (默认为 1 ,即调用 traceback 的那里)。
function myfunction()
    print(debug.traceback("Stack trace"))
    debug.getinfo(1)
    print("Stack trace end")
    return 10
end

myfunction()

输出:

Stack trace
stack traceback:
        test.lua:2: in function 'myfunction'
        test.lua:8: in main chunk
        [C]: in ?
Stack trace end
function newCounter()
    local n = 0
    local k = 0
    return function()
        k = n
        n = n + 1
        return n
    end
end

counter = newCounter()
print(counter())
print(counter())

local i = 1

repeat
    name, val = debug.getupvalue(counter, i)
    if name then
        print("index", i, name, "=", val)
        if (name == "n") then
            debug.setupvalue(counter, 2, 10)
        end
        i = i + 1
    end -- if
until not name

print(counter())

输出:

1
2
index    1    k    =    1
index    2    n    =    2
11

垃圾回收

Lua 采用了自动内存管理。 这意味着你不用操心新创建的对象需要的内存如何分配出来, 也不用考虑在对象不再被使用后怎样释放它们所占用的内存。

Lua 运行了一个垃圾收集器来收集所有死对象 (即在 Lua 中不可能再访问到的对象)来完成自动内存管理的工作。 Lua 中所有用到的内存,如:字符串、表、用户数据、函数、线程、 内部结构等,都服从自动管理。

Lua 实现了一个增量标记-扫描收集器。 它使用这两个数字来控制垃圾收集循环: 垃圾收集器间歇率和垃圾收集器步进倍率。 这两个数字都使用百分数为单位 (例如:值 100 在内部表示 1 )。

垃圾收集器间歇率控制着收集器需要在开启新的循环前要等待多久。 增大这个值会减少收集器的积极性。 当这个值比 100 小的时候,收集器在开启新的循环前不会有等待。 设置这个值为 200 就会让收集器等到总内存使用量达到 之前的两倍时才开始新的循环。

垃圾收集器步进倍率控制着收集器运作速度相对于内存分配速度的倍率。 增大这个值不仅会让收集器更加积极,还会增加每个增量步骤的长度。 不要把这个值设得小于 100 , 那样的话收集器就工作的太慢了以至于永远都干不完一个循环。 默认值是 200 ,这表示收集器以内存分配的"两倍"速工作。

如果你把步进倍率设为一个非常大的数字 (比你的程序可能用到的字节数还大 10% ), 收集器的行为就像一个 stop-the-world 收集器。 接着你若把间歇率设为 200 , 收集器的行为就和过去的 Lua 版本一样了: 每次 Lua 使用的内存翻倍时,就做一次完整的收集。

Lua 提供了以下函数**collectgarbage ([opt [, arg]])**用来控制自动内存管理:

  • collectgarbage("collect"): 做一次完整的垃圾收集循环。通过参数 opt 它提供了一组不同的功能:
  • collectgarbage("count"): 以 K 字节数为单位返回 Lua 使用的总内存数。 这个值有小数部分,所以只需要乘上 1024 就能得到 Lua 使用的准确字节数(除非溢出)。
  • collectgarbage("restart"): 重启垃圾收集器的自动运行。
  • collectgarbage("setpause"): 将 arg 设为收集器的 间歇率。 返回 间歇率 的前一个值。
  • collectgarbage("setstepmul"): 返回 步进倍率 的前一个值。
  • collectgarbage("step"): 单步运行垃圾收集器。 步长"大小"由 arg 控制。 传入 0 时,收集器步进(不可分割的)一步。 传入非 0 值, 收集器收集相当于 Lua 分配这些多(K 字节)内存的工作。 如果收集器结束一个循环将返回 true 。
  • collectgarbage("stop"): 停止垃圾收集器的运行。 在调用重启前,收集器只会因显式的调用运行。
mytable = { "apple", "orange", "banana" }

print(collectgarbage("count"))

mytable = nil

print(collectgarbage("count"))

print(collectgarbage("collect"))

print(collectgarbage("count"))

输出:

21.47265625
21.5078125
0
20.966796875

面向对象

我们知道,对象由属性和方法组成。Lua 中最基本的结构是 table,所以需要用 table 来描述对象的属性。

Lua 中的 function 可以用来表示方法。那么 Lua 中的类可以通过 table + function 模拟出来。

至于继承,可以通过 metetable 模拟出来(不推荐用,只模拟最基本的对象大部分实现够用了)。

Lua 中的表不仅在某种意义上是一种对象。像对象一样,表也有状态(成员变量);也有与对象的值独立的本性,特别是拥有两个不同值的对象(table)代表两个不同的对象;一个对象在不同的时候也可以有不同的值,但他始终是一个对象;与对象类似,表的生命周期与其由什么创建、在哪创建没有关系。对象有他们的成员函数,表也有:

Account = { balance = 0 }

function Account.withdraw(v)
    Account.balance = Account.balance - v
    print(Account.balance) -- -100.00
end

Account.withdraw(100.00)
-- 元类
Rectangle = { area = 0, length = 0, breadth = 0 }

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

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

local r = Rectangle:new(nil, 10, 20)

r:printArea() 
print(r.length) -- 10
print(r.breadth) -- 20
print(r.area) -- 200

继承

继承是指一个对象直接使用另一对象的属性和方法。可用于扩展基础类的属性和方法。

-- 基类
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

-- 创建Square类的对象
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

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

输出:

面积为  100
正方形面积为    100
矩形面积为      200

LuaRocks

LuaRocks 是一个管理第三方 Lua 包的工具。

安装方法直接参考官方文档:LuaRocks - The Lua package manager

安装好之后我们再来安装一个连接数据库的包:LuaSQL

luarocks install luasql-sqlite3

连接本地 sqlite3 数据库

luasql = require "luasql.sqlite3"

--创建环境对象
env = luasql.sqlite3()

--连接数据库
conn = env:connect('/Users/eagleye/Desktop/MySQLite3')

--执行数据库操作
cur = conn:execute("select * from my")

row = cur:fetch({}, "a")

--文件对象的创建
file = io.open("my.txt", "w+");

while row do
    var = string.format("%d %s\n", row.c1, row.c2)

    print(var)

    file:write(var)

    row = cur:fetch(row, "a")
end


file:close() --关闭文件对象
conn:close() --关闭数据库连接
env:close()  --关闭数据库环境