旷世的忧伤

Huoty's Blog

Lua 初学笔记

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

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

Lua 应用场景大致有以下几类:

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

Lua 解释器可以通过编译安装获得,如:

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

编译完成后会得到两个二进制文件,lua 和 luac。其中,lua 为解释器,可以解释执行 lua 源码文件,或者进入交互是解释器默认;luac 为编译器,可将 lua 源码文件编译成二进制文件,以加快解释器载入代码的速度,但并不能提高运行速度。

:本文内容基于 lua 5.3。在 Lua 中索引值默认以 1 为开始。

基本语法

注释

Lua 以 -- 表示注释

-- 第一个 Lua 程序
print('Hello world!')

以上为单行注释,其也可放在语句尾。如果需要多行注释,可用如下形式:

--[[
第一个 Lua 程序
第一个程序通常都是输出 ‘Hello world’
--]]
print("Hello world!")  -- 输出语句

标识符

标识符在编程语言中通常用于定义一个变量,函数,类等。Lua 中的标识符以字母和下划线开头,并包含字母、下划线、数字。Lua 不允许使用特殊字符如 @, $, 和 % 来定义标识符。

Lua 是一个区分大小写的编程语言。因此在 Lua 中 Runoob 与 runoob 是两个不同的标识符。不建议使用下划线加大写字母的标识符,因为 Lua 很多内置变量是这样定义的,容易引起冲突。如 _ 标识符,通常用来表示可以被忽略的、不会使用到的变量:

-- 忽略数组索引
t = {'a', 'b', 'c'}
for _, v in ipairs(t) do
    print(v)
end

变量与作用域

默认情况下,Lua 中的变量总是全局的。全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是 nil。如果想删除一个全局变量,只需要将变量赋值为 nil 即可。

-- 全局变量
a = 1
print(a)  -- 输出 1
print(b)  -- 输出 nil

-- 删除全局变量
a = nil   -- 表示一个无效值,条件表达式中为 False
print(a)  -- 输出 nil

在 Lua 中,全局变量十分危险,很容易被篡改而又不容易察觉在哪里被篡改,这很容易导致难以调试的 Bug。Lua 中的变量默认是全局的,除非使用 local 关键字声明为局部变量。对于变量定义,有一条原则是, 在一切能使用 local 修饰的情况下,都使用 local 进行修饰。全局变量的处理速度比局部变量的速度要慢很多。

Lua 中的作用域以关键字 end 进行标识。每个作用域结束时,其中的局部变量被系统清理。有时,可以用 do .. end 来明确限定局部变量的作用域。

--[[
Lua 中的变量默认是全局的
除非用 local 关键字声明为局部变量

Lua 中的作用域以 end 进行标识
--]]

local v = 0
do
    v = v + 5
    local x = 1
    local y = 2
    z = 3
    print(v, x, y, z)
end
print(v, x, y, z)

--[[
输出:
5	1	2	3
5	nil	nil	3
--]]

可以同时针对多个变量进行赋值,赋值语句会先计算右边所有的值然后再执行赋值操作,当变量个数和值的个数不一致时,值数量不足变量数量时会被补足 nil,多余的值则会被舍弃:

local a, b, c = 1, 2
print(a, b, c)  --> 1, 2, nil

a, b = a+1, b+1, b+2
print(a, b)  --> 2, 3

a, b = b, a
print(a, b)  --> 3, 2

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

在 Lua 中 大小写是敏感的,如果定义变量 a 与 A,则其是两个不同的变量:

> a = 1
> A = 2
> a
1
> A
2

控制结构语句

流程控制

流程控制即根据条件表达式结果来判断要执行的代码分支,通常由 “if … else …” 语句实现。Lua 认为 false 和 nil 为假,true 和非 nil 为真

Lua 支持的控制结构语句包括:

  • if 语句: 由一个布尔表达式作为条件判断,其后紧跟其他语句组成
if(布尔表达式) then
   --[ 在布尔表达式为 true 时执行的语句 --]
end
  • if…else 语句: if 与 else 语句搭配使用, 在 if 条件表达式为 false 时执行 else 语句代码;当需要检测多个条件语句时,可以使用 if…elseif…else 语句
if 布尔表达式 then
   --[ 布尔表达式为 true 时执行该语句块 --]
else
   --[ 布尔表达式为 false 时执行该语句块 --]
end

if 布尔表达式1 then
   --[ 在布尔表达式1 为 true 时执行该语句块 --]
elseif 布尔表达式2 then
   --[ 在布尔表达式2 为 true 时执行该语句块 --]
elseif 布尔表达式3 then
   --[ 在布尔表达式3 为 true 时执行该语句块 --]
