Lua语言入门

2016-11-24 | 阅读

Lua 是一种轻量小巧的脚本语言,用标准C语言编写,并开源。 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。 Lua 是巴西里约热内卢天主教大学里的一个研究小组(由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo所组成)于1993年开发。

特性

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

安装Lua环境

mac :

curl -R -O http://www.lua.org/ftp/lua-5.3.3.tar.gz
tar zxf lua-5.3.3.tar.gz
cd lua-5.3.3
sudo make macosx test
sudo make install

然后编写hello world. 创建一个Helloworld.lua文件, 代码 :

print("Hello World!")

执行命令

lua Helloworld.lua

即可看到输出。

Lua的简单语法

两个减号是单行注释 :

--

多行注释 :

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

标识符 : Lua标识符以 字母A-Za-z_ 开头, 之后可以跟 A-Za-z_0-9

尽量不要用下划线加大写字母开头的标识符, 因为Lua的保留字是这样的.

Lua的标识符区分大小写, 不支持特殊字符.

全局变量

默认情况下,变量都被认为是全局的, 全局变量不需要声明, 给一个变量赋值后,就创建了这个全局变量.

访问一个没有初始化的全局变量, 不会出错, 只不过结果是nil ,如 :

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

而删除一个全局变量, 只需要将变量赋值为nil. 即只有一个变量不等于nil时, 变量才算是存在.

数据类型

Lua是动态类型语言, 变量不需要类型定义, 只需要为变量赋值。Lua的基本类型包括以下8种 :

  • nil : 只有一个值nil, 在条件表达式中相当于false.
  • boolean : truefalse
  • number : 表示双精度的实 浮点数.
  • string : 字符串以一对双引号或者单引号表示.
  • function : 由C或lua编写的函数
  • userdata : 是一种用户自定义数据, 用于表示一种由应用程序或 C/C++ 的任意数据类型的数据.
  • thread : 表示执行的独立线路, 用于执行协同程序
  • table : 表实际是一个关联数组, 数组的索引可以是数字或者字符串.通过 构造表达式创建, 最简单的表达式就是 {} 表示创建一个空表.

可以通过type函数测试变量或者值得类型 :

print(type("Hello world"))      --> string
print(type(10.4*3))             --> number
print(type(print))              --> function
print(type(type))               --> function
print(type(true))               --> boolean
print(type(nil))                --> nil
print(type(type(X)))            --> string

nil

nil类型表示一种没有任何有效值.

而nil的删除作用, 是极其重要的, 如删除table中得数据 :

tab1 = { key1 = "val1", key2 = "val2", "val3" }
for k, v in pairs(tab1) do
    print(k .. " - " .. v)
end
 
tab1.key1 = nil
for k, v in pairs(tab1) do
    print(k .. " - " .. v)
end

第二次输出的结果是 :

1 - val3
key2 - val2

number

Lua中只有一种number类型, 即双精度的浮点数, double .

但可以在luaconf.h中修改默认类型.

string

字符串可以由 单引号或者双引号来包含:

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

也可以使用 [[ ]] 来表示一块字符串 :

string3 = [[
print 'hello world'
]]

当对一个字符串进行算术操作时, Lua会尝试将字符串转换为一个数字 :

> print("2" + 6)
8.0
> print("2" + "6")
8.0
> print("2 + 6")
2 + 6
> print("-2e2" * "6")
-1200.0
> print("error" + 1)
stdin:1: attempt to perform arithmetic on a string value
stack traceback:
	stdin:1: in main chunk
	[C]: in ?
> 

转换失败就会报错 .

而字符串的连接使用 .. :

> print("a" .. 'b')
ab
> print(157 .. 428)
157428
> 

Lua也会自动的将数字类型转换为字符串类型.

使用 #放在字符串前计算字符串的长度 :

