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。在逻辑判断时,只有 false
和 nil
为假,其它值全为真。
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
require
和 dofile
几乎完成同样的功能,但有两点不同:
- 1. require 会自动在 package.path 所指定的目录中搜索并加载文件,不必指定详细的文件路径
- 2. 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 函数开销不小,应尽量避免使用
尽量减少表中的成员是另一个表的引用,容易引发内存泄漏
在合适的位置记录合适的日志
不要优化,还是不要优化,不要过早优化
参考资料
- https://www.lua.org/manual/5.3/
- https://wizardforcel.gitbooks.io/lua-doc/content/
- https://www.kancloud.cn/thinkphp/lua-guide/43808
- http://www.runoob.com/lua/lua-tutorial.html-
- http://lua-users.org/wiki/LuaStyleGuide
- https://www.lua.org/pil/contents.html
- https://moonbingbing.gitbooks.io/openresty-best-practices/content/
- https://www.codingnow.com/2000/download/lua_manual.html
- 高性能 Lua 技巧
本文使用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,转载请注明出处