else
   --[ 如果以上布尔表达式都不为 true 则执行该语句块 --]
end

此外,流程控制语句可以嵌套使用,如可以在 if 或 else if 中使用一个或多个 if 或 else if 语句:

if 布尔表达式1 then
   --[ 布尔表达式 1 为 true 时执行该语句块 --]
   if 布尔表达式2 then
      --[ 布尔表达式 2 为 true 时执行该语句块 --]
   end
end

循环语句

程序设计中通常需要一种循环结构,能在一定条件下反复执行某段程序,这便是循环语句。Lua 中支持的循环语句结构包括:

  • while 循环: 先判断条件语句是否为 true,为 ture 时继续循环体,否则退出循环
while(condition) do
   statements
end
  • for 循环: 重复执行指定语句,重复次数可在 for 语句中控制。Lua 中 for 循环包括 数值 for 循环泛型 for 循环
-- 数值 for 循环
-- var 从 exp1 变化到 exp2,每次变化以 exp3 为步长递增 var,并执行一次"执行体"
-- exp3 是可选的,如果不指定,默认为 1
-- 三个表达式在循环开始前一次性求值,以后不再进行求值
for var = exp1, exp2, exp3 do  
    <执行体>  
end

-- 泛型 for 循环
-- 通过一个迭代器函数来遍历所有值,类似其他语言中的 foreach 语句
-- 如,打印数组 a 的所有值  
a = {"one", "two", "three"}
for i, v in ipairs(a) do
    print(i, v)
end
  • repeat…until: 重复执行循环,直到指定的条件为真时为止
repeat
   statements
until( condition )

-- 不同于 for 和 while循环,repeat...until 在当前循环结束后才判断条件表达式的值

各循环语句可以相互嵌套使用。Lua 提供了 break 语句来跳出循环,但没有其他语言中的 continue 语句。

数据类型

Lua 是动态类型语言,变量不要类型定义,解释器会在变量赋值时自动判断其类型。 Lua 中有 8 中基本类型,分别为:nil、boolean、number、string、table、function、userdata 和 thread。

nil 类型只有一个值,即 nil。布尔(boolean)类型包含两个值,即 false 和 true。在逻辑判断时,只有 falsenil 为假,其它值全为真。

Lua 中只有一种数字类型(不像其他高级语言中会区分 int, float, long 等),即 number。数字字符串与数字可以直接相加,最终得到数据类型:

-- 输出数据类型
print(type(nil))
print(type(false))
print(type(1))
print(type(1.0))

-- 数字字符串会按数字进行运算
print("2"+ 6)
print("1" + "5.6")
print(type('5.0' + '5.0'))

--[[
输出:
nil
boolean
number
number
8.0
6.6
number
--]]

字符串

字符串(String)是由数字、字母、下划线等字符组成的一串字符。Lua 中的字符串可以使用三种方式表示:

  • 单引号间的一串字符
  • 双引号间的一串字符
  • [[]] 间的一串字符
print('Hello world!')
print("Hello world!")
print([[Hello World!]])

字符串操作方式:

方法 说明
string.byte(s [, i [, j]]) 字符转化为数字
string.char(···) 数字转化为字符
string.dump(function [, strip]) 函数转化为二进制字符串
string.find(s, pattern [, init [, plain]]) 查找符合匹配模式的子串
string.format(formatstring, ···) 格式化字符串
string.match(s, pattern [, init]) 子串匹配
string.gmatch(s, pattern) match 的迭代形式
string.sub(s, i [, j]) 截取指定的子串
string.gsub(s, pattern, repl [, n]) 字符串替换
string.len(s) 计算字符串长度
string.lower(s) 转化为小写
string.upper(s) 转化为大写
string.pack(fmt, v1, v2, ···) 以二进制形式序列化字符串
string.unpack(fmt, s [, pos]) 将二进制字符串转化为字符串
string.packsize(fmt) 计算格式 fmt 占用的空间大小
string.rep(s, n [, sep]) 字符串重复 n 次
string.reverse(s) 逆转字符串
.. 字符串连接

Lua 中的字符串格式化需要使用 string.format 函数来实现。格式字符串支持以下的转义格式:

  • %c - 接受一个数字, 并将其转化为 ASCII 码表中对应的字符
  • %d, %i - 接受一个数字并将其转化为有符号的整数格式
  • %o - 接受一个数字并将其转化为八进制数格式
  • %u - 接受一个数字并将其转化为无符号整数格式
  • %x - 接受一个数字并将其转化为十六进制数格式, 使用小写字母
  • %X - 接受一个数字并将其转化为十六进制数格式, 使用大写字母
  • %e - 接受一个数字并将其转化为科学记数法格式, 使用小写字母e
  • %E - 接受一个数字并将其转化为科学记数法格式, 使用大写字母E
  • %f - 接受一个数字并将其转化为浮点数格式
  • %g(%G) - 接受一个数字并将其转化为%e(%E, 对应%G)及%f中较短的一种格式
  • %q - 接受一个字符串并将其转化为可安全被 Lua 编译器读入的格式
  • %s - 接受一个字符串并按照给定的参数格式化该字符串

