内容简介: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(一)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Dive Into Python
Mark Pilgrim / Apress / 2004-11-5 / GBP 31.49
Python is a new and innovative scripting language. It is set to replace Perl as the programming language of choice for shell scripters, and for serious application developers who want a feature-rich, ......一起来看看 《Dive Into Python》 这本书的介绍吧!