string
Lua 的字符串是不可改变的值,不能像在 C 语言中那样直接修改字符串的某个字符,而是根据修改要求来创建一个新的字符串。Lua 也不能通过下标来访问字符串的某个字符(但是可以通过 string.byte 间接访问)。
在 Lua 实现中,Lua 字符串一般都会经历一个“内化”(intern)的过程,即两个完全一样的 Lua 字符串在 Lua 虚拟机中只会存储一份。每一个 Lua 字符串在创建时都会插入到 Lua 虚拟机内部的一个全局的哈希表中。 这意味着:
- 创建相同的 Lua 字符串并不会引入新的动态内存分配操作,所以相对便宜(但仍有全局哈希表查询的开销)。
- 内容相同的 Lua 字符串不会占用多份存储空间。
- 已经创建好的 Lua 字符串之间进行相等性比较时是 O(1) 时间度的开销,而不是通常见到的 O(n)(TODO:长字符串和短字符串还有区别) 。
- 连接两个字符串,可以使用操作符“..”(两个点)。如果其任意一个操作数是数字的话,Lua 会将这个数字转换成字符串(其他类型则不会转换为字符串)。注意,连接操作符只会创建一个新字符串,而不会改变原操作数。也可以使用 string 库函数 string.format 连接字符串。
print("Hello " .. "World") -->打印 Hello World
print(0 .. 1) -->打印 01
str1 = string.format("%s-%s","hello","world")
print(str1) -->打印 hello-world
- 基于 4 中说明的字符串拼接会创建一个新的字符串,因此在循环中进行字符串拼接是一个性能很差的操作,在这种情况下,推荐使用 table 和 table.concat() 来进行很多字符串的拼接:
local pieces = {}
for i, elem in ipairs(my_list) do
pieces[i] = my_process(elem)
end
local res = table.concat(pieces)
- 应当总是使用 # 运算符来获取 Lua 字符串的长度。不要使用 string.len 来完成同样的工作。
table
table 通常实现为一个哈希表、一个数组、或者两者的混合。具体的实现为何种形式,动态依赖于具体的 table 的键分布特点。
- ipairs 和 pairs 的区别在于 ipairs 用来遍历数组,pairs 用来遍历 table 元素。因此在性能敏感的场景,应当合理安排数据结构,避免对哈希表进行遍历。毕竟 hash 结构并不是一个适合遍历操作的数据结构。
- 在初始化一个数组的时候,若不显式地用键值对方式赋值,则会默认用数字作为下标,从 1 开始。由于在 Lua 内部实际采用哈希表和数组分别保存键值对、普通值,所以不推荐混合使用这两种赋值方式。
- 如果
s = { 1, 2, 3, 4, 5, 6 }
,你令s[4] = nil
,#s
会“匪夷所思”地变成 3。 - 对于常规的数组,里面从 1 到 n 放着一些非空的值的时候,它的长度就精确的为 n,即最后一个值的下标。如果数组有一个“空洞”(就是说,nil 值被夹在非空值之间),那么
#t
可能是指向任何一个是 nil 值的前一个位置的下标(就是说,任何一个 nil 值都有可能被当成数组的结束)。这也就说明对于有“空洞”的情况,table 的长度存在一定的不可确定性。 - LuaJIT 2.1 新增加的 table.new 和 table.clear 函数是非常有用的。前者主要用来预分配Lua table 空间,后者主要用来高效的释放 table 空间,并且它们都是可以被 JIT 编译的。
Lua Function
使用函数的好处:
- 降低程序的复杂性:把函数作为一个独立的模块,写完函数后,只关心它的功能,而不
再考虑函数里面的细节。 - 增加程序的可读性:当我们调用 math.max() 函数时,很明显函数是用于求最大值的,
实现细节就不关心了。 - 避免重复代码:当程序中有相同的代码部分时,可以把这部分写成一个函数,通过调用
函数来实现这部分代码的功能,节约空间,减少代码长度。 - 隐含局部变量:在函数中使用局部变量,变量的作用范围不会超出函数,这样它就不会
给外界带来干扰。
因此请大家适当定义函数来表达自己的逻辑。
function function_name (arc) -- arc 表示参数列表,函数的参数列表可以为空
-- body
end
上面的语法定义了一个全局函数,名为 function_name 。全局函数本质上就是函数类型的值赋给了一个全局变量,由于全局变量一般会污染全局名字空间,同时也有性能损耗(即查询全局环境表的开销),因此我们应当尽量使用“局部函数”,其记法是类似的,只是开头加上 local 修饰符:
local function function_name (arc)
-- body
end
由于函数定义等价于变量赋值,我们也可以把函数名替换为某个 Lua 表的某个字段,例如:
function foo.bar(a, b, c)
-- body ...
end
对于此种形式的函数定义,不能再使用 local 修饰符了,因为不存在定义新的局部变量了。
Lua 的函数参数,对于大部分数据类型都是值传递,只有 table 类型是引用传递,因此使用 Lua 函数时需要注意,如果参数是 table 类型,函数内对 table 的修改会直接对函数的实际参数生效,而其他类型的修改则不会传递到函数以外。
用全局变量来代替函数参数的不好编程习惯应该被抵制,良好的编程习惯应该是减少全局变量的使用。
Lua 的设计有一点很奇怪,在一个 block 中的变量,如果之前没有定义过,那么认为它是一个全局变量,而不是这个 block 的局部变量。这一点和别的语言不同。容易造成不小心覆盖了全局同名变量的错误。
Lua 模块
从 Lua 5.1 语言添加了对模块和包的支持。可以使用内建函数 require() 来加载和缓存模块。这个调用会返回一个由模块函数组成的 table,并且还会定义一个包含该 table 的全局变量。
在 Lua 中创建一个模块最简单的方法是:创建一个 table,并将所有需要导出的函数放入其中,最后返回这个 table 就可以了。相当于将导出的函数作为 table 的一个字段,在 Lua 中函数是第一类值,提供了天然的优势。
对于需要导出给外部使用的公共模块,处于安全考虑,是要避免全局变量的出现。
旧式的模块定义方式是通过 module("filename"[,package.seeall])* 来显式声明一个包,现在官方不推荐再使用这种方式。这种方式将会返回一个由 filename 模块函数组成的 table ,并且还会定义一个包含该 table 的全局变量。为什么这种写法现在不被提倡,官方给出了两点原因:
- package.seeall 这种方式破坏了模块的高内聚,原本引入 "filename" 模块只想调用它的foobar() 函数,但是它却可以读写全局属性,例如 "filename.os" 。
- module 函数压栈操作引发的副作用,污染了全局环境变量。例如 module("filename")会创建一个 filename 的 table ,并将这个 table 注入全局环境变量中,这样使得没有引用它的文件也能调用 filename 模块的方法。
看一个官方推荐的模块例子,文件名为 account.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
引用代码示例:
local account = require("account")
local a = account:new()
a:deposit(100)
local b = account:new()
b:deposit(50)
print(a.balance) --> output: 100
print(b.balance) --> output: 50
时间日期函数
不推荐使用 Lua 的标准时间函数,因为这些函数通常会引发不止一
个昂贵的系统调用,同时无法为 LuaJIT JIT 编译,对性能造成较大影响。可以自己用 C 语言实现一个版本来做这些工作。
虚变量
当一个方法返回多个值时,有些返回值有时候用不到,要是声明很多变量来一一接收,显然不太合适(不是不能)。Lua 提供了一个虚变量(dummy variable),以单个下划线(“_”)来命名,用它来丢弃不需要的数值,仅仅起到占位的作用。
for _,v in ipairs(t) do
print(v)
end
其他
- nil ,将nil 赋予给一个全局变量就等同于删除了这个全局变量。
和 C 语言的区别
- 假值,lua 中只有 false 和 nil 是假值,int 0 和空字符串都是逻辑真。
- 在 lua 中整数运算的结果并不是向下取整:
print(5 / 10) -->打印 0.5。
LuaJIT
- LuaJIT 是一个Lua 的解释器与即时编译器,速度比原生 Lua 解释器快,但是LuaJIT 只对 lua 的一个子集指令有优化作用,所以为了提升性能,需要注意尽量使用能被 LuaJIT 优化的指令。
- LuaJIT 独有的 table.new 来恰当地初始化表的空间,以避免该表的动态生长。借此特点可以优化 string 中的第 5 点建议。
FFI
FFI 库,是 LuaJIT 中最重要的一个扩展库。它允许从纯 Lua 代码调用外部 C 函数,使用C 数据结构。有了它,就不用再像 Lua 标准 math 库一样,编写 Lua 扩展库。把开发者从开发 Lua 扩展 C 库(语言/功能绑定库)的繁重工作中释放出来。学习完本小节对开发纯ffi 的库是有帮助的,像 lru-resty-lrucache 中的 pureffi.lua ,这个纯 ffi 库非常高效地
完成了 lru 缓存策略。
静态代码检查
作为开发人员,在日常编码中,难免会范一些低级错误,比如少个括号,少个逗号,使用了未定义变量等等,我们往往会使用编辑器的 lint 插件来检测此类错误。我们可以使用 luacheck 这款静态代码检测工具来帮助我们检查。
参考文献
[1]:OpenResty最佳实践