为进一步细化格式, 可以在 % 号后加入格式限定符,限定符会按如下顺序解析:

  • (1) 符号: 一个 + 号表示其后的数字转义符将让正数显示正号,默认情况下只有负数显示符号
  • (2) 占位符: 一个 0, 在后面指定了字串宽度时占位用,不填时的默认占位符是空格
  • (3) 对齐标识: 在指定了字串宽度时, 默认为右对齐, 增加 - 号可以改为左对齐
  • (4) 宽度数值
  • (5) 小数位数/字串裁切: 在宽度数值后增加的小数部分 n, 若后接 f(浮点数转义符, 如 %6.3f) 则设定该浮点数的小数只保留 n 位, 若后接 s (字符串转义符, 如 %5.3s) 则设定该字符串只显示前 n 位
print(string.format("%c", 83))     -- 输出S
print(string.format("%+d", 17.0))  -- 输出+17
print(string.format("%05d", 17))   -- 输出00017
print(string.format("%o", 17))     -- 输出21
print(string.format("%u", 3))      -- 输出3
print(string.format("%x", 13))     -- 输出d
print(string.format("%X", 13))     -- 输出D
print(string.format("%e", 1000))   -- 输出1.000000e+03
print(string.format("%E", 1000))   -- 输出1.000000E+03
print(string.format("%6.3f", 13))  -- 输出13.000
print(string.format("%s", "hello"))  -- 输出 hello

day = 2; month = 1; year = 2014
print(string.format("date: %02d/%02d/%04d", day, month, year))

Lua 中的表(table)是一种很重要的数据结构,其可以看成是哈希表和数组的结合体,使用其可以方便的实现其他的数据结构,如数组、队列、栈、符号表、集合、 记录、 图、树、等等。Table 类似其他语言中的关联数组,是一种具有特殊索引方式的数组,不仅可以通过整数来索引它,还可以使用字符串或其它类型的值(除了nil)来索引它,其元素可以动态添加或删除,没有固定大小。Table 既不是“值”,也不是“变量”,而是对象,可视其为动态分配的对象。

Table 的创建是通过 “构造表达式” 完成的,最简单的构造表达式就是 {}。

-- 定义空的表
t1 = {}

-- 指定初始元素
t2 = {1, 2, 3}
t3 = {a=100, b=200}

如果要实现原始的数组,则初始化表时可不指定 “key”,而且始终使用数字索引访问元素(注意,Lua 中的索引下标默认从 1 开始)。

-- 定义数组
arr = {"a", "b", "c"}

-- 遍历数组可以用泛型 for
for i, v in ipairs(arr) do
    print(string.format("arr[%d] = %s", i, v))
end

Lua 将 table 中所有没有指定非数字索引的元素视为数组元素,相应值需要通过数字下标访问:

> t = {1, a=11, 2, b=22, 3}
> t[1]
1
> t[2]
2
> t[3]
3
> t['a']
11
> t['b']
22

Table 的对象方法:

  • table.concat(list [, sep [, i [, j]]]) 拼接数组元素
> t = {1, a=11, 2, b=22, 3}
> table.concat(t)
123
> table.concat(t, ',')
1,2,3
> table.concat(t, ',', 2)
2,3
> table.concat(t, ',', 2, 2)
2
  • table.insert(list, [pos,] value) 向数组中插入元素,默认在末尾插入

  • table.remove(list [, pos]) 返回指定位置元素并从数组删除该元素,默认删除末尾元素

> t = {1, a=11, 2, b=22, 3}
> table.insert(t, 4)  -- 默认在末尾插入
> inspect(t)
{ 1, 2, 3, 4,
  a = 11,
  b = 22
}
> table.insert(t, 2, 'x')  -- 在第 2 个位置插入
> inspect(t)
{ 1, "x", 2, 3, 4,
  a = 11,
  b = 22
}
> table.remove(t, 2)  -- 移除第 2 个位置元素
x
> inspect(t)
{ 1, 2, 3, 4,
  a = 11,
  b = 22
}

注: 以上的 inspect 库为第三方库,用于将变量值以可读的形式的输出。

  • table.move(a1, f, e, t [,a2]) 将表中指定区间元素复制到其他表。如果 a2 没有指定则在原 table 中移动,否则会将元素移动到 a2 中