> print(#"www.w3cschool.cc")
16

table

table既是一个 list,也是一个map, 这是一个很有趣的结构体 . 一般来说, Lua的table结构是一个关联数组, 这个数组的索引可以是数字,也可以是字符串. 而我们可以这样理解, 如果索引是数字,那就是一个数组,如果是字符串,那就是个map.

table是动态的,可以随意进行 添加、修改、删除操作. 通过 table[ key | index ] 或者 table.key来访问元素 .

以字符串为索引和以数字为索引,两者是隔离的 :

local tbl = {fruit1 = "apple", "pear",fruit2= "orange", "grape"}
for key, val in pairs(tbl) do
    print("Key", key)
end

输出的结果是 :

Key	       1
Key    	2
Key    	fruit1
Key	       fruit2

所以 table是 一个list + map.

function

函数在 Lua 中是第一等的, 可以存在变量中.

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

function anonymous(tab, fun)
    for k, v in pairs(tab) do
        print(fun(k, v))
    end
end
tab = { key1 = "val1", key2 = "val2" }
anonymous(tab, function(key, val)
    return key .. " = " .. val
end)

thread

在lua里, 最主要的线程, 是协同程序 coroutine . 线程拥有自己独立的栈, 局部变量和指令函数 .

线程与协程的区别是: 线程可以同时多个运行, 而协程任意时刻只能运行一个,且处于运行状态的协程只有被挂起 才会短暂暂停.

变量

Lua中的变量分为 全局变量,局部变量和表中的变量. 除非显示的使用local声明为局部变量, 不然所有变量都是默认为全局变量 :

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*x       <-->       a=10; b=2*x

遇到赋值语句时, Lua会先计算右边所有的值, 然后再进行赋值操作,所以我们可以在一个赋值语句中,进行交换变量的操作 :

x, y = y, x                     -- swap 'x' for 'y'
a[i], a[j] = a[j], a[i]         -- swap 'a[i]' for 'a[j]'

如果左右的值个数不同, 对于值较多的情况, 那就忽略多余的值. 而对于值少于变量的情况,会按照变量个数 补齐 nil :

a, b, c = 0
print(a,b,c)             --> 0   nil   nil

函数

Lua的函数的一个特点是, 支持多个结果. 如 :

function maximum (a)
    local mi = 1             -- 最大值索引
    local m = a[mi]          -- 最大值
    for i,val in ipairs(a) do
       if val > m then
           mi = i
           m = val
       end
    end
    return m, mi
end

print(maximum({8,10,23,12,5}))

输出结果为 23 3

还有一个特点就是,可以接受 可变数目的参数 ,与C语言类似, 在函数参数列表中,使用三点...来表示函数有可变的参数.

Lua 把函数的参数放在一个叫做 arg的表中, 使用 #arg来获取传入参数的个数.

如编写一个取平均值的函数 :

function average(...)
   result = 0
   local arg={...}
   for i,v in ipairs(arg) do
      result = result + v
   end
   print("总共传入 " .. #arg .. " 个数")
   return result/#arg
end

print("平均值为",average(10,5,3,4,5,6))

Lua迭代器

迭代器 是一种对象, 用于遍历标准模板库容器中得部分或全部元素. 在Lua中的迭代器 是一种支持指针类型的结构, 可以遍历集合的每个元素.

泛型for迭代器

迭代器有三个值 :迭代函数,状态常量,控制变量 。一个常见的泛型for迭代器的使用 :

array = {"Lua", "Tutorial"}

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

这里使用了 Lua默认提供的迭代器 ipairs, 我们来看一下这个迭代for循环的执行过程

  • 首先, 初始化, 计算 in 后面表达式的值,以从 迭代器ipairs中获取迭代函数、控制变量和状态常量。
  • 然后, 将状态常量和控制变量作为参数来调用迭代函数。
  • 将迭代函数的结果 赋值给变量列表。
  • 如果迭代函数的返回值的第一个值是nil,则表示循环结束,否则 执行循环体。
  • 回到第二步, 继续调用迭代函数.

迭代器分为无状态和多状态两种 :

无状态的迭代器

无状态的迭代器, 指不保留任何状态的迭代器. 因此,循环中, 我们可以利用无状态迭代器,避免创建闭包的花费。每一次迭代, 迭代函数都用两个变量的值作为参数调用 。 无状态的迭代器, 只能利用这两个值去获取下一个元素.

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

for i,n in square,3,0
do
   print(i,n)
end

在这个for循环的迭代中, 我们传入了迭代函数square、状态常量3和控制变量0 , 所以这里输出的结果是 :

1	1
2	4
3	9

上面讨论的默认的table的迭代器ipairs,其实现大致如下 :

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

通过ipairs(array)去获取到三个值, 迭代函数 iter ,状态常量array, 和控制变量初始值 0 。然后遍历列表中所有元素。

多状态的迭代器

很多情况下, 迭代器要保存多个状态信息,而不是简单地状态常量和控制变量. 这种情况下, 可以使用闭包, 或者将所有状态信息封装到一个table内,将这个table作为迭代器的状态常量, 这种情况下, 也不需要其他参数:

array = {"Lua", "Tutorial"}

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

这里,将状态记录在闭包中,遍历table

Lua 模块与包

模块相当于一个封装库. 从Lua5.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

require 函数

Lua提供了一个名为 require的函数来加载模块, 如 :

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

执行 require后, 会返回一个由模块常量或函数组成的table , 并且还会定义一个包含了该table的全局变量.

-- test_module.lua 文件
-- module 模块为上文提到到 module.lua
require("module")
 
print(module.constant)
 
module.func3()

或者给加载的模块定义成一个变量 :

local m = require("module")
 
print(m.constant)

元表与面向对象

lua 的面向对象,主要通过元表来实现,而元表 metatable主要用来进行table的操作符重载,提供两个方法:

  • setmetatable(table, metatable):此方法用于为一个表设置元表。
  • getmetatable(table):此方法用于获取表的元表对象。

一般如下使用:

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

元表中的函数,第一个参数为self,表示数据, 如我们使用元表来重载_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

元表为方法,而table为数据, 所以面向对象在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

点与冒号的区别: 上面我们可以看到面向对象的函数中,都会有一个self参数,而这个self就是点与冒号的区别所在, 使用冒号时, 默认将自己作为self参数传入、 如果使用点, 就需要写全函数了。

加载机制

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

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

当然,如果没有 LUA_PATH 这个环境变量,也可以自定义设置,在当前用户根目录下打开 .profile 文件(没有则创建,打开 .bashrc 文件也可以),例如把 ~/lua/ 路径加入 LUA_PATH 环境变量里:

#LUA_PATH
export LUA_PATH="~/lua/?.lua;;"

文件路径以 “;” 号分隔,最后的 2 个 “;;” 表示新加的路径后面加上原来的默认路径。 接着,更新环境变量参数,使之立即生效。

source ~/.profile

这时假设 package.path 的值是:

./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua

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

./module.lua
/usr/local/share/lua/5.1/module.lua
/usr/local/share/lua/5.1/module/init.lua
/usr/local/lib/lua/5.1/module.lua
/usr/local/lib/lua/5.1/module/init.lua

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

C库 搜索的文件路径是从全局变量 package.cpath 获取,而这个变量则是通过环境变量 LUA_CPATH 来初始。搜索的策略跟上面的一样,只不过现在换成搜索的是 so 或 dll 类型的文件。如果找得到,那么 require 就会通过 package.loadlib 来加载它。

C 包

Lua和C是很容易结合的,我们可以方便的通过C来为Lua扩展功能。

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

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

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

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

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

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

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

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

Lua 协同程序 coroutine

Lua 中的 coroutine与线程比较, 一个程序可以同时运行多个线程,而在任一时刻,只有一个coroutine在运行。即coroutine是通过共用一个线程实现的多个线程。

基本操作

  • coroutine.create() 创建coroutine,返回coroutine, 参数是一个函数,当和resume配合使用的时候就唤醒函数调用
  • coroutine.resume() 重启coroutine,和create配合使用
  • coroutine.yield() 挂起coroutine,将coroutine设置为挂起状态,这个和resume配合使用能有很多有用的效果
  • coroutine.status() 查看coroutine的状态 .注:coroutine的状态有三种:dead,suspend,running,具体什么时候有这样的状态请参考下面的程序
  • coroutine.wrap() 创建coroutine,返回一个函数,一旦你调用这个函数,就进入coroutine,和create功能重复
  • coroutine.running() 返回正在跑的coroutine,一个coroutine就是一个线程,当使用running的时候,就是返回一个corouting的线程号

演示 :

-- coroutine_test.lua 文件
co = coroutine.create(
    function(i)
        print(i);
    end
)
 
coroutine.resume(co, 1)   -- 1
print(coroutine.status(co))  -- dead
 
print("----------")
 
co = coroutine.wrap(
    function(i)
        print(i);
    end
)
 
co(1)
 
print("----------")
 
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) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3
 
print(coroutine.status(co2))   -- suspended
print(coroutine.running())
 
print("----------")

create一个coroutine的时候就是在新线程中注册了一个事件。 当使用resume触发事件的时候,createcoroutine函数就被执行了,当遇到yield的时候就代表挂起当前线程,等候再次resume触发事件。

resumeyield的配合强大之处在于,resume处于主程中,它将外部状态(数据)传入到协同程序内部;而yield则将内部的状态(数据)返回到主程中。

生产者消费者示例 :

local newProductor

function productor()
     local i = 0
     while true do
          i = i + 1
          send(i)     -- 将生产的物品发送给消费者
     end
end

function consumer()
     while true do
          local i = receive()     -- 从生产者那里得到物品
          print(i)
     end
end

function receive()
     local status, value = coroutine.resume(newProductor)
     return value
end

function send(x)
     coroutine.yield(x)     -- x表示需要发送的值,值返回以后,就挂起该协同程序
end

-- 启动程序
newProductor = coroutine.create(productor)
consumer()

lua直接调用C函数

将C函数代码生成库文件,然后让Lua解析器定位到这些库文件,并引用。

#include <stdio.h>
#include <string.h>
#include <lauxlib.h>
#include <lualib.h>

//待注册的C函数,该函数的声明形式在上面的例子中已经给出。
//需要说明的是,该函数必须以C的形式被导出,因此extern "C"是必须的。
//函数代码和上例相同,这里不再赘述。
int add(lua_State* L) 
{
    double op1 = luaL_checknumber(L,1);
    double op2 = luaL_checknumber(L,2);
    lua_pushnumber(L,op1 + op2);
    return 1;
}

int sub(lua_State* L)
{
    double op1 = luaL_checknumber(L,1);
    double op2 = luaL_checknumber(L,2);
    lua_pushnumber(L,op1 - op2);
    return 1;
}

//luaL_Reg结构体的第一个字段为字符串,在注册时用于通知Lua该函数的名字。
//第一个字段为C函数指针。
//结构体数组中的最后一个元素的两个字段均为NULL,用于提示Lua注册函数已经到达数组的末尾。
static luaL_Reg mylibs[] = { 
    {"add", add},
    {"sub", sub},
    {NULL, NULL} 
}; 

//该C库的唯一入口函数。其函数签名等同于上面的注册函数。见如下几点说明:
//1. 我们可以将该函数简单的理解为模块的工厂函数。
//2. 其函数名必须为luaopen_xxx,其中xxx表示library名称。Lua代码require "xxx"需要与之对应。
//3. 在luaL_register的调用中,其第一个字符串参数为模块名"xxx",第二个参数为待注册函数的数组。
//4. 需要强调的是,所有需要用到"xxx"的代码,不论C还是Lua,都必须保持一致,这是Lua的约定,
//   否则将无法调用。
int luaopen_imageadjust(lua_State* L) 
{
    const char* libName = "imageadjust";
    // lua_register(L,libName,mylibs); 5.1可用
    lua_newtable(L);   
    //先把一个table压入VS,然后在调用luaL_setfuncs就会把所以的func存到table中  
    //注意不像luaL_register这个table是个无名table,可以在的使用只用一个变量来存入这个table。  
    //e.g local clib = require "libname". 这样就不会污染全局环境。比luaL_register更好。  
    luaL_setfuncs(L, mylibs, 0);  
    return 1;
}

然后将该库编译成.so库, 然后供使用。

gcc -w -shared -fPIC -o imageadjust.so imageadjust.c -I/usr/local/include/ -L/usr/local/lib/ /usr/local/lib/liblua.a

见如下Lua代码:

imageadjust = require "imageadjust"  --指定包名称
 
print(imageadjust.sub(20.1,19))

lua通过ffi调用C函数

ffi库, 允许从纯Lua代码调用外部C函数, 使用C数据结构:

local ffi = require("ffi") // 加载ffi库
ffi.cdef[[
// 在ffi库中声明函数 ,直接从需要调用的C的头文件中复制过来
 int printf(const char* fmt,...);
]]
// 最后调用C函数。
ffi.C.printf("Hello %s!","word");

上面是调用当前系统C环境所支持的函数,要调用一些库的函数时,还有一个常用接口为 ffi.load(name [,global]) , 表示加载指定的库文件,一般为 .so或者.dll文件。该函数的第二个参数是一个bool值, 表示是否创建全局空间, 默认应该为否

直接调用时 , lua传入的函数类型是 string ,也就是const char *,所以接口也必须是这个类型,如果是char *, 就会爆格式不符合的错误。

luajit

luajit 是高效的运行时编译器,性能比原始编译器要高很多。

所以可以直接使用 luajit xx.lua运行脚本。

但是 luajit中含有 ffi, 所以Openresty 必须绑定luajit.

使用LuaSocket

LuaSocket是Lua的网络模块库,它可以很方便地提供 TCP、UDP、DNS、FTP、HTTP、SMTP、MIME 等多种网络协议的访问操作。它由两部分组成:一部分是用 C 写的核心,提供对 TCP 和 UDP 传输层的访问支持。另外一部分是用 Lua 写的,负责应用功能的网络接口处理。

安装 :

luarocks install luasocket

使用

socket= require("socket")
local t0 = socket.gettime()

获取当前时间距 1970年的秒数.