Lua 初学者相关技巧

string

Lua 的字符串是不可改变的值,不能像在 C 语言中那样直接修改字符串的某个字符,而是根据修改要求来创建一个新的字符串。Lua 也不能通过下标来访问字符串的某个字符(但是可以通过 string.byte 间接访问)。

在 Lua 实现中,Lua 字符串一般都会经历一个“内化”(intern)的过程,即两个完全一样的 Lua 字符串在 Lua 虚拟机中只会存储一份。每一个 Lua 字符串在创建时都会插入到 Lua 虚拟机内部的一个全局的哈希表中。 这意味着:

  1. 创建相同的 Lua 字符串并不会引入新的动态内存分配操作,所以相对便宜(但仍有全局哈希表查询的开销)。
  2. 内容相同的 Lua 字符串不会占用多份存储空间。
  3. 已经创建好的 Lua 字符串之间进行相等性比较时是 O(1) 时间度的开销,而不是通常见到的 O(n)(TODO:长字符串和短字符串还有区别) 。
  4. 连接两个字符串,可以使用操作符“..”(两个点)。如果其任意一个操作数是数字的话,Lua 会将这个数字转换成字符串(其他类型则不会转换为字符串)。注意,连接操作符只会创建一个新字符串,而不会改变原操作数。也可以使用 string 库函数 string.format 连接字符串。
print("Hello " .. "World")    -->打印 Hello World
print(0 .. 1)     -->打印 01
str1 = string.format("%s-%s","hello","world")
print(str1)    -->打印 hello-world
  1. 基于 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)
  1. 应当总是使用 # 运算符来获取 Lua 字符串的长度。不要使用 string.len 来完成同样的工作。

table

table 通常实现为一个哈希表、一个数组、或者两者的混合。具体的实现为何种形式,动态依赖于具体的 table 的键分布特点。

  1. ipairs 和 pairs 的区别在于 ipairs 用来遍历数组,pairs 用来遍历 table 元素。因此在性能敏感的场景,应当合理安排数据结构,避免对哈希表进行遍历。毕竟 hash 结构并不是一个适合遍历操作的数据结构。
  2. 在初始化一个数组的时候,若不显式地用键值对方式赋值,则会默认用数字作为下标,从 1 开始。由于在 Lua 内部实际采用哈希表和数组分别保存键值对、普通值,所以不推荐混合使用这两种赋值方式。
  3. 如果 s = { 1, 2, 3, 4, 5, 6 },你令 s[4] = nil#s 会“匪夷所思”地变成 3。
  4. 对于常规的数组,里面从 1 到 n 放着一些非空的值的时候,它的长度就精确的为 n,即最后一个值的下标。如果数组有一个“空洞”(就是说,nil 值被夹在非空值之间),那么 #t 可能是指向任何一个是 nil 值的前一个位置的下标(就是说,任何一个 nil 值都有可能被当成数组的结束)。这也就说明对于有“空洞”的情况,table 的长度存在一定的不可确定性。
  5. LuaJIT 2.1 新增加的 table.new 和 table.clear 函数是非常有用的。前者主要用来预分配Lua table 空间,后者主要用来高效的释放 table 空间,并且它们都是可以被 JIT 编译的。

Lua Function

使用函数的好处:

  1. 降低程序的复杂性:把函数作为一个独立的模块,写完函数后,只关心它的功能,而不
    再考虑函数里面的细节。
  2. 增加程序的可读性:当我们调用 math.max() 函数时,很明显函数是用于求最大值的,
    实现细节就不关心了。
  3. 避免重复代码:当程序中有相同的代码部分时,可以把这部分写成一个函数,通过调用
    函数来实现这部分代码的功能,节约空间,减少代码长度。
  4. 隐含局部变量:在函数中使用局部变量,变量的作用范围不会超出函数,这样它就不会
    给外界带来干扰。

因此请大家适当定义函数来表达自己的逻辑。

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 的全局变量。为什么这种写法现在不被提倡,官方给出了两点原因:

  1. package.seeall 这种方式破坏了模块的高内聚,原本引入 "filename" 模块只想调用它的foobar() 函数,但是它却可以读写全局属性,例如 "filename.os" 。
  2. 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

其他

  1. nil ,将nil 赋予给一个全局变量就等同于删除了这个全局变量。

和 C 语言的区别

  1. 假值,lua 中只有 false 和 nil 是假值,int 0 和空字符串都是逻辑真。
  2. 在 lua 中整数运算的结果并不是向下取整:
print(5 / 10)   -->打印 0.5。

LuaJIT

  1. LuaJIT 是一个Lua 的解释器与即时编译器,速度比原生 Lua 解释器快,但是LuaJIT 只对 lua 的一个子集指令有优化作用,所以为了提升性能,需要注意尽量使用能被 LuaJIT 优化的指令。
  2. 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最佳实践

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342