--[[
move 方法将表 "a1" 中从整数索引 "f" 到整数索引 "e" 之间(源区间)的元素
复制到表 "a2" 中整数索引"t"及之后的位置(目标区间),
表 "a2" 默认为 "a1",目标区间与源区间可以重叠
--]]

> inspect = require("inspect")
> t = {1, a=11, 2, b=22, 3}
> inspect(table.move(t, 2, 3, 1, {}))
{ 2, 3 }
> inspect(table.move(t, 2, 3, 1))
{ 2, 3, 3,
  a = 11,
  b = 22
}
  • table.pack(···) 获取一个索引从 1 开始的参数表 table,并会对这个 table 预定义一个字段 n,表示该表的长度
-- pack 方法多用于可变参数函数中
function table_pack(param, ...)
    local arg = table.pack(...)
    print("this arg table length is", arg.n)
    for i = 1, arg.n do
        print(i, arg[i])
    end
end

table_pack("test", "param1", "param2", "param3")
  • table.sort(list [, comp]) 对数组排序,默认为升序,comp 为排序比较函数
> t = {4, 1, 2, 3, 5}
> table.sort(t)  -- 升序排列
> inspect(t)
{ 1, 2, 3, 4, 5 }
> table.sort(t, function(a, b) return (a > b) end)  -- 降序排列
> inspect(t)
{ 5, 4, 3, 2, 1 }
  • table.unpack(list [, i [, j]]) 数组解包,即返回数组元素
> t = {1, a=11, 2, b=22, 3}
> print(table.unpack(t))
1	2	3
> a, b, c = table.unpack(t)
> print(a, b, c)
1	2	3

函数

函数一般用于完成指定的任务,并在需要的时候返回值。在 Lua 中函数也被视为是一种数据类型,函数名实际上是一个变量。函数定义语法:

function func_name(arguments-list)
   statements-list;
end;

需要返回值时使用 return 语句,如果函数中没有 return 语句,在函数结束时会默认加上 return 语句。需要注意的是,return 和 break 一样,只能出现在 block 的结尾一句(在 end 之前,或 else 前,或 until 前)。如果一定要在中间返回,则可以使用 do return end 语句。

函数参数列表为空时,必须使用 () 表明是函数调用,但如果函数只有一个参数并且这个参数是字符串或者表构造的时候,则 () 可有可无。如:

print"Hello world"
print "Hello World"


local function foo(t)
    for k, v in pairs(t) do
        print(k, v)
    end
end

foo{a=1, b=2}

函数的声明可以是匿名的:

-- 命名函数
local function foo()
    print("hello world")
end

-- 等价于:

-- 匿名函数
local foo = function ()
    print("hello world")
end

函数可以返回多个值,对函数返回值的接收同赋值语句,多余的会被舍弃,不足则补 nil。也可以用 _ 忽略某个位置的值。如:

local function foo() return 1, 2, 3 end

a, b = foo()
print(a, b)  --> 1 2

a, b, c = foo()
print(a, b, c)  --> 1 2 3

a, b, c, d = foo()
print(a, b, c, d)  --> 1 2 3 nil

a, _, c = foo()
print(a, c)  --> 1 3

函数可以在参数列表中使用三点(…) 表示函数有可变的参数,如:

