Lua 简介

Lua 是一个小巧的脚本语言。是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组并于 1993 年开发。其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua 由标准 C 编写而成,几乎在所有操作系统和平台上都可以编译、运行。Lua 并没有提供强大的库,这是由它的定位决定的。所以 Lua 不适合作为开发独立应用程序的语言。Lua 有一个同时进行的 JIT 项目,提供在特定平台上的即时编译功能

  • Lua 脚本可以很容易的被 C/C++ 代码调用,也可以反过来调用 C/C++ 的函数,这使得 Lua 在应用程序中可以被广泛应用

  • 不仅仅作为扩展脚本,也可以作为普通的配置文件,代替 XML、ini 等文件格式,并且更容易理解和维护

  • 标准 Lua 5.1 解释器由标准 C 编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译和运行;

  • 一个完整的标准 Lua 5.1 解释器不足 200 KB。而本书推荐使用的 LuaJIT 2 的代码大小也只有不足 500 KB

  • 同时也支持大部分常见的体系结构。在目前所有脚本语言引擎中,LuaJIT 2 实现的速度应该算是最快的之一。这一切都决定了 Lua 是作为嵌入式脚本的最佳选择。

Lua 语言的各个版本是不相兼容的。因此本书只介绍 Lua 5.1 语言,这是为标准 Lua 5.1 解释器和 LuaJIT 2 所共同支持的。LuaJIT 支持的对 Lua 5.1 向后兼容的 Lua 5.2 和 Lua 5.3 的特性,我们也会在方便的时候予以介绍。

Lua 环境搭建

http://openresty.org

Helloworld

# cat hello.lua
print("hello world")
# luajit hello.lua
hello world

基本数据类型

print(type("helloworld"))
print(type('helloworld'))
print(type('true'))
print(type(1))
print(type(2.1))
print(type(nil))
function hello()
    print("hello")
end
print(type(hello))

输出

string
string
string
number
number
nil
function

Nil

Nil 是一种类型,Lua 将 nil 用于表示“无效值”。

  • 一个变量在第一次赋值前的默认值是 nil,

  • 将 nil 赋予给一个全局变量就等同于删除它。

local num
print(num)        -->output:nil
 
num = 100
print(num)        -->output:100

Boolean (布尔)

布尔类型,可选值 true/false;

  • Lua 中 nil 和 false 为“假”

  • 其它所有值均为“真”。比如 0 和空字符串就是“真”;

local a = true
local b = 0
local c = nil
if a then
    print("a")        -->output:a
else
    print("not a")    --这个没有执行
end
 
if b then
    print("b")        -->output:b
else
    print("not b")    --这个没有执行
end
 
if c then
    print("c")        --这个没有执行
else
    print("not c")    -->output:not c
end

number(数字)

Number 类型用于表示实数,和 C/C++ 里面的 double 类型很类似。可以使用数学函数 math. Floor(向下取整)和 math. Ceil(向上取整)进行取整操作。

一般地,Lua 的 number 类型就是用双精度浮点数来实现的。值得一提的是,LuaJIT 支持所谓的“dual-number”(双数)模式,

  • LuaJIT 会根据上下文用整型来存储整数,而用双精度浮点数来存放浮点数。
local order = 3.99
local score = 98.01
print(math.floor(order))   -->output:3
print(math.ceil(score))    -->output:99
print(9223372036854775807LL - 1)  -->output:9223372036854775806LL

String(字符串)

