美文网首页
Lua 初学者相关技巧

Lua 初学者相关技巧

作者: 豌豆很烦 | 来源:发表于2017-12-04 17:45 被阅读0次

    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最佳实践

    相关文章

      网友评论

          本文标题:Lua 初学者相关技巧

          本文链接:https://www.haomeiwen.com/subject/swmnbxtx.html