local function foo(...)
    arg = {...}
    print("arg len:", #arg)
    for i, v in ipairs(arg) do
        print(i, v)
    end
end

foo('a', 'b', 'c')
foo(1, 2)

-- 也可以现有固定参数
function foo(a, b, ...) end

Lua 的函数参数是和位置相关的,调用时实参会按顺序依次传给形参。但如果可以在传参时指定参数名字,则更加一目了然。然而 Lua 并不支持类似 foo(a=1, b=2) 这样的命名参数传参方式。但可以通过 表 来实现命名参数:

local function foo(kwarg)
    print(kwarg.a, kwarg.b)
end

foo{a=1, b=2}  --> 1 2

Lua 中的函数可以定义在另一个函数中。当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部的函数的局部变量,该特性称为词法定界。闭包 即外部函数的局部变量,也就是说闭包指的是值而不是指函数,函数仅仅是闭包的一个原型声明。而通常,在不会导致混淆的情况下大多使用函数代指闭包。使用闭包可以实现很有用的功能,如(缓存函数执行结果):

local function cache(func)
    local results = {}

    function _func(s)
        local key = string.format("k_%s_%s", func, s)
        local cached_result = results[key]
        if cached_result == nil then
            print(string.format("call function %s, and cache result, key: %s", func, key))
            local result = func(s)
            results[key] = result
            return result
        else
            print(string.format("get result from cache, key: %s", key))
            return results[key]
        end
    end

    return _func
end

local f1 = cache(function (s)
    return "hello " .. s
end)

local f2 = cache(function (s)
    return "this is " .. s
end)

print(f1("world"))
print(f1("world"))
print(f1("world"))
print(f2("apple"))
print(f2("apple"))
print(f2("apple"))

函数可以直接定义在表中,作为标的域:

funcs = {
    foo = function (x,y) return x + y end,
    goo = function (x,y) return x - y end
}

Lua 还提供了另一种写法:

funcs = {}

function funcs.foo (x,y)
   return x + y
end

function funcs.goo (x,y)
   return x - y
end

高级特性

元表与元方法

元表(metatable)用于改变表的行为,而元方法(metamethod)则是定义在元表中的用于改变表具体行为的方法。可选的元方法有:

__add(a, b)     -- 加法
__sub(a, b)     -- 减法
__mul(a, b)     -- 乘法
__div(a, b)     -- 除法
__mod(a, b)     -- 取模
__pow(a, b)     -- 乘幂
__unm(a)        -- 相反数
__concat(a, b)  -- 连接
__len(a)        -- 长度
__eq(a, b)      -- 相等
__lt(a, b)      -- 小于
__le(a, b)      -- 小于等于
__index(a, b)   -- 索引查询
__newindex(a, b, c)  -- 索引更新
__call(a, ...)  -- 执行方法调用
__tostring(a)   -- 字符串输出
__metatable     -- 保护元表

所有的表都可以设置元表,但 新创建的表默认没有元表。Lua 只会在元表的域中查找元方法,而不会在自己的域中查找元方法。使用 setmetatable 方法可以设置元表,getmetatable 方法可以获取元表。

print(getmetatable({}))  --> nil

local t = {
    a = 1,
    __index = function (_t, name)
        return "hello"
    end
}

print(t.a)  --> 1
print(t.b)  --> nil

setmetatable(t, t)
print(t.b)  --> hello
print(getmetatable(t))  --> t

任何一个表都可以是其他一个表的 metatable,一组相关的表可以共享一个 metatable (描述他们共同的行为)。一个表也可以是自身的 metatable(描述其私有行为)。

二元操作符的元方法,如 __add,选择 metamethod 的原则是,如果第一个参数存在带有 __add 域的 metatable,则使用它作为 metamethod,和第二个参数无关;否则第二个参数存在带有 __add 域的 metatable,则使用它作为 metamethod, 否则报错。

使用 getmetatable 可以轻易的获取元表,使用 setmetatable 也可以很容易的修改元表,这在某些时候可能是危险的。在设置和获取元表时,元表会用到 __metatable 字段。如果想保护元表不被修改,可以设置该字段的值,此后,getmetatable 就会返回设置的值,而 setmetatable 则会引发一个错误。如:

local mt = {
    __index = function (t, name)
        return "None"
    end
}
print(string.format("mt id: %s", mt))

local tbl = { a = 1 }
print(string.format("tbl id: %s", tbl))
print(tbl.a)  --> 1
print(tbl.b)  --> nil

setmetatable(tbl, mt)
print(string.format("tbl metatable: %s", getmetatable(tbl)))
print(tbl.b, tbl.c)  --> None None

mt.__metatable = "error, cannot get the metatable"
print(getmetatable(tbl))  --> error, cannot get the metatable
print(tbl.d, tbl.e)  --> None, None
setmetatable(tbl)  --> error will be

当访问表中不存在的字段时,其元表中的 __index 元方法会被调用,并返回该方法返回的值,该方法可以是一个函数或者表。当对表中不存在的字段赋值时,其元表中的 __newindex 元方法会被调用,该方法同样可以为一个函数或表。示例:

local data = {}
data.prototype = { a = 1 }

local mt = {}
mt.__index = function (t, name)
    return t.prototype[name]
end
mt.__newindex = function (t, name, value)
    t.prototype[name] = value
end

setmetatable(data, mt)

print(data.a, data.b)  --> 1 nil
data.c = 3
print(data.c, data.d)  --> 3 nil

一个表被设置元表后,其行为可能会发生改变,但有时候可能不希望使用元表的行为,此时可以 rawget 忽略元表。如:

mt = {
    __index = function (t, name)
        return "xxx"
    end
}

t = setmetatable({}, mt)
print(t.a, rawget(t, a))  --> xxx nil

模块与包

模块可以认为是一些程序集。从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。模块的内容通常放到一个表中,其可由变量、函数等已知元素组成。因此创建一个模块即创建一个表,然后把需要导出的常量、函数等放入其中,最后将表返回。创建模块的一般格式如下:

-- mod.lua

local _M = { _VERSION = "0.0.1 "}

_M.a = 1

function _M.foo()
   print("hello world")
end

function _M.bar(str)
   print(str)
end

return _M

一般只把一个模块写到一个文件中,所以通常将一个文件视为一个模块。模块的导入使用 require 函数:

local mod = require("lua")
print(mod.a)
mod.foo()
mod.bar()

require 是一个高级的函数,其底层使用 loadfile 函数。loadfile 从文件中读入代码并编译代码成中间码然后返回编译后的 chunk 作为一个函数。,其功能类似于:

local mod = function()
    -- 读入文件内容作为函数体
end

所以模块载入实际上是将模块代码作为函数然后调用。此外,dofile 也能载入 lua 文件,其底层仍然是调用 loadfile 函数,其功能类似于:

function dofile (filename)
   local func = assert(loadfile(filename))
   return func()
end

requiredofile 几乎完成同样的功能,但有两点不同:

    1. require 会自动在 package.path 所指定的目录中搜索并加载文件,不必指定详细的文件路径
    1. require 会判断是否文件已经加载避免重复加载同一文件

因此 require 功能更强大,调用它只需要指定文件名(虚文件名,无需详细路径,也无需文件后缀),系统会自动查找相应文件并载入。模块的查找路径可以通过 LUA_PATH 环境变量来设置,如:

export LUA_PATH="?.lua;?/init.lua;/usr/local/share/lua/5.3/?.lua;/usr/local/share/lua/5.3/?/init.lua

LUA_PATH 环境变量中不同的搜索路径用分号隔开,系统会用 虚文件名 替换掉路径中的 ? 然后尝试访问文件。

与 loadfile 有同样功能的函数还有一个 loadstring。loadstring (5.2 之后被替换成了 load 函数) 不从文件中读取内容,而是直接从字符串中读入代码并编译成函数:

> f = load("local a = 10; return a + 20")
> f()
30

有时候可能需要把许多模块组合在一个,在其它语言中通常把一些模块组合起来作为一个程序包。使用 require 函数时,系统会尝试查目录中的 init.lua 文件并载入,所以 Lua 中,可以将包含 init.lua 文件的目录作为一个包。

Lua 也可以调用由 C/C++ 编译而成的 .so 文件,标准库 package 模块中的 loadlib 函数即用于加载动态库:

-- 函数 loadlib 原型定义: package.loadlib (libname, funcname)

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

也可以设置 LUA_CPATH 环境变量,然后用 require 函数载入动态库,这样系统会自动到相应目录搜索动态库文件。

环境

Lua 使用一个全局的 _G 变量来保存整个程序运行过程中的所有全局变量(其中 _G._G 等 于 _G), 实际上 _G 就是一个 环境(environment),只不过它是一个全局的环境。全局变量不需要声明,没被 local 修饰的变量都是全局变量。这一点很容易引入 Bug,例如拼写错误则会引入新的全局变量。但是在必要的时候可以改变这种行为,通过设置 _G 的 metatables:

local _declared_names = {}

function declare(name, initval)
   rawset(_G, name, initval)
   _declared_names[name] = true
end

setmetatable(_G, {
    __newindex = function (t, n, v)
        if not _declared_names[n] then
            error(string.format("attempt to write to undeclared variable '%s'", n), 2)
        else
            rawset(t,n,v)
        end
    end,
    __index = function (_, n)
        if not _declared_names[n] then
            error(string.format("attempt to read undeclared variable '%s'", n), 2)
        else
            return nil
        end
    end,
})

Lua 支持改变函数的上下文运行环境,这通过 setfenv(f, table) 函数来完成,其中 table 是新的环境,f 表示需要被改变环境的函数,如果 f 是数字,则将其视为堆栈层级(Stack Level),从而指明函数(1 为当前函数,2 为上一级函数)。同时有函数 getfenv(f) 来获取当前环境。示例:

a = 1
print(_G)         --> table: 0x02c1e9d8
print(getfenv())  --> table: 0x02c1e9d8


function foobar(env)
    return setfenv(function() print(a) end, env)
end

foobar({a=11, print=print})()  --> 11


setfenv(1, {g=_G})
g.print(g.getfenv())  --> table: 0x02c25678
g.print(g.a)          --> 1

从 Lua 5.2 开始,所有对全局变量 var 的访问都会在语法上翻译为 _ENV.var。而 _ENV 本身被认为是处于当前块外的一个局部变量。(于是只要你自己定义一个名为 _ENV 的变量,就自动成为了其后代码所处的 环境(enviroment)。其优点是原先虚无缥缈只能通过 setfenv、getfenv 控制的所谓 环境 被实体化为一个变量 _ENV。所以自 5.2 版本开始,setfenv、getfenv 函数已不再可用。以下两段代码是等价的:

-- Lua 5.1
function foobar()
  setfenv(1, {})
  -- code here
end

-- Lua 5.2
function foobar()
  local _ENV = {}
  -- code here
end

错误处理

Lua 中的错误类型包括语法错误、运行错误,语法错误发生在编译阶段,运行错误发生在运行阶段。assert 和 error 函数可以在运行时主动抛出异常。

assert(v [, message])
- v 为布尔表达式
- message 为 v 为 false 时抛出的错误信息


error(message [, level])
- message 为错误信息
- level 为 1 (默认) 时会输出 error 位置(文件+行号),为 2 时输出调用的函数,
  为 0 则输出行号和调用函数

Lua 中使用 pcall 和 xpcall 来捕获函数调用的异常。其原型定义如下:

pcall(f [, arg1, ···])

xpcall(f, err)
xpcall(f, msgh [, arg1, ···])  -- 5.2 之后

函数 pcall 调用函数成功时返回 true 以及函数的返回值,如果出错则返回 false 和错误信息;函数 xpcall 可以传递一个错误处理函数,在发生错误是该函数被调用,在 Lua 5.2 版本之前 xpcall 函数不支持给函数传递参数,函数 xpcall 调用函数成功时返回 true 和函数的返回值,否则返回 false 和错误处理函数的返回值。示例:

function calc(a, b)
    a = a or 1
    b = b or 2
    return a + b, a - b
end

function foo()
   error("this is error", 0)
end

function handle_error(err)
   print("error message:", err)
   print(debug.traceback())
   return "test"
end

print(pcall(calc, 11, 22))
print(pcall(foo))
print(xpcall(calc, handle_error))
print(xpcall(foo, handle_error))

--[[
输出结果:
true	33	-11
false	this is error
true	3	-1
error message:	this is error
stack traceback:
	handle_error.lua:13: in function 'handle_error'
	[C]: in function 'error'
	handle_error.lua:8: in function 'foo'
	[C]: in function 'xpcall'
	handle_error.lua:20: in main chunk
	[C]: in ?
false	test
--]]

面向对象

Lua 没有提供原生的面向对象编程支持,但可以使用“无所不能”的表(Table)来模拟面向对象编程。表有面向对象编程中的对象的概念,拥有两个不同值的对象(table)是不同的对象,一个对象在不同的时期可以有不同的值。且像对象一样,表也有状态(成员变量),有行为(成员方法)。在面向对象编程中,通常有“类”的概念,类是创建对象的模板,对象则是类的实例。Lua 不存在类的概念,每个对象定义他自己的行为并拥有自己的形状。但 Lua 可基于原型(prototype)来模拟类的概念。所以,Lua 总的对象没有类,但每个对象都有一个 prototype (原型),当调用不属于对象的某些操作时,会最先会到 prototype 中查找这些操作。Lua 中的面向对象编程,即是创建一个对象作为其它对象的原型(可以理解为,原型对象为类,其它对象为类的 instance)。

例如,有两个对象 a 和 b,如果让 b 作为 a 的 prototype,则可以:

setmetatable(a, {__index = b})

这样,访问 a 中任何不存在的属性时,都会尝试到 b 中查找,这里的 b 就相当于是类,而 a 则想相当于是类 b 的实例。

为了让一个 prototype 看起来更像是类,一般做如下的定义(类定义的一般方法):

local Person = {
    height = 0,
    weight = 0,
    age = 0,
    sex = 'male',

    _sex_set = { male = true, female = true },
}

function Person:new(obj)
    obj = obj or {}
    setmetatable(obj, self)
    self.__index = self

    obj:check_sex()

    return obj
end

function Person:check_sex()
    assert(self._sex_set[self.sex], 'sex must be male or female')
end

上例中使用了冒号(:)运算符访问对象成员,其与点(.)运算符的区别是,冒号运行符在访问成员是会自动加上 self 参数,self 指向调用者,如 Person:new 则 self 指向 Person,object:check_sex 则 self 指向 object。

如果再利用 继承 的思想,就实现了 prototypes (原型链)。Lua 中继承是通过将父类对象作为原型来实现的,例如从 Person 派生出 Student 和 Programer:

local Student = Person:new{grade=1}

function Student:learn()
    print("learning...")
end


local Programer = Person:new{company='google'}

function Programer:work()
    print("working...")
end


Student:new():learn()
Programer:new():work()

由于 Lua 没有原生的面向对象编程支持,以上所实现的类也只是简单的模拟,所以创建类的方式可以有很多种。例如要实现 多重继承,用上边的方式创建类则不能实现。Lua 中的多重继承可以通过一个工厂函数来实现:

local function class(name, bases, object)
    local cls = object or {}

    if bases then
        local function getattr(key)
            for i=1, #bases do
                local val = bases[i][key]
                if val then return val end
            end
        end

        setmetatable(cls, {__index = function (_, key)
           return getattr(key)
        end})
    end

    cls.__name = name
    cls.__index = cls

    function cls:new(...)
       instance = setmetatable({}, self)
       instance.__class = self

       if instance.init then
           instance:init(...)
       end

       return instance
    end

    function cls:_merge(object)
        if object then
            for key, val in pairs(object) do self[key] = val end
        end
    end

    return cls
end

local Person = class("Person", nil, {height=0, weight=0, age=0, sex='male'})

function Person:init(obj)
    self:_merge(obj)
    self._sex_set = {male=true, female=true}
    self:check_sex()
end

function Person:check_sex()
    assert(self._sex_set[self.sex], 'sex must be male or female')
end


local Student = class("Student", {Person}, {grade=1})

function Student:learn()
    print("learning...")
end

function Student:show_info()
    print(string.format("height=%s, weight=%s, age=%s, sex=%s, grade=%s",
        self.height, self.weight, self.age, self.sex, self.grade
    ))
end


local Programer = class("Programer", {Person}, {company='google'})

function Programer:work()
    print("working...")
end

function Programer:show_info()
    print(string.format("height=%s, weight=%s, age=%s, sex=%s, company=%s",
        self.height, self.weight, self.age, self.sex, self.company
    ))
end


local stu = Student:new{height=1.2, weight=70, age=7}
stu:show_info()
stu:learn()
local pgm = Programer:new{height=1.68, weight=98, age=25, sex="female"}
pgm:show_info()
pgm:work()


local Ac = class("Ac", nil, {a=1})
local Bc = class("Bc", nil, {b=2})
local ABc = class("ABc", {Ac, Bc}, {c=3})
local obj = ABc:new{cc=33}
print(obj.a, obj.b, obj.c, obj.cc) --> 1 2 3 nil

以下为 Lua 面向对象编程的一些建议:

  • 类名首字母必须大写
  • 将类的 __index 元方法指向自身
  • 必要时使用 __gc 元方法定义销毁操作
  • 实例属性使用点(.)语法进行声明和访问
  • 类方法和实例方法声明和调用使用冒号(:)语法声明(能默认提供 self 参数)
  • 类的构造方法命名为 new 或者 create

编码规范

编码规范旨在统一编码标准,使代码通俗易懂,提高开发效率,易于维护。以下列举一些编码建议:

命名规范:

  • Lua 文件的命名使用小写字母、下划线,但尽量短
  • 类名、变量名尽可能使用有意义的英文,类名使用帕斯卡命名法(单词首字符大写),变量名可根据习惯使用骆驼式命名法、下划线法命名法等
  • 常量、消息号定义时用大写,单词间用 _ 分割,如: KIND_PET_FOOD
  • 单词前或尾加 _ 表示私有的变量或者临时变量

分隔和缩进:

  • 建议使用 4 个空格来缩进代码块(使用 Tab 还是 空格 来缩进代码见仁见智)
  • 在函数之间,代码逻辑段落小节之间使用使用空行分割
  • 在运算符之间,逗号之后空格符
  • 不建议在一行中写多条语句,一行建议不要超过 80 个字符
  • 使用小括号强制确定运算优先级,同时小括号内的内容可自动被系统视为一行

代码编排:

  • 必要时加上文件头注释,如作者、创建日期、版权等
  • 短注释使用 --,长注释使用 --[[ --]]
  • 文件行尽量不要太长(建议不超过 80,但绝不运行超过 100)
  • 文件尽量使用 UTF8 格式

编码建议:

  • 在一切能使用 local 的地方使用 local 关键字修饰变量

  • 必要时使用 do .. end 来明确限定局部变量的作用域

  • 直接判断真假值:

-- 不推荐
if a ~= nil and b == false then
    -- ...
end

-- 推荐
if a and not b then
    -- ...
end
  • 实现表的拷贝 u = {unpack(t)}

  • 判断表是否为空,用 #t == 0 并不能判断表是否为空,因为 # 预算符会忽略所有不连续的数字下标和非数字下标。正确的方法是:

if next(t) == nil then
    -- 表为空
    -- ...
end
  • 更快的想数组中插入值:
-- 更慢,不推荐
table.insert(t, value)

-- 更快,推荐,避免了高层函数调用的开销
t[#t+1] = value
  • assert 函数开销不小,应尽量避免使用

  • 尽量减少表中的成员是另一个表的引用,容易引发内存泄漏

  • 在合适的位置记录合适的日志

  • 不要优化,还是不要优化,不要过早优化

参考资料

Top