Lua 中有三种方式表示字符串:

  1. 使用一对匹配的单引号。例:‘hello’。

  2. 使用一对匹配的双引号。例:“abclua”。

  3. 字符串还可以用一种长括号(即 )括起来的方式定义

    1. 我们把两个正的方括号(即[[)间插入 n 个等号定义为第 n 级正长括号。

    2. 0 级正的长括号写作 [[ ,一级正的长括号写作 [=[

    3. 反的长括号也作类似定义;举个例子,4 级反的长括号写作 ]====]

    4. 一个长字符串可以由任何一级的正的长括号开始,而由第一个碰到的同级反的长括号结束。整个词法分析过程将不受分行限制,不处理任何转义符,并且忽略掉任何不同级别的长括号

local str1 = 'hello world'
local str2 = "hello lua"
local str3 = [["add\name",'hello']]
local str4 = [=[string have a [[]].]=]
local str5 = [=[asdfasd]=]
 
print(str1)    -->output:hello world
print(str2)    -->output:hello lua
print(str3)    -->output:"add\name",'hello'
print(str4)    -->output:string have a [[]].
print(str5)    -->output:asdfasd

在 Lua 实现中,Lua 字符串一般都会经历一个“内化”(intern)的过程,即两个完全一样的 Lua 字符串在 Lua 虚拟机中只会存储一份。每一个 Lua 字符串在创建时都会插入到 Lua 虚拟机内部的一个全局的哈希表

  1. 创建相同的 Lua 字符串并不会引入新的动态内存分配操作,所以相对便宜(但仍有全局哈希表查询的开销),

  2. 内容相同的 Lua 字符串不会占用多份存储空间,

  3. 已经创建好的 Lua 字符串之间进行相等性比较时是 O(1) 时间度的开销,而不是通常见到的 O(n).

Table (表)

Table 类型实现了一种抽象的“关联数组”。“关联数组”是一种具有特殊索引方式的数组,

  • 索引通常是字符串(string)或者 number 类型,但也可以是除 nil 以外的任意类型的值
 
local corp = {
    web = "www.google.com",   --索引为字符串,key = "web",
    --            value = "www.google.com"
    telephone = "12345678",   --索引为字符串
    staff = {"Jack", "Scott", "Gary"}, --索引为字符串,值也是一个表
    100876,              --相当于 [1] = 100876,此时索引为数字
    --      key = 1, value = 100876
    100191,              --相当于 [2] = 100191,此时索引为数字
    [10] = 360,          --直接把数字索引给出
    ["city"] = "Beijing" --索引为字符串
}
 
print(corp.web)               -->output:www.google.com
print(corp["web"])               -->output:www.google.com
print(corp["telephone"])      -->output:12345678
print(corp[2])                -->output:100191
print(corp["city"])           -->output:"Beijing"
print(corp.staff[1])          -->output:Jack
print(corp["staff"][1])          -->output:Jack
print(corp[10])               -->output:360

在内部实现上,table 通常实现为一个哈希表、一个数组、或者两者的混合。具体的实现为何种形式,动态依赖于具体的 table 的键分布特点。

Function (函数)

在 Lua 中,函数 也是一种数据类型,函数可以存储在变量中,可以通过参数传递给其他函数,还可以作为其他函数的返回值

local function foo()
    print("in the function")
    --dosomething()
    local x = 10
    local y = 20
    return x + y
end
 
local a = foo    --把函数赋给变量
 
print(a())
 
--output:
--in the function
--30
 
function foo()
end
--等价于
 
foo = function ()
end
 
local function foo()
end
-- 等价于
 
local foo = function ()
end

表达式

算术运算符

算术运算符说明
+加法
-减法
*乘法
/除法
^指数
%取模
print(1 + 2)       -->打印 3
print(5 / 10)      -->打印 0.5。 这是Lua不同于c语言的
print(5.0 / 10)    -->打印 0.5。 浮点数相除的结果是浮点数
-- print(10 / 0)   -->注意除数不能为0,计算的结果会出错
print(2 ^ 10)      -->打印 1024。 求2的10次方
 
local num = 1357
print(num % 2)       -->打印 1
print((num % 2) == 1) -->打印 true。 判断num是否为奇数

关系运算符

关系运算符说明
<小于
>大于
小于等于
>=大于等于
==等于
~=不等于
print(1 < 2)    -->打印 true
print(1 == 2)   -->打印 false
print(1 ~= 2)   -->打印 true
local a, b = true, false
print(a == b)  -->打印 false
  • 在使用“==”做等于判断时,要注意对于 table, userdate 和函数, Lua 是作引用比较的。也就是说,只有当两个变量引用同一个对象时,才认为它们相等
local a = { x = 1, y = 0}
local b = { x = 1, y = 0}
if a == b then
    print("a==b")
else
    print("a~=b")
end
---output:
a~=b
  • Lua 字符串总是会被“内化”,即相同内容的字符串只会被保存一份,因此 Lua 字符串之间的相等性比较可以简化为其内部存储地址的比较。

  • 这意味着 Lua 字符串的相等性比较总是为 O (1)

逻辑运算符

逻辑运算符说明
and逻辑与
or逻辑或
not逻辑非

在 c 语言中,and 和 or 只得到两个值 1 和 0,其中 1 表示真,0 表示假。而 Lua 中 and 的执行过程是这样的:

  • a and b 如果 a 为 nil,则返回 a,否则返回 b;

  • a or b 如果 a 为 nil,则返回 b,否则返回 a。

  • 所有逻辑操作符将 false 和 nil 视作假,其他任何值视作真,对于 and 和 or,“短路求值”,对于 not,永远只返回 true 或者 false。

local c = nil
local d = 0
local e = 100
print(c and d)  -->打印 nil
print(c and e)  -->打印 nil
print(d and e)  -->打印 100
print(c or d)   -->打印 0
print(c or e)   -->打印 100
print(not c)    -->打印 true
print(not d)    -->打印 false

字符串连接

Lua 中连接两个字符串,可以使用操作符“..”(两个点)

  • 如果其任意一个操作数是数字的话,Lua 会将这个数字转换成字符串。

  • 注意,连接操作符只会创建一个新字符串,而不会改变原操作数

  • 也可以使用 string 库函数 string.format 连接字符串

print("Hello " .. "World")    -->打印 Hello Worldprint(0 .. 1)                 -->打印 01
 
str1 = string.format("%s-%s","hello","world")
print(str1)              -->打印 hello-world
 
str2 = string.format("%d-%s-%.2f",123,"world",1.21)
print(str2)              -->打印 123-world-1.21

于 Lua 字符串本质上是只读的,因此字符串连接运算符几乎总会创建一个新的(更大的)字符串。这意味着如果有很多这样的连接操作(比如在循环中使用 .. 来拼接最终结果),则性能损耗会非常大。在这种情况下,推荐使用 table 和 table.concat() 来进行很多字符串的拼接

local pieces = {}
for i, elem in ipairs(my_list) do
    pieces[i] = my_process(elem)
end
local res = table.concat(pieces)

上面的例子还可以使用 LuaJIT 独有的 table.new 来恰当地初始化 pieces 表的空间,以避免该表的动态生长。

优先级

f
^
not # -
* / %
+ -
..
< > >= == ~=
and
or
local a, b = 1, 2
local x, y = 3, 4
local i = 10
local res = 0
res = a + i < b/2 + 1  -->等价于res =  (a + i) < ((b/2) + 1)
res = 5 + x^2*8        -->等价于res =  5 + ((x^2) * 8)
res = a < y and y <=x  -->等价于res =  (a < y) and (y <= x)

控制结构

If-else

单个 if 分支型

x = 10
if x > 0 then
    print("x is a positive number")
end

两个分支 if-else 型

x = 10
if x > 0 then
    print("x is a positive number")
else
    print("x is a non-positive number")
end

多个分支的 if-elseif-else

 
score = 90
if score == 100 then
    print("Very good!Your score is 100")
elseif score >= 60 then
    print("Congratulations, you have passed it,your score greater or equal to 60")
    --此处可以添加多个elseif
else
    print("Sorry, you do not pass the exam! ")
end

与 C 语言的不同之处是 else 与 if 是连在一起的,若将 else 与 if 写成 “else if” 则相当于在 else 里嵌套另一个 if 语句,如下代码:

score = 0
if score == 100 then
    print("Very good!Your score is 100")
elseif score >= 60 then
    print("Congratulations, you have passed it,your score greater or equal to 60")
else
    if score > 0 then
        print("Your score is better than 0")
    else
        print("My God, your score turned out to be 0")
    end --与上一示例代码不同的是,此处要添加一个end
end

While

while 表达式 do
    --body
end

Repeat

Lua 中的 repeat 控制结构类似于其他语言(如:C++ 语言)中的 do-while,但是控制方式是刚好相反的。简单点说,执行 repeat 循环体后,直到 until 的条件为真时才结束

-- 以下代码会死循环
x = 10
repeat
    print(x)
until false

For

for 数字型

for var = begin, finish, step do
    --body
end
  1. Var 从 begin 变化到 finish,每次变化都以 step 作为步长递增 var

  2. Begin、finish、step 三个表达式只会在循环开始时执行一次

  3. 第三个表达式 step 是可选的,默认为 1

  4. 控制变量 var 的作用域仅在 for 循环内,需要在外面控制,则需将值赋给一个新的变量

  5. 循环过程中不要改变控制变量的值,那样会带来不可预知的影响

for i = 1, 5 do
    print(i)
end
-- output:
1
2
3
4
5
 
for i = 1, 10, 2 do
    print(i)
end
-- output:
1
3
5
7
9

For 泛型

泛型 for 循环通过一个迭代器(iterator)函数来遍历所有值:

-- 打印数组a的所有值local a = {"a", "b", "c", "d"}
for i, v in ipairs(a) do
    print("index:", i, " value:", v)
end
-- output:
index:  1  value: a
index:  2  value: b
index:  3  value: c
index:  4  value: d

Lua 的基础库提供了 ipairs,这是一个用于遍历数组的迭代器函数。在每次循环中,i 会被赋予一个索引值,同时 v 被赋予一个对应于该索引的数组元素值。

-- 打印table t中所有的
keyfor k in pairs(t) do
    print(k)
end

通过不同的迭代器,几乎可以遍历所有的东西,而且写出的代码极具可读性。标准库提供了几种迭代器,包括用于迭代文件中每行的(io. Lines)、迭代 table 元素的(pairs)、迭代数组元素的(ipairs)、迭代字符串中单词的(string. Gmatch)

泛型 for 循环与数字型 for 循环有两个相同点:

  1. 循环变量是循环体的局部变量;

  2. 决不应该对循环变量作任何赋值。

在 LuaJIT 2.1 中,ipairs() 内建函数是可以被 JIT 编译的,而 pairs() 则只能被解释执行。因此在性能敏感的场景,应当合理安排数据结构,避免对哈希表进行遍历

Break

语句 break 用来终止 whilerepeatfor 三种循环的执行,并跳出当前循环体,继续执行当前循环之后的语句

-- 计算最小的x,使从1到x的所有数相加和大于100
sum = 0
i = 1while true do
    sum = sum + i
    if sum > 100 then
        break
    end
    i = i + 1
end
print("The result is " .. i)  
-->output:The result is 14

Return

return 主要用于从函数中返回结果,或者用于简单的结束一个函数的执行。

local function add(x, y)
    return x + y
    --print("add: I will return the result " .. (x + y))
    --因为前面有个return,若不注释该语句,则会报错
end
 
local function is_positive(x)
    if x > 0 then
        return x .. " is positive"
    else
        return x .. " is non-positive"
    end
 
    --由于return只出现在前面显式的语句块,所以此语句不注释也不会报错
    --,但是不会被执行,此处不会产生输出
    print("function end!")
end
 
local sum = add(10, 20)
print("The sum is " .. sum)  -->output:The sum is 30
local answer = is_positive(-10)
print(answer)                -->output:-10 is non-positive

Goto

有了 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

输出结果

$ luajit test.lua
1   yes continue
i'm end
2   yes continue
i'm end
3    no continue
i'm end

函数

定义

function function_name (arc)  -- arc 表示参数列表,函数的参数列表可以为空
    -- body
end

上面的语法定义了一个全局函数,名为 function_name. 全局函数本质上就是函数类型的值赋给了一个全局变量,即上面的语法等价于

function_name = function (arc)
     -- body
end

由于全局变量一般会污染全局名字空间,同时也有性能损耗(即查询全局环境表的开销),因此我们应当尽量使用“局部函数”,其记法是类似的,只是开头加上 local 修饰符:

local function function_name (arc)
    -- body
end

定义函数

  1. 利用名字来解释函数、变量的目的,使人通过名字就能看出来函数、变量的作用。

  2. 每个函数的长度要尽量控制在一个屏幕内,一眼可以看明白。

  3. 让代码自己说话,不需要注释最好。

由于函数定义等价于变量赋值,我们也可以把函数名替换为某个 Lua 表的某个字段,例如

local foo = {}
function foo.pr()
    print("ssss")
end
 
foo.pr()

参数

按值传递

Lua 函数的参数大部分是按值传递的当函数参数是 table 类型时,传递进来的是实际参数的引用

值传递就是调用函数时,实参把它的值通过赋值运算传递给形参,然后形参的改变和实参就没有关系了。在这个过程中,实参是通过它在参数表中的位置与形参匹配起来的。

local function swap(a, b) --定义函数swap,函数内部进行交换两个变量的值
    local temp = a
    a = b
    b = temp
    print(a, b)
end
 
local x = "hello"
local y = 20
print(x, y)
swap(x, y)    --调用swap函数
print(x, y)   --调用swap函数后,x和y的值并没有交换
 
-->output
hello 20
20  hello
hello 20

在调用函数的时候,若形参个数和实参个数不同时,Lua 会自动调整实参个数。调整规则:

  • 若实参个数大于形参个数,从左向右,多余的实参被忽略;

  • 若实参个数小于形参个数,从左向右,没有被实参初始化的形参会被初始化为 nil

local function fun1(a, b)       --两个形参,多余的实参被忽略掉
    print(a, b)
end
 
local function fun2(a, b, c, d) --四个形参,没有被实参初始化的形参,用nil初始化
    print(a, b, c, d)
end
 
local x = 1
local y = 2
local z = 3
 
fun1(x, y, z)         -- z被函数fun1忽略掉了,参数变成 x, y
fun2(x, y, z)         -- 后面自动加上一个nil,参数变成 x, y, z, nil
 
-->output
1   2
1   2   3   nil

变长参数

其实 Lua 还支持变长参数。若形参为 ...,表示该函数可以接收不同长度的参数。访问参数的时候也要使用 ...

 
local function func( ... )                -- 形参为 ... ,表示函数采用变长参数
 
    local temp = {...}                     -- 访问的时候也要使用 ...
    local ans = table.concat(temp, " ")    -- 使用 table.concat 库函数对数
    -- 组内容使用 " " 拼接成字符串。
    print(ans)
end
 
func(1, 2)        -- 传递了两个参数
func(1, 2, 3, 4)  -- 传递了四个参数
 
-->output
1 2
 
1 2 3 4

具名参数

Lua 还支持通过名称来指定实参,这时候要把所有的实参组织到一个 table 中,并将这个 table 作为唯一的实参传给函数。

local function change(arg) -- change 函数,改变长方形的长和宽,使其各增长一倍
  arg.width = arg.width * 2
  arg.height = arg.height * 2return arg
endlocal rectangle = { width = 20, height = 15 }
print("before change:", "width  =", rectangle.width,
                        "height =", rectangle.height)
rectangle = change(rectangle)
print("after  change:", "width  =", rectangle.width,
                        "height =", rectangle.height)
 
-->output
before change: width = 20  height =  15
after  change: width = 40  height =  30

按引用传递

当函数参数是 table 类型时,传递进来的是实际参数的引用,此时在函数内部对该 table 所做的修改,会直接对调用者所传递的实际参数生效,而无需自己返回结果和让调用者进行赋值

function change(arg) --change函数,改变长方形的长和宽,使其各增长一倍
  arg.width = arg.width * 2  --表arg不是表rectangle的拷贝,他们是同一个表
  arg.height = arg.height * 2end                  -- 没有return语句了local rectangle = { width = 20, height = 15 }
print("before change:", "width = ", rectangle.width,
                        " height = ", rectangle.height)
change(rectangle)
print("after change:", "width = ", rectangle.width,
                       " height =", rectangle.height)
 
--> output
before change: width = 20  height = 15
after  change: width = 40  height = 30

函数返回值

Lua 具有一项与众不同的特性,允许函数返回多个值。

local function swap(a, b)   
    -- 定义函数 swap,实现两个变量交换值
    return b, a              
    -- 按相反顺序返回变量的值
end
 
local x = 1
local y = 20
x, y = swap(x, y)           -- 调用 swap 函数
print(x, y)                 --> output   20     1

当函数返回值的个数和接收返回值的变量的个数不一致时,Lua 也会自动调整参数个数调整规则:

  • 若返回值个数大于接收变量的个数,多余的返回值会被忽略掉;

  • 若返回值个数小于参数个数,从左向右,没有被返回值初始化的变量会被初始化为 nil。

function init()             
    --init 函数 返回两个值 1 和 "lua"
    return 1, "lua"
end
 
x = init()
print(x)
 
x, y, z = init()
print(x, y, z)
 
--output
1
1 lua nil

当一个函数有一个以上返回值,且函数调用不是一个列表表达式的最后一个元素,那么函数调用只会产生一个返回值, 也就是第一个返回值。

local function init()       -- init 函数 返回两个值 1 和 "lua"
    return 1, "lua"
end
 
local x, y, z = init(), 2   -- init 函数的位置不在最后,此时只返回 1
print(x, y, z)              -->output  1  2  nil
 
local a, b, c = 2, init()   -- init 函数的位置在最后,此时返回 1 和 "lua"
print(a, b, c)              -->output  2  1  lua

函数调用的实参列表也是一个列表表达式。考虑下面的例子:

local function init()
    return 1, "lua"
end
 
print(init(), 2)   -->output  1  2
print(2, init())   -->output  2  1  lua

如果你确保只取函数返回值的第一个值,可以使用括号运算符

local function init()
    return 1, "lua"
end
print((init()), 2)   -->output  1  2
print(2, (init()))   -->output  2  1

值得一提的是,如果实参列表中某个函数会返回多个值,同时调用者又没有显式地使用括号运算符来筛选和过滤,则这样的表达式是不能被 LuaJIT 2 所 JIT 编译的,而只能被解释执行。

全动态函数调用

调用回调函数,并把一个数组参数作为回调函数的参数。

local args = {...} or {}
method_name(unpack(args, 1, table.maxn(args)))
local function run(x, y)
    print('run', x, y)
end
 
local function attack(targetId)
    print('targetId', targetId)
end
 
local function do_action(method, ...)
    local args = {...} or {}
    method(unpack(args, 1, table.maxn(args)))
end
 
do_action(run, 1, 2)         -- output: run 1 2
do_action(attack, 1111)      -- output: targetId    1111

模块

从 Lua 5.1 语言添加了对模块和包的支持。一个 Lua 模块的数据结构是用一个 Lua 值(通常是一个 Lua 表或者 Lua 函数)一个 Lua 模块代码就是一个会返回这个 Lua 值的代码块

  • 可以使用内建函数 require() 来加载和缓存模块。

  • 简单的说,一个代码模块就是一个程序库,可以通过 require 来加载。模块加载后的结果通过是一个 Lua table

  • 这个表就像是一个命名空间,其内容就是模块中导出的所有东西,比如函数和变量require 函数会返回 Lua 模块加载后的结果,即用于表示该 Lua 模块的 Lua 值。

Lua 提供了一个名为 require 的函数用来加载模块。要加载一个模块,只需要简单地调用 require “file” 就可以了,file 指模块所在的文件名。这个调用会返回一个由模块函数组成的 table,并且还会定义一个包含该 table 的全局变量。

在 Lua 中创建一个模块最简单的方法是:创建一个 table,并将所有需要导出的函数放入其中,最后返回这个 table 就可以了。相当于将导出的函数作为 table 的一个字段,在 Lua 中函数是第一类值,提供了天然的优势。

  • 创建 my. Lua
local _M = {}
 
local function get_name()
    return "Lucy"
    end
function _M.greeting()
    print("hello " .. get_name())
end
 
return _M
  • 把下面代码保存在文件 main. Lua 中,然后执行 main. Lua,调用上述模块。
local my_module = require("my")
my_module.greeting()     -->output: hello Lucy
  • 对于需要导出给外部使用的公共模块,处于安全考虑,是要避免全局变量的出现。我们可以使用 lj-releng 或 luacheck 工具完成全局变量的检测。至于如何做,到后面再讲。

  • 另一个要注意的是,由于在 LuaJIT 中,require 函数内不能进行上下文切换所以不能够在模块的顶级上下文中调用 cosocket 一类的 API。否则会报 attempt to yield across C-call boundary 错误。

String

Lua 字符串总是由字节构成的。Lua 核心并不尝试理解具体的字符集编码(比如 GBK 和 UTF-8 这样的多字节字符编码)

Lua 字符串内部用来标识各个组成字节的下标是从 1 开始的,这不同于像 C 和 Perl 这样的编程语言。这样数字符串位置的时候再也不用调整,对于非专业的开发者来说可能也是一个好事情,string.Sub (str, 3, 7) 直接表示从第三个字符开始到第七个字符(含)为止的子串。

string.Byte (s [, i [, j ]])

返回字符 s[i]、s[i + 1]、s[i + 2]、······、s[j] 所对应的 ASCII 码

print(string.byte("abc", 1, 3))
print(string.byte("abc", 3)) -- 缺少第三个参数,第三个参数默认与第二个相同,此时为 3
print(string.byte("abc"))    -- 缺少第二个和第三个参数,此时这两个参数都默认为 1
 
-->output
97    98    99
99
97

string. Char (…)

接收 0 个或更多的整数(整数范围:0~255),返回这些整数所对应的 ASCII 码字符组成的字符串。当参数为空时,默认是一个 0。

print(string.char(96, 97, 98))
print(string.char())        -- 参数为空,默认是一个0,-- 你可以用string.byte(string.char())测试一下print(string.char(65, 66))
 
--> output
`ab
 
AB

string.Upper (s)

接收一个字符串 s,返回一个把所有小写字母变成大写字母的字符串。

print(string.upper("Hello Lua"))  -->output  HELLO LUA

string.Lower (s)

接收一个字符串 s,返回一个把所有大写字母变成小写字母的字符串。

print(string.lower("Hello Lua"))  -->output   hello lua

string.Len (s)

接收一个字符串,返回它的长度。

print(string.len("hello lua")) -->output  9

使用此函数是不推荐的。应当总是使用 # 运算符来获取 Lua 字符串的长度

string.Find (s, p [, init [, plain]])

在 s 字符串中第一次匹配 p 字符串。若匹配成功,则返回 p 字符串在 s 字符串中出现的开始位置和结束位置;若匹配失败,则返回 nil,

第三个参数第三个参数 init 默认为 1,并且可以为负整数,

当 init 为负数时,表示从 s 字符串的 string.Len (s) + init + 1 索引处开始向后匹配字符串 p 。

第四个参数默认为 false,当其为 true 时,只会把 p 看成一个字符串对待。

local find = string.find
print(find("abc cba", "ab"))
print(find("abc cba", "ab", 2))     -- 从索引为2的位置开始匹配字符串:ab
print(find("abc cba", "ba", -1))    -- 从索引为7的位置开始匹配字符串:ba
print(find("abc cba", "ba", -3))    -- 从索引为5的位置开始匹配字符串:ba
print(find("abc cba", "(%a+)", 1))  -- 从索引为1处匹配最长连续且只含字母的字符串
print(find("abc cba", "(%a+)", 1, true)) --从索引为1的位置开始匹配字符串:(%a+)
 
-->output
1   2
nil
nil
6   7
1   3   abc
nil

string.Format (formatstring, …)

按照格式化参数 formatstring,返回后面 ... 内容的格式化版本

print(string.format("%.4f", 3.1415926))     -- 保留4位小数
print(string.format("%d %x %o", 31, 31, 31))-- 十进制数31转换成不同进制
d = 29; m = 7; y = 2015                     -- 一行包含几个语句,用;分开
print(string.format("%s %02d/%02d/%d", "today is:", d, m, y))
 
-->output
3.1416
31 1f 37
today is: 29/07/2015

string.Match (s, p [, init])

在字符串 s 中匹配(模式)字符串 p,若匹配成功,则返回目标字符串中与模式匹配的子串;否则返回 nil。第三个参数 init 默认为 1,并且可以为负整数,当 init 为负数时,表示从 s 字符串的 string.Len (s) + init + 1 索引处开始向后匹配字符串 p。

print(string.match("hello lua", "lua"))
print(string.match("lua lua", "lua", 2))  --匹配后面那个luaprint(string.match("lua lua", "hello"))
print(string.match("today is 27/7/2015", "%d+/%d+/%d+"))
 
-->output
lua
lua
nil27/7/2015

string.Gmatch (s, p)

返回一个迭代器函数,通过这个迭代器函数可以遍历到在字符串 s 中出现模式串 p 的所有地方。

s = "hello world from Lua"
for w in string.gmatch(s, "%a+") do  --匹配最长连续且只含字母的字符串
    print(w)
end
 
-->output
hello
world
from
Lua
 
 
t = {}
s = "from=world, to=Lua"
for k, v in string.gmatch(s, "(%a+)=(%a+)") do  --匹配两个最长连续且只含字母的
    t[k] = v                                    --字符串,它们之间用等号连接
end
for k, v in pairs(t) do
    print (k,v)
end
 
-->output
to      Lua
from    worl

string.Rep (s, n)

返回字符串 s 的 n 次拷贝。

print(string.rep("abc", 3)) 
 
--拷贝3次"abc"-->output  abcabcabc

string.Sub (s, i [, j])

返回字符串 s 中,索引 i 到索引 j 之间的子字符串。当 j 缺省时,默认为 -1,也就是字符串 s 的最后位置。I 可以为负数。当索引 i 在字符串 s 的位置在索引 j 的后面时,将返回一个空字符串。

print(string.sub("Hello Lua", 4, 7))
print(string.sub("Hello Lua", 2))
print(string.sub("Hello Lua", 2, 1))    --看到返回什么了吗print(string.sub("Hello Lua", -3, -1))
 
-->output
lo L
ello Lua
 
Lua

string.Gsub (s, p, r [, n])

将目标字符串 s 中所有的子串 p 替换成字符串 r。可选参数 n,表示限制替换次数。返回值有两个,第一个是被替换后的字符串,第二个是替换了多少次。

print(string.gsub("Lua Lua Lua", "Lua", "hello"))
print(string.gsub("Lua Lua Lua", "Lua", "hello", 2)) --指明第四个参数-->output
hello hello hello   3
hello hello Lua     2

string. Reverse (s)

接收一个字符串 s,返回这个字符串的反转

print(string.reverse("Hello Lua"))  --> output: auL olleH

Table

下标从 1 开始

数组下标从 1 开始计数。

而 Lua 最初设计是一种类似 XML 的数据描述语言,所以索引(index)反应的是数据在里面的位置,而不是偏移量。

在初始化一个数组的时候,若不显式地用键值对方式赋值,则会默认用数字作为下标,从 1 开始。由于在 Lua 内部实际采用哈希表和数组分别保存键值对、普通值,所以不推荐混合使用这两种赋值方式

local color={first="red", "blue", third="green", "yellow"}
print(color["first"])                 --> output: red
print(color[1])                       --> output: blue
print(color["third"])                 --> output: green
print(color[2])                       --> output: yellow
print(color[3])                       --> output: nil
  • 当我们把 table 当作栈或者队列使用的时候,容易犯错,追加到 table 的末尾用的是 s[#s+1] = something****, 而不是 s[#s] = something

  • 而且如果这个 something 是一个 nil 的话**,会导致这一次压栈(或者入队列)没有存入任何东西**, s 的值没有变

  • 如果 s = { 1, 2, 3, 4, 5, 6 },你令 s[4] = nil, s 会令你“匪夷所思”地变成 3。

table. Getn 获取长度

取长度操作符写作一元操作 。字符串的长度是它的字节数(就是以一个字符一个字节计算的字符串长度)

  • 对于常规的数组,里面从 1 到 n 放着一些非空的值的时候,它的长度就精确的为 n,即最后一个值的下标

  • 如果数组有一个“空洞”(就是说,nil 值被夹在非空值之间),那么 t 可能是指向任何一个是 nil 值的前一个位置的下标

  • 这也就说明对于有“空洞”的情况,table 的长度存在一定的 不可确定性

local tblTest1 = { 1, a = 2, 3 }
print("Test1 " .. table.getn(tblTest1))
 
local tblTest2 = { 1, nil }
print("Test2 " .. table.getn(tblTest2))
 
local tblTest3 = { 1, nil, 2 }
print("Test3 " .. table.getn(tblTest3))
 
local tblTest4 = { 1, nil, 2, nil }
print("Test4 " .. table.getn(tblTest4))
 
local tblTest5 = { 1, nil, 2, nil, 3, nil }
print("Test5 " .. table.getn(tblTest5))
 
local tblTest6 = { 1, nil, 2, nil, 3, nil, 4, nil }
print("Test6 " .. table.getn(tblTest6))

我们使用 Lua 5.1 和 LuaJIT 2.1 分别执行这个用例,结果如下:

# lua test.lua
Test1 2
Test2 1
Test3 3
Test4 1
Test5 3
Test6 1
# luajit test.lua
Test1 2
Test2 1
Test3 1
Test4 1
Test5 1
Test6 1

不要在 Lua 的 table 中使用 nil 值,如果一个元素要删除,直接 remove,不要用 nil 去代替

table. Concat (table [, sep [, i [, j ] ] ])

对于元素是 string 或者 number 类型的表 table,返回 table[i]..sep..table[i+1] ··· sep..table[j] 连接成的字符串。填充字符串 sep 默认为空白字符串。起始索引位置 i 默认为 1,结束索引位置 j 默认是 table 的长度。

local a = {1, 3, 5, "hello" }
print(table.concat(a))              -- output: 135hello
print(table.concat(a, "|"))         -- output: 1|3|5|hello
print(table.concat(a, " ", 4, 2))   -- output:
print(table.concat(a, " ", 2, 4))   -- output: 3 5 hello

table. Insert (table, [pos ,] value)

在(数组型)表 table 的 pos 索引位置插入 value,其它元素向后移动到空的地方。Pos 的默认值是表的长度加一,即默认是插在表的最后

local a = {1, 8}             --a[1] = 1,a[2] = 8
table.insert(a, 1, 3)   --在表索引为1处插入3
print(a[1], a[2], a[3])
table.insert(a, 10)    --在表的最后插入10
print(a[1], a[2], a[3], a[4])
 
-->output
3    1    8
3    1    8    10

table. Maxn (table)

返回(数组型)表 table 的最大索引编号;如果此表没有正的索引编号,返回 0。

local a = {}
a[-1] = 10
print(table.maxn(a))
a[5] = 10
print(table.maxn(a))
 
-->output05

table. Remove (table [, pos])

在表 table 中删除索引为 pos(pos 只能是 number 型)的元素,并返回这个被删除的元素,它后面所有元素的索引值都会减一。Pos 的默认值是表的长度,即默认是删除表的最后一个元素。

local a = { 1, 2, 3, 4}
print(table.remove(a, 1)) --删除速索引为1的元素print(a[1], a[2], a[3], a[4])
 
print(table.remove(a))   --删除最后一个元素print(a[1], a[2], a[3], a[4])
 
-->output12    3    4    nil42    3    nil    nil

table. Sort (table [, comp])

按照给定的比较函数 comp 给表 table 排序,也就是从 table[1] 到 table[n],这里 n 表示 table 的长度。比较函数有两个参数,如果希望第一个参数排在第二个的前面,就应该返回 true,否则返回 false。如果比较函数 comp 没有给出,默认从小到大排序。

 
local function compare(x, y) --从大到小排序
    return x > y         --如果第一个参数大于第二个就返回true,否则返回false
end
 
local a = { 1, 7, 3, 4, 25}
table.sort(a)           --默认从小到大排序
print(a[1], a[2], a[3], a[4], a[5])
table.sort(a, compare) --使用比较函数进行排序
print(a[1], a[2], a[3], a[4], a[5])
 
-->output
1    3    4    7    25
25    7    4    3    1

其他

LuaJIT 2.1 新增加的 table.newtable.clear 函数是非常有用的。前者主要用来预分配 Lua table 空间,后者主要用来高效的释放 table 空间,并且它们都是可以被 JIT 编译的

日期时间

函数 time、date 和 difftime 提供了所有的日期和时间功能。

在 OpenResty 的世界里,不推荐使用这里的标准时间函数,因为这些函数通常会引发不止一个昂贵的系统调用,同时无法为 LuaJIT JIT 编译,对性能造成较大影响。推荐使用 ngx_lua 模块提供的带缓存的时间接口,如 ngx.today, ngx.time, ngx.utctime, ngx.localtime, ngx.now, ngx.http_time,以及 ngx.cookie_time 等。

os. Time ([table])

如果不使用参数 table 调用 time 函数,

  • 它会返回当前的时间和日期(它表示从某一时刻到现在的秒数)。

  • 如果用 table 参数,它会返回一个数字,表示该 table 中所描述的日期和时间(它表示从某一时刻到 table 中描述日期和时间的秒数)。Table 的字段如下:

字段名称取值范围
year四位数字
month1—12
day1—31
hour0—23
min0—59
sec0—61
isdstboolean(true 表示夏令时)

对于 time 函数,如果参数为 table,那么 table 中必须含有 year、month、day 字段。其他字缺省时段默认为中午(12:00:00)。

示例代码:(地点为北京)

print(os.time())    -->output  1438243393
a = { year = 1970, month = 1, day = 1, hour = 8, min = 1 }
print(os.time(a))   -->output  60

os. Difftime (t 2, t 1)

返回 t 1 到 t 2 的时间差,单位为秒。

示例代码:

local day1 = { year = 2015, month = 7, day = 30 }
local t1 = os.time(day1)
 
local day2 = { year = 2015, month = 7, day = 31 }
local t2 = os.time(day2)
print(os.difftime(t2, t1))   -->output  86400

os. Date ([format [, time]])

把一个表示日期和时间的数值,转换成更高级的表现形式。

  • 其第一个参数 format 是一个格式化字符串,描述了要返回的时间形式。

  • 第二个参数 time 就是日期和时间的数字表示,缺省时默认为当前的时间。

  • 使用格式字符 “*t”,创建一个时间表。

示例代码:

local tab1 = os.date("*t")  --返回一个描述当前日期和时间的表
local ans1 = "{"
for k, v in pairs(tab1) do  --把tab1转换成一个字符串
    ans1 = string.format("%s %s = %s,", ans1, k, tostring(v))
end
 
ans1 = ans1 .. "}"
print("tab1 = ", ans1)
 
 
local tab2 = os.date("*t", 360)  --返回一个描述日期和时间数为360秒的表
local ans2 = "{"
for k, v in pairs(tab2) do      --把tab2转换成一个字符串
    ans2 = string.format("%s %s = %s,", ans2, k, tostring(v))
end
 
ans2 = ans2 .. "}"
print("tab2 = ", ans2)
 
-->output
tab1 = { hour = 17, min = 28, wday = 5, day = 30, month = 7, year = 2015, sec = 10, yday = 211, isdst = false,}
tab2 = { hour = 8, min = 6, wday = 5, day = 1, month = 1, year = 1970, sec = 0, yday = 1, isdst = false,}

该表中除了使用到了 time 函数参数 table 的字段外,这还提供了星期(wday,星期天为 1)和一年中的第几天(yday,一月一日为 1)。除了使用 “*t” 格式字符串外,如果使用带标记(见下表)的特殊字符串,os. Date 函数会将相应的标记位以时间信息进行填充,得到一个包含时间的字符串。表如下:

格式字符含义
%a一星期中天数的简写(例如:Wed)
%A一星期中天数的全称(例如:Wednesday)
%b月份的简写(例如:Sep)
%B月份的全称(例如:September)
%c日期和时间(例如:07/30/15 16:57:24)
%d一个月中的第几天[01 ~ 31]
%H24 小时制中的小时数[00 ~ 23]
%I12 小时制中的小时数[01 ~ 12]
%j一年中的第几天[001 ~ 366]
%M分钟数[00 ~ 59]
%m月份数[01 ~ 12]
%p“上午(am)”或“下午(pm)”
%S秒数[00 ~ 59]
%w一星期中的第几天[1 ~ 7 = 星期天 ~ 星期六]
%x日期(例如:07/30/15)
%X时间(例如:16:57:24)
%y两位数的年份[00 ~ 99]
%Y完整的年份(例如:2015)
%%字符’%‘

示例代码:

print(os.date("today is %A, in %B"))
print(os.date("now is %x %X"))
 
-->output
today is Thursday, in July
now is 07/30/15 17:39:22

数学库

Ua 数学库由一组标准的数学函数构成。数学库的引入丰富了 Lua 编程语言的功能,同时也方便了程序的编写。常用数学函数见下表:

asdsdfa
函数名函数功能
math.Rad (x)角度 x 转换成弧度
math.Deg (x)弧度 x 转换成角度
math.Max (x, …)返回参数中值最大的那个数,参数必须是 number 型
math.Min (x, …)返回参数中值最小的那个数,参数必须是 number 型
math. Random ([m [, n]])不传入参数时,返回一个在区间[0,1)内均匀分布的伪随机实数;只使用一个整数参数 m 时,返回一个在区间[1, m]内均匀分布的伪随机整数;使用两个整数参数时,返回一个在区间[m, n]内均匀分布的伪随机整数
math. Randomseed (x)为伪随机数生成器设置一个种子 x,相同的种子将会生成相同的数字序列
math.Abs (x)返回 x 的绝对值
math.Fmod (x, y)返回 x 对 y 取余数
math.Pow (x, y)返回 x 的 y 次方
math.Sqrt (x)返回 x 的算术平方根
math.Exp (x)返回自然数 e 的 x 次方
math.Log (x)返回 x 的自然对数
math. Log 10 (x)返回以 10 为底,x 的对数
math.Floor (x)返回最大且不大于 x 的整数
math.Ceil (x)返回最小且不小于 x 的整数
math. Pi圆周率
math.Sin (x)求弧度 x 的正弦值
math.Cos (x)求弧度 x 的余弦值
math.Tan (x)求弧度 x 的正切值
math.Asin (x)求 x 的反正弦值
math.Acos (x)求 x 的反余弦值
math.Atan (x)求 x 的反正切值
print(math.pi)           -->output  3.1415926535898
print(math.rad(180))     -->output  3.1415926535898
print(math.deg(math.pi)) -->output  180
 
print(math.sin(1))       -->output  0.8414709848079
print(math.cos(math.pi)) -->output  -1
print(math.tan(math.pi / 4))  -->output  1
 
print(math.atan(1))      -->output  0.78539816339745
print(math.asin(0))      -->output  0
 
print(math.max(-1, 2, 0, 3.6, 9.1))     -->output  9.1
print(math.min(-1, 2, 0, 3.6, 9.1))     -->output  -1
 
print(math.fmod(10.1, 3))   -->output  1.1
print(math.sqrt(360))      -->output  18.97366596101
 
print(math.exp(1))         -->output  2.718281828459
print(math.log(10))        -->output  2.302585092994
print(math.log10(10))      -->output  1
 
print(math.floor(3.1415))  -->output  3
print(math.ceil(7.998))    -->output  8

使用 math.random() 函数获得伪随机数时,如果不使用 math.randomseed() 设置伪随机数生成种子或者设置相同的伪随机数生成种子,那么得得到的伪随机数序列是一样的。

math.randomseed (100) --把种子设置为100
print(math.random())         -->output  0.0012512588885159
print(math.random(100))      -->output  57
print(math.random(100, 360)) -->output  150

稍等片刻,再次运行上面的代码。

math.randomseed (100) --把种子设置为100
print(math.random())         -->output  0.0012512588885159
print(math.random(100))      -->output  57
print(math.random(100, 360)) -->output  150

两次运行的结果一样。为了避免每次程序启动时得到的都是相同的伪随机数序列,通常是使用当前时间作为种子。

修改上例中的代码:

math.randomseed (os.time())   --把100换成os.time()
print(math.random())          -->output 0.88369396038697
print(math.random(100))       -->output 66
print(math.random(100, 360))  -->output 228

稍等片刻,再次运行上面的代码。

math.randomseed (os.time())   --把100换成os.time()
print(math.random())          -->output 0.88946195867794
print(math.random(100))       -->output 68
print(math.random(100, 360))  -->output 129

文件

Lua I/O 库提供两种不同的方式处理文件:隐式文件描述,显式文件描述。

这些文件 I/O 操作,在 OpenResty 的上下文中对事件循环是会产生阻塞效应。OpenResty 比较擅长的是高并发网络处理,在这个环境中,任何文件的操作,都将阻塞其他并行执行的请求。实际中的应用,在 OpenResty 项目中应尽可能让网络处理部分、文件 I/0 操作部分相互独立,不要揉和在一起

隐式文件描述

设置一个默认的输入或输出文件,然后在这个文件上进行所有的输入或输出操作。所有的操作函数由 io 表提供。

打开已经存在的 test1.txt 文件,并读取里面的内容

file = io.input("test1.txt")    -- 使用 io.input() 函数打开文件repeat
    line = io.read()            -- 逐行读取内容,文件结束时返回nil
    if nil == line then
        break
    end
    print(line)
until (false)
 
io.close(file)                  -- 关闭文件--> output
my test file
hello
lua

test1.txt 文件的最后添加一行 “hello world”

file = io.open("test1.txt", "a+")   -- 使用 io.open() 函数,以添加模式打开文件
io.output(file)                     -- 使用 io.output() 函数,设置默认输出文件
io.write("\nhello world")           -- 使用 io.write() 函数,把内容写到文件
io.close(file)

在相应目录下打开 test1.txt 文件,查看文件内容发生的变化。

显式文件描述

使用 file: XXX () 函数方式进行操作, 其中 file 为 io.Open () 返回的文件句柄。

打开已经存在的 test 2. Txt 文件,并读取里面的内容

file = io.open("test2.txt", "r")    -- 使用 io.open() 函数,以只读模式打开文件
 
for line in file:lines() do         -- 使用 file:lines() 函数逐行读取文件
    print(line)
end
 
file:close()
 
-->output
my test2
hello lua

在 test 2. Txt 文件的最后添加一行 “hello world”

file = io.open("test2.txt", "a")  -- 使用 io.open() 函数,以添加模式打开文件
file:write("\nhello world")       -- 使用 file:write() 函数,在文件末尾追加内容
file:close()

在相应目录下打开 test2.txt 文件,查看文件内容发生的变化。

文件操作函数

io. Open (filename [, mode])

按指定的模式 mode,打开一个文件名为 filename 的文件,成功则返回文件句柄,失败则返回 nil 加错误信息。模式:

模式含义文件不存在时
”r”读模式 (默认)返回 nil 加错误信息
”w”写模式创建文件
”a”添加模式创建文件
”r+“更新模式,保存之前的数据返回 nil 加错误信息
”w+“更新模式,清除之前的数据创建文件
”a+“添加更新模式,保存之前的数据, 在文件尾进行添加创建文件

模式字符串后面可以有一个 ‘b’,用于在某些系统中打开二进制文件。

注意 “w” 和 “wb” 的区别

  • ”w” 表示文本文件。某些文件系统 (如 Linux 的文件系统)认为 0 x 0 A 为文本文件的换行符,Windows 的文件系统认为 0 x 0 D 0 A 为文本文件的换行符。为了兼容其他文件系统(如从 Linux 拷贝来的文件),Windows 的文件系统在写文件时,会在文件中 0 x 0 A 的前面加上 0 x 0 D。使用 “w”,其属性要看所在的平台。

  • “wb” 表示二进制文件。文件系统会按纯粹的二进制格式进行写操作,因此也就不存在格式转换的问题。(Linux 文件系统下 “w” 和 “wb” 没有区别)

file: close ()

关闭文件。注意:当文件句柄被垃圾收集后,文件将自动关闭。句柄将变为一个不可预知的值。

io. Close ([file])

关闭文件,和 file: close () 的作用相同。没有参数 file 时,关闭默认输出文件。

file: flush ()

把写入缓冲区的所有数据写入到文件 file 中。

io. Flush ()

相当于 file: flush (),把写入缓冲区的所有数据写入到默认输出文件。

io. Input ([file])

当使用一个文件名调用时,打开这个文件(以文本模式),并设置文件句柄为默认输入文件;当使用一个文件句柄调用时,设置此文件句柄为默认输入文件;当不使用参数调用时,返回默认输入文件句柄。

file: lines ()

返回一个迭代函数, 每次调用将获得文件中的一行内容, 当到文件尾时,将返回 nil,但不关闭文件。

io. Lines ([filename])

打开指定的文件 filename 为读模式并返回一个迭代函数, 每次调用将获得文件中的一行内容, 当到文件尾时,将返回 nil,并自动关闭文件。若不带参数时 io.Lines () 等价于 io.Input (): lines () 读取默认输入设备的内容,结束时不关闭文件。

io. Output ([file])

类似于 io. Input,但操作在默认输出文件上。

file: read (…)

按指定的格式读取一个文件。按每个格式将返回一个字符串或数字, 如果不能正确读取将返回 nil,若没有指定格式将指默认按行方式进行读取。格式:

格式含义
”*n”读取一个数字
”*a”从当前位置读取整个文件。若当前位置为文件尾,则返回空字符串
”*l”读取下一行的内容。若为文件尾,则返回 nil。(默认)
number读取指定字节数的字符。若为文件尾,则返回 nil。如果 number 为 0, 则返回空字符串,若为文件尾, 则返回 nil

io. Read (…)

相当于 io.Input ():read

io. Type (obj)

检测 obj 是否一个可用的文件句柄。如果 obj 是一个打开的文件句柄,则返回 “file” 如果 obj 是一个已关闭的文件句柄,则返回 “closed file” 如果 obj 不是一个文件句柄,则返回 nil。

file: write (…)

把每一个参数的值写入文件。参数必须为字符串或数字,若要输出其它值,则需通过 tostring 或 string. Format 进行转换。

io. Write (…)

相当于 io.Output (): write。

file: seek ([whence] [, offset])

设置和获取当前文件位置,成功则返回最终的文件位置 (按字节,相对于文件开头), 失败则返回 nil 加错误信息。缺省时,whence 默认为 “cur”,offset 默认为 0 。参数 whence:

whence含义
”set”文件开始
”cur”文件当前位置 (默认)
“end”文件结束

file: setvbuf (mode [, size])

设置输出文件的缓冲模式。模式:

模式含义
”no”没有缓冲,即直接输出
”full”全缓冲,即当缓冲满后才进行输出操作 (也可调用 flush 马上输出)
“line”以行为单位,进行输出

最后两种模式,size 可以指定缓冲的大小(按字节),忽略 size 将自动调整为最佳的大小。

元表

元表 (metatable)表现行为类似于 C++ 语言中的操作符重载,例如我们可以重载 “__add” 元方法 (metamethod),来计算两个 Lua 数组的并集;或者重载 “__index” 方法,来定义我们自己的 Hash 函数。Lua 提供了两个十分重要的用来处理元表的方法

  • Setmetatable (table, metatable):此方法用于为一个表设置元表。

  • Getmetatable (table):此方法用于获取表的元表对象

设置元表

local mytable = {}
local mymetatable = {}
setmetatable(mytable, mymetatable)

修改表的操作符行为

通过重载 “__add” 元方法来计算集合的并集实例

local set1 = {10, 20, 30}   -- 集合
local set2 = {20, 40, 50}   -- 集合
 
-- 将用于重载__add的函数,注意第一个参数是self
local union = function (self, another)
    local set = {}
    local result = {}
 
    -- 利用数组来确保集合的互异性
    for i, j in pairs(self) do set[j] = true end
    for i, j in pairs(another) do set[j] = true end
 
    -- 加入结果集合
    for i, j in pairs(set) do table.insert(result, i) end
    return result
end
setmetatable(set1, {__add = union}) -- 重载 set1 表的 __add 元方法
 
local set3 = set1 + set2
for _, j in pairs(set3) do
    io.write(j.." ")               -->output:30 50 20 40 10
end

除了加法可以被重载之外,Lua 提供的所有操作符都可以被重载:

元方法含义
”__addNAME ?
”__sub- 操作其行为类似于 “add” 操作
”__mul* 操作其行为类似于 “add” 操作
”__div/ 操作其行为类似于 “add” 操作
”__mod% 操作其行为类似于 “add” 操作
”__pow^ (幂)操作其行为类似于 “add” 操作
”__unm”一元 - 操作
”__concat”.. (字符串连接)操作
”__len”# 操作
”__eq”== 操作函数 getcomphandler 定义了 Lua 怎样选择一个处理器来作比较操作仅在两个对象类型相同且有对应操作相同的元方法时才起效
”__lt”< 操作
”__le” 操作

除了操作符之外,如下元方法也可以被重载,下面会依次解释使用方法:

元方法含义
”__index”取下标操作用于访问 table[key]
“__newindex”赋值给指定下标 table[key] = value
”__tostring”转换成字符串
”__call”当 Lua 调用一个值时调用
”__mode”用于弱表 (week table)
“__metatable”用于保护 metatable 不被访问

__index 元方法

mytable = setmetatable({key1 = "value1"},   --原始表
{__index = function(self, key)            --重载函数
    if key == "key2" then
        return "metatablevalue"
    end
end
})
 
print(mytable.key1,mytable.key2)  --> output:value1 metatablevalue

关于 __index 元方法,有很多比较高阶的技巧,例如:__index 的元方法不需要非是一个函数,他也可以是一个表。

t = setmetatable({[1] = "hello"}, {__index = {[2] = "world"}})
print(t[1], t[2])   -->hello wor

__tostring 元方法

与 Java 中的 toString () 函数类似,可以实现自定义的字符串转换。

arr = {1, 2, 3, 4}
arr = setmetatable(arr, {__tostring = function (self)
    local result = '{'
    local sep = ''
    for _, i in pairs(self) do
        result = result ..sep .. i
        sep = ', '
    end
    result = result .. '}'
    return result
end})
print(arr)  --> {1, 2, 3, 4}

__call 元方法

__call 元方法的功能类似于 C++ 中的仿函数,使得普通的表也可以被调用。

functor = {}
function func1(self, arg)
    print ("called from", arg)
end
setmetatable(functor, {__call = func1})
 
functor("functor")  --> called from functor
print(functor)      --> output:0x00076fc8 (后面这串数字可能不一样)

__metatable 元方法

假如我们想保护我们的对象使其使用者既看不到也不能修改 metatables。我们可以对 metatable 设置了 __metatable 的值,getmetatable 将返回这个域的值,而调用 setmetatable 将会出错

bject = setmetatable({}, {__metatable = "You cannot access here"})
 
print(getmetatable(Object)) --> You cannot access heresetmetatable(Object, {})    --> 引发编译器报错

面向对象

在 Lua 中,我们可以使用表和函数实现面向对象。将函数和相关的数据放置于同一个表中就形成了一个对象。

local _M = {}
 
local mt = { __index = _M }
 
function _M.deposit (self, v)
    self.balance = self.balance + v
end
 
function _M.withdraw (self, v)
    if self.balance > v then
        self.balance = self.balance - v
    else
        error("insufficient funds")
    end
end
 
function _M.new (self, balance)
    balance = balance or 0
    return setmetatable({balance = balance}, mt)
end
 
return _M

引用

local account = require("account")
 
local a = account:new()
a:deposit(100)
 
local b = account:new()
b:deposit(50)
 
print(a.balance)  --> output: 100
print(b.balance)  --> output: 50

上面这段代码 “setmetatable ({balance = balance}, mt)“,其中 mt 代表 { __index = _M } ,这句话值得注意。根据我们在元表这一章学到的知识,我们明白,setmetatable 将 _M 作为新建表的原型,所以在自己的表内找不到 ‘deposit’、‘withdraw’ 这些方法和变量的时候,便会到 __index 所指定的 _M 类型中去寻找。

继承

继承可以用元表实现,它提供了在父类中查找存在的方法和变量的机制。在 Lua 中是不推荐使用继承方式完成构造的,这样做引入的问题可能比解决的问题要多,下面一个是字符串操作类库,给大家演示一下。

---------- s_base.lualocal _M = {}
 
local mt = { __index = _M }
 
function _M.upper (s)return string.upper(s)
endreturn _M
 
---------- s_more.lualocal s_base = require("s_base")
 
local _M = {}
_M = setmetatable(_M, { __index = s_base })
 
 
function _M.lower (s)return string.lower(s)
endreturn _M
 
---------- test.lualocal s_more = require("s_more")
 
print(s_more.upper("Hello"))   -- output: HELLOprint(s_more.lower("Hello"))   -- output: hello

成员私有性

在动态语言中引入成员私有性并没有太大的必要,反而会显著增加运行时的开销,毕竟这种检查无法像许多静态语言那样在编译期完成。下面的技巧把对象作为各方法的 upvalue,本身是很巧妙的,但会让子类继承变得困难,同时构造函数动态创建了函数,会导致构造函数无法被 JIT 编译。

在 Lua 中,成员的私有性,使用类似于函数闭包的形式来实现。在我们之前的银行账户的例子中,我们使用一个工厂方法来创建新的账户实例,通过工厂方法对外提供的闭包来暴露对外接口。而不想暴露在外的例如 balance 成员变量,则被很好的隐藏起来。

function newAccount (initialBalance)
    local self = {balance = initialBalance}
    local withdraw = function (v)
        self.balance = self.balance - v
    end
    local deposit = function (v)
        self.balance = self.balance + v
    end
    local getBalance = function () 
        return self.balance 
    end
    
    return {
        withdraw = withdraw,
        deposit = deposit,
        getBalance = getBalance
    }
end
 
a = newAccount(100)
a.deposit(100)
print(a.getBalance()) --> 200print(a.balance)      --> nil

局部变量

Lua 的设计有一点很奇怪,在一个 block 中的变量,如果之前没有定义过,那么认为它是一个全局变量而不是这个 block 的局部变量。这一点和别的语言不同。容易造成不小心覆盖了全局同名变量的错误

定义

Lua 中的局部变量要用 local 关键字来显式定义,不使用 local 显式定义的变量就是全局变量

g_var = 1         -- global var
local l_var = 2   -- local var

作用域

局部变量的生命周期是有限的,它的作用域仅限于声明它的块(block)。一个块是一个控制结构的执行体、或者是一个函数的执行体再或者是一个程序块(chunk)。

x = 10
local i = 1         -- 程序块中的局部变量 i
 
while i <=x do
    local x = i * 2   -- while 循环体中的局部变量 x
    print(x)          -- output: 2, 4, 6, 8, ...
    i = i + 1
end
 
if i > 20 then
    local x           -- then 中的局部变量 x
    x = 20
    print(x + 2)      -- 如果i > 20 将会打印 22,此处的 x 是局部变量
else
    print(x)          -- 打印 10,这里 x 是全局变量
end
 
print(x)            -- 打印 10

使用局部变量的好处

  1. 局部变量可以避免因为命名问题污染了全局环境

  2. Local 变量的访问比全局变量更快

  3. 由于局部变量出了作用域之后生命周期结束,这样可以被垃圾回收器及时释放

检测模块的函数使用局部变量

Foo. Lua

local _M = { _VERSION = '0.01' }
 
function _M.add(a, b)     --两个number型变量相加
    return a + b
end
 
function _M.update_A()    --更新变量值
    A = 365               -- A 是全局变量
end
 
return _M

Use_foo. Lua

A = 360     --定义全局变量
 
local foo = require("foo")
 
local b = foo.add(A, A)
print("b = ", b)
 
foo.update_A()
print("A = ", A)

因为 A 是全局变量,改变了 A 的值

Lua 上下文中应当严格避免使用自己定义的全局变量。可以使用一个 lj-releng 工具来扫描 Lua 代码,定位使用 Lua 全局变量的地方。Lj-releng 的相关链接:https://github.com/openresty/openresty-devel-utils/blob/master/lj-releng

Windows 用户把 lj-releng 文件所在的目录的绝对路径添加进 PATH 环境变量。然后进入你自己的 Lua 文件所在的工作目录,得到如下结果:

#  lj-releng
foo.lua: 0.01 (0.01)
Checking use of Lua global variables in file foo.lua...
op no.  line  instruction args  ; code
2  [8] SETGLOBAL 0 -1  ; A
Checking line length exceeding 80...
WARNING: No "_VERSION" or "version" field found in `use_foo.lua`.
Checking use of Lua global variables in file use_foo.lua...
op no.  line  instruction args  ; code
2  [1] SETGLOBAL 0 -1  ; A
7  [4] GETGLOBAL 2 -1  ; A
8  [4] GETGLOBAL 3 -1  ; A
18 [8] GETGLOBAL 4 -1  ; A

当然,更推荐采用 luacheck 来检查项目中全局变量,之后的“代码静态分析”一节,我们还会讲到如何使用 luacheck

判断数组的大小

  • Table.Getn (t) 等价于 t 但计算的是数组元素,不包括 hash 键值。而且数组是以第一个 nil 元素来判断数组结束。

  • # 只计算 array 的元素个数,它实际上调用了对象的 metatable 的 __len 函数。对于有 __len 方法的函数返回函数返回值,不然就返回数组成员数目

  • Lua 内部实际采用哈希表和数组分别保存键值对、普通值,所以不推荐混合使用这两种赋值方式。

  • Lua 数组中允许 nil 值的存在,但是数组默认结束标志却是 nil。这类比于 C 语言中的字符串,字符串中允许 ‘\0’ 存在,但当读到 ‘\0’ 时,就认为字符串已经结束了。

  • 初始化是例外,在 Lua 相关源码中,初始化数组时首先判断数组的长度,若长度大于 0 ,并且最后一个值不为 nil,返回包括 nil 的长度;若最后一个值为 nil,则返回截至第一个非 nil 值的长度。

  • 如果你要删除一个数组中的元素,请使用 remove 函数,而不是用 nil 赋值

-- test.lua
local tblTest1 = { 1, a = 2, 3 }
print("Test1 " .. #(tblTest1))
 
local tblTest2 = { 1, nil }
print("Test2 " .. #(tblTest2))
 
local tblTest3 = { 1, nil, 2 }
print("Test3 " .. #(tblTest3))
 
local tblTest4 = { 1, nil, 2, nil }
print("Test4 " .. #(tblTest4))
 
local tblTest5 = { 1, nil, 2, nil, 3, nil }
print("Test5 " .. #(tblTest5))
 
local tblTest6 = { 1, nil, 2, nil, 3, nil, 4, nil }
print("Test6 " .. #(tblTest6))

我们分别使用 Lua 和 LuaJIT 来执行一下:

➜ luajit test.lua
Test1 2
Test2 1
Test3 1
Test4 1
Test5 1
Test6 1
 
➜ lua test.lua
Test1 2
Test2 1
Test3 3
Test4 1
Test5 3
Test6 1

这一段的输出结果,就是这么 匪夷所思。不要在 Lua 的 table 中使用 nil 值,如果一个元素要删除,直接 remove,不要用 nil 去代替

非空判断

有时候不小心引用了一个没有赋值的变量,这时它的值默认为 nil。如果对一个 nil 进行索引的话,会导致异常。

local person = {name = "Bob", sex = "M"}
 
-- do something
person = nil
-- do something
print(person.name)

会报错

stdin:1:attempt to index global 'person' (a nil value)
stack traceback:
   stdin:1: in main chunk
   [C]: ?

在实际的工程代码中,我们很难这么轻易地发现我们引用了 nil 变量。因此,在很多情况下我们在访问一些 table 型变量时,需要先判断该变量是否为 nil,例如将上面的代码改成

local person = {name = "Bob", sex = "M"}
 
-- do something
person = nil
-- do something
if person ~= nil and person.name ~= nil then
    print(person.name)
else
-- do somethingend

对于简单类型的变量,我们可以用 if (var == nil) then 这样的简单句子来判断。但是对于 table 型的 Lua 对象,就不能这么简单判断它是否为空了。一个 table 型变量的值可能是 {},这时它不等于 nil。我们来看下面这段代码:

local next = next
local a = {}
local b = {name = "Bob", sex = "Male"}
local c = {"Male", "Female"}
local d = nil
 
print(#a)
print(#b)
print(#c)
--print(#d)    -- error
 
if a == nil then
    Print ("a == nil")
End
 
If b == nil then
    Print ("b == nil")
End
 
If c == nil then
    Print ("c == nil")
End
 
If d== nil then
    Print ("d == nil")
End
 
If next (a) == nil then
    Print ("next (a) == nil")
End
 
If next (b) == nil then
    Print ("next (b) == nil")
End
 
If next (c) == nil then
    Print ("next (c) == nil")
End

输出

0
0
2
D == nil
Next (a) == nil

因此,我们要判断一个 table 是否为 {},不能采用 #table == 0 的方式来判断。可以用下面这样的方法来判断:

Function isTableEmpty (t)
    Return t == nil or next (t) == nil
End

注意:next 指令是不能被 LuaJIT 的 JIT 编译优化,并且 LuaJIT 貌似没有明确计划支持这个指令优化,在不是必须的情况下,尽量少用。

正则表达式

同时存在两套正则表达式规范:Lua 语言的规范和 ngx. Re.* 的规范,即使您对 Lua 语言中的规范非常熟悉,我们仍不建议使用 Lua 中的正则表达式。

  • 一是因为 Lua 中正则表达式的性能并不如 ngx. Re.* 中的正则表达式优秀;

  • 二是 Lua 中的正则表达式并不符合 POSIX 规范,而 ngx. Re.* 中实现的是标准的 POSIX 规范,后者明显更具备通用性。

ngx. Re.* 中的 o 选项,指明该参数,被编译的 Pattern 将会在工作进程中缓存,并且被当前工作进程的每次请求所共享。Pattern 缓存的上限值通过 lua_regex_cache_max_entries 来修改,它的默认值为 1024。

ngx. Re.* 中的 j 选项,指明该参数,如果使用的 PCRE 库支持 JIT,OpenResty 会在编译 Pattern 时启用 JIT。启用 JIT 后正则匹配会有明显的性能提升。较新的平台,自带的 PCRE 库均支持 JIT。如果系统自带的 PCRE 库不支持 JIT,出于性能考虑,最好自己编译一份 libpcre. So,然后在编译 OpenResty 时链接过去。要想验证当前 PCRE 库是否支持 JIT,可以这么做

  1. 编译 OpenResty 时在 ./configure 中指定 --with-debug 选项

  2. error_log 指令中指定日志级别为 debug

  3. 运行正则匹配代码,查看日志中是否有 pcre JIT compiling result: 1

即使运行在不支持 JIT 的 OpenResty 上,加上 j 选项也不会带来坏的影响。在 OpenResty 官方的 Lua 库中,正则匹配至少都会带上 jo 这两个选项。

Location /test {
    Content_by_lua_block {
        local regex = [[\d+]]
 
        -- 参数 "j" 启用 JIT 编译,参数 "o" 是开启缓存必须的
        Local m = ngx.Re.Match ("hello, 1234", regex, "jo")
        If m then
            Ngx.Say (m[0])
        Else
            Ngx.Say ("not matched!")
        End
    }
}

Lua 正则简单汇总

Lua 中正则表达式语法上最大的区别,Lua 使用 ’%’ 来进行转义,而其他语言的正则表达式使用 符号来进行转义。其次,Lua 中并不使用 ’?’ 来表示非贪婪匹配,而是定义了不同的字符来表示是否是贪婪匹配。定义如下:

符号匹配次数匹配模式
+匹配前一字符 1 次或多次非贪婪
*匹配前一字符 0 次或多次贪婪
-匹配前一字符 0 次或多次非贪婪
?匹配前一字符 0 次或 1 次仅用于此,不用于标识是否贪婪
符号匹配模式
.任意字符
%a字母
%c控制字符
%d数字
%l小写字母
%p标点字符
%s空白符
%u大写字母
%w字母和数字
%x十六进制数字
%z代表 0 的字符

虚变量

当一个方法返回多个值时,有些返回值有时候用不到,要是声明很多变量来一一接收,显然不太合适(不是不能)。Lua 提供了一个虚变量 (dummy variable)的概念,按照**惯例**以一个下划线(“_”)来命名,用它来表示丢弃不需要的数值,仅仅起到占位的作用。

返回值

-- string. Find (s, p) 从 string 变量 s 的开头向后匹配 string
-- p,若匹配不成功,返回 nil,若匹配成功,返回第一次匹配成功
-- 的起止下标。
 
Local start, finish = string.Find ("hello", "he") --start 值为起始下标,finish
--值为结束下标
Print ( start, finish )                          --输出 1   2
 
Local start = string.Find ("hello", "he")      -- start 值为起始下标
Print ( start )                               -- 输出 1
 
 
Local _, finish = string.Find ("hello", "he")   --采用虚变量(即下划线),接收起
--始下标值,然后丢弃,finish 接收
--结束下标值
Print ( finish )                              --输出 2
Print ( _ )    

迭代

-- test. Lua 文件
Local t = {1, 3, 5}
 
Print ("all  data: ")
For i, v in ipairs (t) do
    Print (i, v)
End
 
Print ("")
Print ("part data: ")
For _, v in ipairs (t) do
    Print (v)
End

输出

# Luajit test. Lua
All  data:
1   1
2   3
3   5
 
Part data:
1
3
5

抵制使用 module () 定义模块

旧式的模块定义方式是通过 module ("filename"[, package. Seeall])* 来显式声明一个包,现在官方不推荐再使用这种方式

这种方式将会返回一个由 filename 模块函数组成的 table,并且还会定义一个包含该 table 的全局变量。

  1. package. Seeall 这种方式破坏了模块的高内聚,原本引入 “filename” 模块只想调用它的 foobar () 函数,但是它却可以读写全局属性,例如 "filename. Os"

  2. module 函数压栈操作引发的副作用,污染了全局环境变量。例如 module ("filename") 会创建一个 filenametable,并将这个 table 注入全局环境变量中,这样使得没有引用它的文件也能调用 filename 模块的方法。

推荐的模块定义

-- square. Lua 长方形模块
Local _M = {}           -- 局部的变量
_M._VERSION = '1.0'     -- 模块版本
 
Local mt = { __index = _M }
 
function _M.new (self, width, height)
    Return setmetatable ({ width=width, height=height }, mt)
End
 
function _M.get_square (self)
    Return self. Width * self. Height
End
 
function _M.get_circumference (self)
    Return (self. Width + self. Height) * 2
End
 
Return _M

使用

Local square = require "square"
Local s 1 = square: new (1, 2)
Print (s 1: get_square ())          --output: 2
Print (s 1: get_circumference ())   --output: 6

另一个跟 Lua 的 module 模块相关需要注意的点是,当 lua_code_cache on 开启时,require 加载的模块是会被缓存下来的,这样我们的模块就会以最高效的方式运行,直到被显式地调用如下语句(这里有点像模块卸载):

Package. Loaded["square"] = nil

调用函数前先定义函数

Lua 里面的函数必须放在调用的代码之前,下面的代码是一个常见的错误:

-- test. Lua 文件 local i = 100
I = add_one (i)
 
Function add_one (i)
    Return i + 1
End

因此在函数定义之前使用函数相当于在变量赋值之前使用变量,Lua 世界对于没有赋值的变量,默认都是 nil,所以这里也就产生了一个 nil 的错误。

点号操作符和冒号操作符的区别

Local str = "abcde"
 
Print ("case 1: ", str: sub (1, 2))
Print ("case 2: ", str.Sub (str, 1, 2))

输出

Case 1: ab
Case 2: ab
  • 冒号操作会带入一个 self 参数,用来代表 自己****。

  • 而点号操作,只是 内容 的展开。

在函数定义时,使用冒号将默认接收一个 self 参数,而使用点号则需要显式传入 self 参数

示例代码:

Obj = { x = 20 }
 
Function obj: fun 1 ()
    Print (self. X)
End

等价于

Obj = { x = 20 }
 
Function obj. Fun 1 (self)
    Print (self. X)
End

Module 的缺点

由于 lua_code_cache off 情况下,缓存的代码会伴随请求完结而释放。Module 的最大好处缓存这时候是无法发挥的,所以本章的内容都是基于 lua_code_cache on 的情况下。

先看看下面代码:

Local ngx_socket_tcp = ngx. Socket. Tcp           -- ①
 
Local _M = { _VERSION = '0.06' }                -- ②
Local mt = { __index = _M }                     -- ③
 
function _M.new (self)
    Local sock, err = ngx_socket_tcp ()          -- ④
    If not sock then
        Return nil, err
    End
    Return setmetatable ({ sock = sock }, mt)    -- ⑤
End
 
function _M.set_timeout (self, timeout)
    Local sock = self. Sock
    If not sock then
        Return nil, "not initialized"
    End
 
    Return sock: settimeout (timeout)
End
 
-- ... 其他功能代码,这里简略
 
Return _M
  1. 对于比较底层的模块,内部使用到的非本地函数,都需要 local 本地化,这样做的好处:

    1. 避免命名冲突:防止外部是 require (...) 的方法调用造成全局变量污染

    2. 访问局部变量的速度比全局变量更快、更快、更快(重要事情说三遍)

  2. 每个基础模块最好有自己 _VERSION 标识,方便后期利用 _VERSION 完成热代码部署等高级特性,也便于使用者对版本有整体意识。

  3. 其实 _Mmt 对于不同的请求实例(require 方法得到的对象)是相同的,因为 module 会被缓存到全局环境中。所以在这个位置千万不要放单请求内个性信息,例如 ngx. Ctx 等变量。

  4. 这里需要实现的是给每个实例绑定不同的 tcp 对象,后面 setmetatable 确保了每个实例拥有自己的 socket 对象,所以必须放在 new 函数中。如果放在 ③ 的下面,那么这时候所有的不同实例内部将绑定了同一个 socket 对象。

Local mt = { __index = _M }                     -- ③
Local sock = ngx_socket_tcp ()                   -- ④ 错误的
 
function _M.new (self)
    Return setmetatable ({ sock = sock }, mt)    -- ⑤
End
  1. Lua 的 module 有两种类型:

    1. 支持面向对象痕迹可以保留私有属性;静态方法提供者,没有任何私有属性。

    2. 真正起到区别作用的就是 setmetatable 函数,是否有自己的个性元表,最终导致两种不同的形态。

FFI

https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/FFI.html

FFI 库,是 LuaJIT 中最重要的一个扩展库。它允许从纯 Lua 代码调用外部 C 函数,使用 C 数据结构。

FFI 库最大限度的省去了使用 C 手工编写繁重的 Lua/C 绑定的需要。不需要学习一门独立/额外的绑定语言——它解析普通 C 声明。这样可以从 C 头文件或参考手册中,直接剪切,粘贴。它的任务就是绑定很大的库,但不需要捣鼓脆弱的绑定生成器。

FFI 紧紧的整合进了 LuaJIT(几乎不可能作为一个独立的模块)。JIT 编译器在 C 数据结构上所产生的代码,等同于一个 C 编译器应该生产的代码。在 JIT 编译过的代码中,调用 C 函数,可以被内连处理,不同于基于 Lua/C API 函数调用。

ffi 库词汇

nounExplanation
cdeclA definition of an abstract C type (actually, is a lua string)
ctypeC type object
cdataC data object
ctC type format, is a template object, may be cdecl, cdata, ctype
cbcallback object
VLAAn array of variable length
VLSA structure of variable length

ffi. API*

功能: Lua ffi 库的 API,与 LuaJIT 不可分割。

毫无疑问,在 lua 文件中使用 ffi 库的时候,必须要有下面的一行。

Local ffi = require "ffi"

JIT

看一下 LuaJIT 官方的解释:LuaJIT is a Just-In-Time Compilerfor the Lua programming language。

LuaJIT 的运行时环境包括一个用手写汇编实现的 Lua 解释器和一个可以直接生成机器代码的 JIT 编译器

  • 一开始的时候,Lua 字节码总是被 LuaJIT 的解释器解释执行。LuaJIT 的解释器会在执行字节码时同时记录一些运行时的统计信息,比如每个 Lua 函数调用入口的实际运行次数,还有每个 Lua 循环的实际执行次数。

  • 当这些次数超过某个预设的阈值时,便认为对应的 Lua 函数入口或者对应的 Lua 循环足够的“热”,这时便会触发 JIT 编译器开始工作。

  • JIT 编译器会从热函数的入口或者热循环的某个位置开始尝试编译对应的 Lua 代码路径。编译的过程是把 LuaJIT 字节码先转换成 LuaJIT 自己定义的中间码(IR),然后再生成针对目标体系结构的机器码(比如 x 86_64 指令组成的机器码)

  • 如果当前 Lua 代码路径上的所有的操作都可以被 JIT 编译器顺利编译,则这条编译过的代码路径便被称为一个“trace”,在物理上对应一个 trace 类型的 GC 对象(即参与 Lua GC 的对象)。

JIT 编译器不支持的原语被称为 NYI(Not Yet Implemented)原语。比较完整的 NYI 列表在这篇文档里面:

http://wiki.luajit.org/NYI

所谓“让更多的 Lua 代码被 JIT 编译”,其实就是帮助更多的 Lua 代码路径能为 JIT 编译器所接受。这一般通过两种途径来实现:

  1. 调整对应的 Lua 代码,避免使用 NYI 原语

  2. 增强 JIT 编译器,让越来越多的 NYI 原语能够被编译。

可以被 JIT 编译的元操作

下面给大家列一下截止到目前已经可以被 JIT 编译的元操作。其他还有 IO、Bit、FFI、Coroutine、OS、Package、Debug、JIT 等分类,使用频率相对较低,这里就不罗列了,可以参考官网:http://wiki.luajit.org/NYI

基础库的支持情况

函数编译?备注
assertyes
collectgarbageno
dofilenever
errornever
getfenv2.1 partial只有 getfenv (0) 能编译
getmetatableyes
ipairsyes
loadnever
loadfilenever
loadstringnever
nextno
pairsno
pcallyes
printno
rawequalyes
rawgetyes
rawlen (5.2)yes
rawsetyes
selectpartial第一个参数是静态变量的时候可以编译
setfenvno
setmetatableyes
tonumberpartial不能编译非 10 进制,非预期的异常输入
tostringpartial只能编译:字符串、数字、布尔、nil 以及支持 __tostring 元方法的类型
typeyes
unpackno
xpcallyes

字符串库

函数编译?备注
string. Byteyes
string. Char2.1
string. Dumpnever
string. Find2.1 partial只有字符串样式查找(没有样式)
string. Format2.1 partial不支持 %p 或非字符串参数的 %s
string. Gmatchno
string. Gsubno
string. Lenyes
string. Lower2.1
string. Matchno
string. Rep2.1
string. Reverse2.1
string. Subyes
string. Upper2.1

函数编译?备注
table. Concat2.1
table. Foreachno2.1: 内部编译,但还没有外放
table. Foreachi2.1
table. Getnyes
table. Insertpartial只有 push 操作
table. Maxnno
table. Pack (5.2)no
table. Remove2.1部分,只有 pop 操作
table. Sortno
table. Unpack (5.2)no

math 库

函数编译?备注
math. Absyes
math. Acosyes
math. Asinyes
math. Atanyes
math. Atan 2yes
math. Ceilyes
math. Cosyes
math. Coshyes
math. Degyes
math. Expyes
math. Flooryes
math. Fmodno
math. Frexpno
math. Ldexpyes
math. Logyes
math. Log 10yes
math. Maxyes
math. Minyes
math. Modfyes
math. Powyes
math. Radyes
math. Randomyes
math. Randomseedno
math. Sinyes
math. Sinhyes
math. Sqrtyes
math. Tanyes
math. Tanhyes