内容简介:Lua 是一种轻量小巧的脚本语言,用标准 C 语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。其具有如下特性:Lua 应用场景大致有以下几类: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
编码规范
编码规范旨在统一编码标准,使代码通俗易懂,提高开发效率,易于维护。以下列举一些编码建议:
命名规范:
_ _
分隔和缩进:
- 建议使用 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 技巧
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- MongoDB初学笔记(1)
- 初学Docker容器网络不得不看的学习笔记
- 初学Python的学习笔记11----使用元类、错误处理和调试
- nginx初学入门
- webpack 初学指南
- 初学NodeJS(一)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web Data Mining
Bing Liu / Springer / 2011-6-26 / CAD 61.50
Web mining aims to discover useful information and knowledge from Web hyperlinks, page contents, and usage data. Although Web mining uses many conventional data mining techniques, it is not purely an ......一起来看看 《Web Data Mining》 这本书的介绍吧!