學(xué) Lua 語言?看這一篇就夠了

Lua 全教程

本文目錄

  • Lua 簡介
  • Lua 版本
  • Lua 環(huán)境
    • 開發(fā)工具
    • 軟件包管理
    • 分析和調(diào)試
  • 基礎(chǔ)概念
    • 常量和標識符
    • 變量和數(shù)據(jù)類型
    • 表達式(expression)
    • 語句(statement)
  • 慣用法
  • 高級特性
    • 高級語法結(jié)構(gòu)
      • 元表(metatable)
      • 環(huán)境(environment)
      • 閉包(closure)
      • 協(xié)程(coroutine)
    • 代碼組織
      • 模塊(module)
        • 定義模塊
        • 使用模塊
        • 模塊查找
      • 包(package)
    • 面向?qū)ο?/li>
    • 語言互操作
      • C API
      • FFI
  • 其它
    • 命令行參數(shù)
    • 裝飾器
  • 優(yōu)化建議
  • 常用類庫
  • 參考資料
  • Footnotes

Lua 簡介

#!/usr/bin/env lua
print("Hello World!")

Lua (LOO-ah) 是一種可嵌入、輕量、快速、功能強大的腳本語言。它支持過程式編程、 面向?qū)ο缶幊?、函?shù)式編程、數(shù)據(jù)驅(qū)動編程和數(shù)據(jù)描述(data description)。

Lua 將簡潔的過程式語法和基于關(guān)聯(lián)數(shù)組、可擴展語義的數(shù)據(jù)描述語法結(jié)構(gòu)結(jié)合了起來。 Lua 是動態(tài)類型的語言,它使用基于寄存器的虛擬機解釋和運行字節(jié)碼(bytecode),并 使用增量垃級回收(incremental garbage collection)機制自動管理內(nèi)存。這些特點使 得 Lua 很適合用于配置、腳本化和快速構(gòu)造原型的場景。

Lua 是第一個由第三世界國家(巴西)開發(fā)者開發(fā)的流行度很高的語言(and the leading scripting language in games)。

Lua 解釋器只有 2w+ 多行 ANSI C/C++ 代碼, 可執(zhí)行文件 200+ KB 大小。

下面是幾個嵌入了 Lua 解釋器,可以使用 Lua 擴展功能的知名應(yīng)用程序:

  • World of Warcraft
  • Angry Birds
  • Redis
  • Wireshark
  • Wrk
  • Nmap
  • MySQL Workbench
  • VLC

Lua 版本

Lua 官方于 2011 年發(fā)布的 5.2 和 2015 年發(fā)布了 5.3 版本,和用戶規(guī)模很大的 2006 年發(fā)布的 5.1 相比,改動很大,在 Lua 語法和 C API 方面都互不兼容。OpenResty 和 LuaJIT 對于這兩個最新版本的支持存在難度:

https://openresty.org/en/faq.html
Lua 5.2+ are incompatible with Lua 5.1 on both the C API land and the Lua
land (including various language semantics)...Lua 5.2+ are essentially
incompatible different languages.

Supporting Lua 5.2+ requires nontrivial architectural changes in ngx_lua's
basic infrastructure. The most troublesome thing is the quite different
"environments" model in Lua 5.2+. At this point, we would hold back ading
suport for Lua 5.2+ to ngx_lua.

https://www.reddit.com/r/lua/comments/2zutj8/mike_pall_luajit_dislikes_lua_53
Mike Pall (LuaJIT) dislikes Lua 5.3

總而言之,Lua 語言像 Python 一樣甩開了「向前兼容」的包袱,大跨步向前發(fā)展。這對 語言本身來講,是件好事兒,但是對使用者來講,短期內(nèi)是件痛苦的事兒。

由于我們本次學(xué)習(xí) Lua 的目的是為 OpenResty 開發(fā)做準備,所以,本文概念和示例主要 圍繞 Lua 5.1 展示。

Lua 環(huán)境

開發(fā)工具

軟件包管理

  • LuaRocks

分析和調(diào)試

  • print(), and tracing:

    [Wikipedia: https://en.wikipedia.org/wiki/Tracing_(software)]
    Tracing involves a specialized use of logging to record information to
    record information about a program's execution. This information is
    typically used by programmers for debugging purposes.
    
    Logging - error reporting.
    Tracing - following your program flow and data to find out where the
    performance bottlenecks are and even more important when an error occurs
    you have a chance to find out how you did get there. In an ideal world
    every function would have some tracing enabled with the function duration,
    passed parameters and how far you did get in your function.
    
    
  • luatrace - Toolset for tracing/analyzing/profiling script execution and generating detailed reports.

  • luaprofiler - Time profiler for the Lua programming language.

  • StackTracePlus - Drop-in upgrade to Lua's stack traces which adds local context and improves readability.

  • MobDebug - Powerful remote debugger with breakpoints and stack inspection. Used by ZeroBraneStudio.

  • Debug interface debug provided by Lua itself.

更多工具參見:lua-users.org/wiki/Progra…

基礎(chǔ)概念

常量和標識符

  • 語言關(guān)鍵字

    and, break, do, else, elseif, end, false, for, function, if, in,
    local, nil, not, or, repeat, return, then, true, until, while
    
    
  • 其它標識符

    + - * / % ^ # = ~= <= >= < > = ( ) { } [ ] ; : , . .. ...
    
    -- ',' is not an operator in Lua, but only a delimiter
    
    
  • 常量字符串使用 '" 分隔,其中可以使用類似 C 語言的轉(zhuǎn)義字符序列:

    \a, \b, \f, \n, \r, \t, \v, \\, \", \ddd, \0
    
    
  • 作為慣例,Lua 把以 _ 字符開始全部使用大寫字母的變量名,保留為內(nèi)部全局變量;

    _VERSION
    
    
  • 常量字符串可以使用 long brackets 語法表示:

    • [[opening bracket]]closing bracket ??梢允褂?= 表示層級,例如, [[ 表示第 0 層級, [====[ 表示第 4 層級;
    • 若其中含有轉(zhuǎn)義序列,該轉(zhuǎn)義序列不會被 Lua 轉(zhuǎn)義;
    • opening brackets 后面緊跟的換行會被 Lua 忽略;
    • opening brackets 會和第一次出現(xiàn)的同一層級的 closing brackets 匹配;
    a = [=[
    abc
    xyz]=]
    
    
  • Lua 使用雙精度浮點數(shù)表示數(shù)字常量,數(shù)據(jù)常量可以使用十六進制或科學(xué)計數(shù)法:

    3, 3.0, 3.1416, 314.16e-2, 0xff, 0x56
    
    
  • Lua 注釋有短注釋和長注釋兩種注釋方式:

    -- short comment
    
    --[[
         this is a very loooooooooooooong
         comment
    ]]
    
    --[=[
        Same scheme used with long string.
    ]=]
    
    

變量和數(shù)據(jù)類型

  • Lua 是一個動態(tài)類型語言,也就是說:

    • 「變量」沒有類型信息,但是「值」有類型信息;
    • 代碼中沒有類型定義語句,類型信息由「值」攜帶;
    • 所有的「值」都是 first-class 的,它們可以存儲在變量中、作為參數(shù)傳遞給其它函 數(shù)或者作為函數(shù)的返回值;
  • Lua 提供了 8 種基礎(chǔ)類型:

    • nil - 此類型下定義的值只有 nil ,它的主要屬性就是:和其它值不一 樣。通常, nil 用在其它有意義的值缺失的場景。

    • boolean - 此類型下定義的值有 falsetrue 。 在 Lua 的條件 表達式里, 除了 nilfalse 為「假值」外,其它類型都是「真值」(比 如, 0'' 在 Lua 中都是 「真」值)。

    • number - Lua 默認使用雙精度浮點型存儲該類型的值。

    • string - 此類型的值可以由任何 8-bit 字符組成。Lua 在內(nèi)存中為相同的字符 串保留一份數(shù)據(jù)( interning ),同時,Lua 不允許對字符串常量進行修改 (immutable)。

    • function - 此類型的值是使用 Lua 或者 C 編寫的函數(shù)。

    • userdata - 此類型的值是 C 語言數(shù)據(jù),從 Lua 的角度看,這些值對應(yīng)一塊無 預(yù)定義行為的裸內(nèi)存。Lua 不允許使用 Lua 代碼創(chuàng)建或者修改 userdata ,但 是允許使用 C API 實現(xiàn)這樣的功能。另外,用戶可以為 userdata 設(shè)定 metatable ,定義更多可以在它上面執(zhí)行的操作。

    • thread - 此類型的值是 Lua 線程,Lua 通過它實現(xiàn)協(xié)程功能。

    • table - 此類型的值是關(guān)聯(lián)數(shù)組(associative array),該類型有數(shù)組部分和字 典部分組成。數(shù)組部分保存索引為整數(shù),并且從 1 開始連續(xù)的數(shù)據(jù);字典部分保存剩 余其它數(shù)據(jù)(包括索引是整數(shù),但是不在 [1, #table] 的數(shù)據(jù))。

      • table 是 Lua 提供的僅有的數(shù)據(jù)結(jié)構(gòu)構(gòu)造機制:它可以用來表示 array, symbol table, set, record, graph, tree 等等數(shù)據(jù)結(jié)構(gòu);

      • nil 不能作用 table 的索引( error: table index is nil );

      • nil 不是有效的 table 元素值,如果將 nil 賦值給某個元素時, 相當(dāng)于從 table 中刪除了該元素;

      • Lua 提供了下面幾種創(chuàng)建 table 的方法:

        -- A field of the form `name = exp` is equivalent to
        -- `["name"] = exp`; A field of form `exp` is equivalent to
        -- `[i] = exp`, where `i` are consecutive numerical integers,
        -- starting with 1.
        a = { ["name"]= "dhb"; "male", [10]= 25, addr= "beijing",
              job= "code monkey"; }
        -- or
        a = {}
        a["name"] = "dhb"
        a[1] = "male"
        a[10] = 25
        a["addr"] = "beijing"
        a.job = "code monkey"
        a.removed = nil       -- this field will be removed by gc
        
        
      • Lua 為 a["name"] 的使用形式提供了語法糖: a.name

    • table, function, threaduserdata 類型的值在賦值、參數(shù) 傳遞、函數(shù)返回值等操作中,使用對它們引用,而非拷貝;

    • 可以使用 type() 函數(shù)得到描述值類型的字符串;

    • 在運行時,Lua 會自動根據(jù)上下文對 stringnumber 類型值互相轉(zhuǎn)換 類型(作為強類型語言,Lua 只支持如下隱式轉(zhuǎn)換):

      • 算術(shù)運算 中,將 string 類型轉(zhuǎn)換為 number 類型;

        10 + "10"
        
        
      • 當(dāng) number 類型用于需要 string 類型 (where a string is expected) 參與的場合時,將 number 類型轉(zhuǎn)換為 string ;

        -- valid
        10 .. " boxes"
        
        -- invalid
        10 == "10"
        
        
  • Lua 語言有三種類型的變量:全局變量,局部變量和 table 字段:

    • 變量默認是全局變量;

    • 函數(shù)參數(shù)是局部變量;

    • 使用 local 定義的變量是局部變量,并且 Lua 為其使用 詞法作用域( lexically scoped, or statically scoped) [1];

      function foo()
          function b() print(type(a), a) end
          local a = 10
          function f() print(type(a), a) end
          b(); f()
      end
      
      foo()
      
      
      • Local variables have their scope limited to the block where they are declared: The scope of a local variable begins at the first statement after its declaration and lasts until the last non-void statement of the innermost block that includes the declaration.

        -- example.lua
        local a = 10
        b = 10
        
        print(_G.g, _G.h)     -- nil, 10
        
        
      • A block is a list of statements; syntactically, a block is the same as a chunk:

        A chunk is an outermost block which you feed to "load()"
        -- Roberto
        
        
      • Lua handles a chunk as the body of an anonymous function with a variable number of arguments. As such, chunks can define local variables, receive arguments, and return values.

      • Explicit blocks are useful to control the scope of variable declarations. Explicit blocks are sometimes used to add a return or break statement in the middle of another block.

      -- explicit block
      do
          local a = 10
          print(type(a), a)
      end
      print(type(a), a)
      
      -- control structure
      if true then
          local a = 10
          print(type(a), a)
      end
      print(type(a), a)
      
      
      • 在局部變量作用域內(nèi)定義的內(nèi)部函數(shù)可以使用該局部變量,對內(nèi)部函數(shù)來說,這個 局部變量被稱為 upvalue ,或者 external local variable ;

        do
            local a = 10
        
            function bar()
                print(a)      -- `a` is called upvalue for `bar`
            end
        end
        
        
      • 每次使用 local 語句,都會創(chuàng)建一個新的局部變量;

        a = {}                -- global variable
        local x = 20
        for i = 1, 10 do
            local y = 0
            a[i] = function () y = y + 1; return x + y end
        end
        
        
    • 未賦值的變量默認值是 nil ,所以,變量最好在使用前進行定義(下文 「慣用法」一節(jié)提供了幾種檢測代碼里使用未定義變量的方式);

      -- error in Lua
      function foo()
          local function bar() zog() end
          local function zog() print "Zog!" end
          bar()
      end
      
      
      # alid in Python
      def foo():
          def bar():
              zog()
          def zog():
              print "Zog!"
          bar()
      
      
    • Lua 使用 table 保存所有的全局變量,這個 table 被稱為 environment table ,或 environment 。

      • 每個函數(shù)都擁有一份對 environment table 的引用,這樣一來,在函數(shù)中對全 局變量的查找都通過它完成;
      • 函數(shù)創(chuàng)建時,它會從創(chuàng)建者繼承 environment ;
      • 在函數(shù)中,可以通過 getfenv() 顯式獲取對它使用的 environment 的引 用;也可以通過 setfenv() 使用新的 environment 替換原來的 environment ;

表達式(expression)

  • 運算符:arithmetic operator, relational operators, logical operators, concatenation operator, unary minus, unary not, unary length operator.

    • 邏輯 not 運算符的結(jié)果是 true 或者 false ;邏輯 andor 運算符具有「短路」特征,它們的運算結(jié)果是第一個操作數(shù)或者第二個操作 數(shù);

      a = 10 and nil or 11
      
      
    • 長度運算符 # 可以用于獲取常量字符串的字節(jié)個數(shù),和 table 數(shù)組部分 元素的個數(shù):

      #"abcd"                     -- 4
      #{1, 2, 3}                  -- 3
      #{[0]= 0, [1]= 1, [2]= 2}   -- ?
      #{[2]= 2, [3]= 3, [4]= 4}   -- ?
      #{[1]= 1, name= "dhb"}      -- ?
      
      
    • 除了 ..^ 是右結(jié)合(right associative)外,其它運算符都是左 結(jié)合(left associative);

      From Wikipedia:
      
      The associativity of an operator is a property that determines how
      operators of the same precedence are grouped in the absence of
      parentheses. If an operand is both preceded and followed by operators,
      and those operators have equal precedence, then the operand may be
      used as input to two different operations. The choice of which
      operations to apply the operand to, is determined by the
      "associativity" of the operators.
      
      * Left-associative - the operations are grouped from the left.
      * Right-associative - the operations are grouped from the right.
      
      
    • 運算符優(yōu)先級

      6 + 2 - 7 ^ 2 ^ 2          -- ?
      
      
    • Lua 提供了專用的字符串連接操作符 ..

      Overloading `+` to mean string concatenation is a long tradition. But
      concatenation is not addition, and it is useful to keep the concepts
      separate, In Lua, strings can convert into numbers when appropriate
      (e.g 10 + "20") and numbers can convert into strings
      (e.g 10 ..  "hello"). Having separate operators means that there is no
      confusion, as famously happens in JavaScript.
      
      
  • 表達式:常量表達式,算術(shù)表達式,關(guān)系表達式,邏輯表達式,連接表達式,Vararg 表達式,函數(shù)調(diào)用表達式,函數(shù)聲明, table 構(gòu)造表達式等等;

    • 幾個關(guān)系表達式的例子:

      local t1, t2 = {}, {}
      print(t1 == t2)         -- false. Tables are never compared "element by
                              -- element".
      
      local s1, s2 = "abc", "abc"
      print(s1 == s2)         -- true. There is only ever one instance of any
                              -- particular string stored in memory, so
                              -- comparison is very quick (interning). And
                              -- strings are "immutable", there is no way in
                              -- Lua to modify the contents of a string
                              -- directly
      
      
    • 函數(shù)定義是一種可執(zhí)行的表達式,它的返回結(jié)果是 function 類型的值:

      function f() body end               -- equivalent to
      f = function () body end
      
      function a.b.c.f() body end         -- equivalent to
      a.b.c.f = function () body end
      
      local function f() body end         -- equivalent to
      local f; f = function () body end
      
      
      • 函數(shù)定義可以通過 varargs 表達式支持可變參數(shù)。varargs 表達式出現(xiàn)在函數(shù) 參數(shù)列表的最后,它會接收多出的參數(shù),以便在函數(shù)體中使用。由于 varargs 也 會返回多個值,Lua 對它的返回值的處理方式和函數(shù)調(diào)用類似。

        function foo(...)
            -- The expression `...` behaves like a multiple return function
            -- returning all varargs of the current function.
            local a, b, c = ...
            -- ...
            -- The expression `{...}` results in a array with all collected
            -- arguments.
            for i, v in ipairs{...} do
                -- ...
            end
            -- ...
            -- Whe the varargs list may contain valid `nil`s, we can use the
            -- `select` function to get specific arguments.
            local args = {n= select("#", ...), ...}
            for i = 1, args.n do
                print(args[i])
            end
        end
        
        
    • 函數(shù)調(diào)用表達式語法和其它語言類似,參數(shù)中的表達式在函數(shù)調(diào)用發(fā)生之前求值。 同時,Lua 在此基礎(chǔ)上提供了兩個語法糖:

      • v:name(args)v.name(v, args) 的語法糖,同時,也隱式指明, v 對象會被傳遞給 name 函數(shù)的第一個參數(shù) self
      • 如果參數(shù)只有一個并且其類型是 string 時,可以使用 f"string" 的形式調(diào) 用函數(shù);如果參數(shù)只有一個并且其類型是 table 時,可以使用 f{fields} 的形式調(diào)用函數(shù);

語句(statement)

  • 賦值語句

    i = 3
    i, a[i] = i + 1, 20
    
    x, y, z = y, z, x
    
    -- and cannot do following because it's a statement
    a = b = 1
    if (a = 1) then ... end
    
    
    • Lua 會將賦值語句兩側(cè)包含的表達式求值后,才真正進行賦值操作;

    • 在賦值語句執(zhí)行前,Lua 會根據(jù)左側(cè)的變量數(shù)據(jù)調(diào)整右側(cè)的值列表:如果右側(cè)的值 個數(shù)大于左側(cè)的變量個數(shù),超出的部分會被丟棄;如果右側(cè)的值個數(shù)小于左側(cè)的變 量個數(shù),Lua 會使用 nil 補充值列表;如果右側(cè)最后一個為函數(shù)調(diào)用,函數(shù) 所有的返回值都被補充到值列表中;

      -- in Lua
      x, y = {1, 2}       -- maybe `unpack` is needed
      
      
      # n Python
      x, y = (1, 2)
      
      
    • 函數(shù)調(diào)用時的參數(shù)值傳遞規(guī)則和賦值語句一致;而對于函數(shù)返回值的處理,Lua 還 有如下規(guī)則:

      • 如果函數(shù)調(diào)用作用單獨語句使用時,Lua 默認丟棄所有返回值;

        foo()
        
        
      • () 括起起來的表達式,包括函數(shù)調(diào)用,Lua 保留第一個返回值作為 整個表達式的值;

        function foo() return 1, 2, 3 end
        x, y, z = (foo())
        
        
      • 函數(shù)調(diào)用作為表達式的一部分使用時,Lua 保留其第一個返回值;

        function foo() return 1, 2, 3 end
        print(foo())
        print(1 + foo())
        
        
      • 除了上面描述過的,函數(shù)調(diào)用出現(xiàn)在賦值語句值列表的最后位置或函數(shù)調(diào)用參數(shù) 列表的最后位值,Lua 將其所有返回值補充到值列表外,在 table 構(gòu)造列表 和 return 語句返回值列表的最后位置,Lua 都會使用它的所有返回值;

        function foo() return 1, 2, 3 end
        t1 = {foo()}
        t2 = {4, foo()}
        t3 = {foo(), 5}
        
        
      • 和 Python 的區(qū)別

        -- in Lua
        x, y = foo()
        
        
        # n Python
        x, y = foo()      #  tuple and implicit unpack involved
        
        
  • 控制結(jié)構(gòu)

    while exp do block end
    repeat block until exp
    if exp then block {elseif exp then block} [else block] end
    
    -- numeric ``for``
    for var = exp, exp [, exp] do block end
    -- generic ``for``
    for var [, var...] in explist do block end
    -- ``explist`` is evaluated only once. Its results are an *iterator*
    -- function, a *state*, and an initial value for the first *iterator*
    -- variable
    
    break
    return
    -- but no ``continue``
    
    
  • 其它語句

    -- Function calls as statements
    foo()
    -- Local declaration
    local var
    
    

慣用法

The Lua way.

  • 內(nèi)存在沒有任何引用時,會自垃圾回收機制自動釋放,一般情況下,釋放時機由 Lua 解 釋器選擇。開發(fā)者可以通過調(diào)用 collectgarbage("collect") 強制 Lua 解 釋器進行垃圾回收,但是,通常需要連續(xù)調(diào)用兩次;

  • 如果想要記錄稀疏數(shù)組的元素個數(shù),需要使用者自己通過計數(shù)器保存和維護元素個數(shù);

    local t = {counter= 0}
    t[2] = "dhb"
    t.counter = t.counter + 1
    
    
  • Lua 函數(shù)的錯誤信息一般通過返回值返回給調(diào)用者,通常做法是:函數(shù)的第一個返回值 如果是 nil 或者 false 時,第二個返回值就是實際的錯誤信息;

  • 在 Lua 中使用 pcall/xpcall 調(diào)用可以實現(xiàn)其它語言中 try/catch 相同的作 用,而 error 就是其它語言中用于拋出異常的 throw 或者 raise ;

    local ok, err = pcall(function()
        t.alpha = 2.0    -- will throw an error if `t` is nil or not a table
    end)
    if not ok then
        print(err)
    end
    
    
  • 回調(diào)函數(shù):

    Callback function is one of the most powerful programming paradigms because
    it enables a general purpose function to do very specific things.
    
    
  • Lua 函數(shù)定義和函數(shù)調(diào)用都不支持「有名參數(shù)」(named argument),如果有類似的需 要時,可以使用 table 保存參數(shù),然后將該 table 作為函數(shù)的唯一參數(shù);

    function foo(args)
        local name = args.name or "anonymous"
        local os = args.os or "Linux"
        local email = args.os or name .. "@" .. os
        ...
    end
    
    foo{name= "bill", os="windows"}
    
    
  • 所有的全局變量都存放于名為 _Gtable 中;

    a = 10
    print(a, _G.a)
    _G._G == _G
    
    
  • 在表示常量字符串時,單線號 ' 或雙引號 " 沒有任何區(qū)別,字符串中都可以 使用轉(zhuǎn)義序列表示特殊字符;

  • 打印 table

    --[[
    This is not very efficient for big tables because of all the string
    concatenations involved, and will freak if you have *circular references
    or 'cycles'
    ]]
    function dumptable(o)
        if type(o) == 'table' then
            local s = "{"
            for k, v in pairs(o) do
                if type(k) ~= 'number' then k = '"' .. k .. '"' end
                s = s .. '[' .. k .. '] = ' .. dumptable(v) .. ','
            end
            return s .. '}'
        else
            return tostring(o)
        end
    end
    
    -- More options: http://lua-users.org/wiki/TableSerialization
    
    
  • 讀取文件

    • 使用 io.lines 函數(shù)讀取文件。該函數(shù)自動打開和關(guān)閉文件:

      for line in io.lines "myfile" do
          ...
      end
      
      
    • 使用 io.openio.close 顯式打開和關(guān)閉文件:

      local f, err = io.open("myfile")
      if not f then return print(err) end
      for line in f:lines() do
          ...
      end
      f:close()
      
      -- alternative reading
      local line = f:read '*l'
      while line do
          ...
          line = f:read '*l'
      end
      
      -- to read the whole file
      local s = f:read '*a'
      
      
  • 一些字符串處理的例子 [[5]]

  • 在運行時,Lua 默認會從全局環(huán)境或者模塊環(huán)境中查找未知變量,如果該變量未定義, Lua 將 nil 作為它的值返回。在 Lua 中 nil 也是合法的變量值,所以,在 運行時,很難區(qū)分某個變量的值是 nil 還是它未被定義。這種機制對于變量名拼 寫錯誤的情況不是好消息,在 Lua 中,有以下慣用方式用來檢測代碼中的未定 義(undeclared variables)變量:

    <http://lua-users.org/wiki/DetectingUndefinedVariables>
    
    In Lua programs, typos in variable names can be hard to spot because, in
    general, Lua will not complain that a variable is undefined...If a
    variable if not recognized by Lua as a local variable (e.g. by static
    declaration of the variable using a "local" keyword or function parameter
    definition), the variable is instead intepreted as a global
    variable...Whether a global varaible is defined is not as easy to
    determine or describe.
    
    
    • 運行時檢測

      • 通過重載當(dāng)前函數(shù)環(huán)境(下面的實現(xiàn)直接針對全局環(huán)境)的 metatable__index__newindex 字段,可以在運行時檢測全局未定義變量的讀 寫操作,并拋出運行時錯誤。這個方式的缺點是:只能用于運行時;無法檢測到 運行時未執(zhí)行(未覆蓋)到的代碼中的未定義變量引用;

        • strict module in the Lua distribution (etc/strict.lua);
        • LuaStrict by ThomasLauer for an extension of the strict approach;
      • Niklas Frykholm 實現(xiàn)了一個用于強制局部變量定義的模塊。它要求所有變量必須 使用 local 定義為局部變量了未定的變量。這個實現(xiàn)相當(dāng)于上面方式的擴展 版本,它的用法更優(yōu)雅,侵入性更低;

        --===================================================
        --=  Niklas Frykholm
        -- basically if user tries to create global variable
        -- the system will not let them!!
        -- call GLOBAL_lock(_G)
        --
        --===================================================
        function GLOBAL_lock(t)
          local mt = getmetatable(t) or {}
          mt.__newindex = lock_new_index
          setmetatable(t, mt)
        end
        
        --===================================================
        -- call GLOBAL_unlock(_G)
        -- to change things back to normal.
        --===================================================
        function GLOBAL_unlock(t)
          local mt = getmetatable(t) or {}
          mt.__newindex = unlock_new_index
          setmetatable(t, mt)
        end
        
        function lock_new_index(t, k, v)
          if (k~="_" and string.sub(k,1,2) ~= "__") then
            GLOBAL_unlock(_G)
            error("GLOBALS are locked -- " .. k ..
                  " must be declared local or prefix with '__' for globals.", 2)
          else
            rawset(t, k, v)
          end
        end
        
        function unlock_new_index(t, k, v)
          rawset(t, k, v)
        end
        
        -- Basically anytime you call ``GLOBAL_lock(_G)`` somewhere in your
        -- code, from that point onwards anytime you try to use a variable
        -- without explicitly declaring it as 'local', Lua will raise an error.
        
        
    • 靜態(tài)分析檢測 - 除了運行時動態(tài)檢測,我們還可以在代碼運行之前,使用靜態(tài)分析的方 式檢測未定義變量:

    • 運行時/靜態(tài)分析混合方式

  • ipairs 可以用來按索引從小到大的順序遍歷 table 中數(shù)組部分; pairs 可以遍歷 table 中的所有元素,但是輸出結(jié)果是無序的;

  • 如果需要為常量字符調(diào)用字符串處理函數(shù),可以使用 ("string"):method(...) 的 形式:

    ("%s=%d"):format("hello", 42)  -- is equivalent to
    string.format("%s=%d", "hello", 42)
    
    

高級特性

高級語法結(jié)構(gòu)

元表(metatable)
Every value in Lua can have a *metatable*.

Lua 通過 metatable 定義數(shù)據(jù)(original value)在某些特殊操作下(算術(shù)運算、大 小比較、連接操作、長度操作和索引操作等)的行為。

我們將 metatable 支持的具體操作稱為 event ,操作對應(yīng)的行為由 metamethod 體現(xiàn)。 metatable 實際上是一個普通的 tableevent 名添加 __ 下 劃線前綴后,作為 metatable 的索引(key),索引對應(yīng)的值(value)就是 metamethod 。 比如,使用非數(shù)字類型的值作為算術(shù)加+ 的操作數(shù)時,Lua 會使用 metatable__add 對應(yīng)的 metamethod 完成算術(shù)加運算。

metatable 提供的主要 event 有:

  • add - the + operation;

  • sub - the - operation;

  • mul - the * operation;

  • div - the / operation;

  • mod - the % operation;

  • pow - the ^ operation;

  • unm - the unary - operation;

  • concat - the .. operation;

  • len - the # operation;

  • eq - the == operation;

    • 可以通過重載 metatable__eq 方法重新定義對象的「相等性」規(guī)則, 但是需要注意的是, __eq 要求參與比較的兩個操作數(shù)有相同的類型并使用相同 的 __eq metamethod ;
  • lt - the < operation;

  • le - the <= operation;

  • index - The indexing access table[key];

    ``__index`` fires when Lua cannot find a key inside a table. ``__index``
    can be set to either a table or to a function; objects are often
    implemented by setting ``__index`` to be the metatable itself, and by
    putting the methods in the metatable. A naive ``Set`` class would put
    some methods in the metatable and store the elements of the set as keys
    in the object itself.
    
    
    • __indexmetamethod 可以是 table 或者函數(shù)
    -- simulation
    function gettable_event(table, key)
        local h
        if type(table) == "table" then
            local v = rawget(table, key)
            if v ~= nil then return v end
            h = metatable(table).__index
            if h == nil then return nil end
        else
            h = metatable(table).__index
            if h == nil then error(...) end
        end
        if type(h) == "function" then
            return (h(table, key))  -- call the handler
        else
            return h[key]           -- or repeat operation on it
        end
    end
    
    
  • newindex - the indexing assignment table[key] = value;

  • call - called when Lua calls a value;

    function function_event(func, ...)
        if type(func) == "function" then
            return func(...) -- primitive call
        else
            local h = metatable(func).__call
            if h then
                return h(func, ...)
            else
                error(...)
            end
        end
    end
    
    

在 Lua 代碼中,可以為每個 tableuserdata 設(shè)置不同的 metatable , 而其它 6 種數(shù)據(jù)類型每種類型的值使用相同的 metatable 。在 Lua 代碼中,只能 設(shè)置和修改 table 類型值的 metatable,其它類型的 metatable 可以使 用 C API 修改。

userdata 由 C API lua_newuserdata 創(chuàng)建,它和 malloc 創(chuàng)建的內(nèi)存塊有 所不同: userdata 占用的內(nèi)存會被垃圾回收器回收; 可以為 userdata 設(shè)置 metatable ,定制它的行為。 userdata 只支持兩種 event__len__gc ,其中, __gcmetamethod 由垃圾回收環(huán)節(jié),由垃圾回收器調(diào)用。比 如,標準庫提供的 file 類型的對象,它的 __gc metamethod 負責(zé)關(guān)閉底層文 件句柄。

另外, metatable 可以使用 __mode eventtable 定義為 weak table

To understand weak tables, you need to understand a common problem with
garbage collection. The collector must be conservative as possible, so
cannot make any assumptions about data; it is not allowed to be pyshic. As
soon as objects are kept in a table, they are considered referenced and
will not be collected as long as that table is referenced.

Objects referenced by a weak table will be collected if there is no other
reference to them. Putting a value in a table with weak values is effect
telling the garbage collector that this is not an important reference and
can be safed collected.

A weak table can have weak keys, weak values, or both. A table with weak
keys allows the collection of its keys, but prevents the collection of its
value. A table with both weak keys and weak values allows the collection of
both keys and values. In any case, if either key or the value is collected,
the whole pair is removed from the table.

環(huán)境(environment)

environment 是除了 metatable 外另外一種可以和 thread , function , userdata 類型的值相關(guān)聯(lián)的 table 。 environment 相當(dāng)于 命名空間,對象通過它查找可以訪問的變量。

對象間可以共享同一個 environment

  • thread 關(guān)聯(lián)的 environment 稱為 全局 enironment ,它們是該線程創(chuàng)建 的其它子 thread和非嵌套 function 的默認 enironment ;
  • 和 Lua function 關(guān)聯(lián)的 environment 是該 function 創(chuàng)建的嵌套 function 的默認 environment ;
  • 和 C function 關(guān)聯(lián)的 environment 只能在 C 代碼中訪問,它也會作為該函 數(shù)中創(chuàng)建的 userdata 的默認 environment
  • userdata 關(guān)聯(lián)的 environment 對 Lua 代碼沒有特殊含義,它只是一種為 userdata 附帶數(shù)據(jù)的較為方便的方式;

Lua 代碼中可以使用 getfenvsetfenv 操作 Lua function 和 正在運行的 threadenvironment , 而 C function , userdata 和其 它 threadenvironment 只能使用 C API 操作。

閉包(closure)
function count()
    local i = 0
    return function()
        i = i + 1
        return i
    end
end

local counter = count()
print(counter())
print(counter())

-- partial function
function bind(val, f)
    return function(...)
        return f(val, ...)
    end
end

prt = bind("hello", print)
prt(10, 20)

協(xié)程(coroutine)

Lua 原生支持協(xié)程,使用 coroutine 類型表示。Lua 協(xié)程代表有獨立執(zhí)行流程的線 程。協(xié)程由 Lua 解釋器調(diào)度執(zhí)行,一個協(xié)程顯式讓出執(zhí)行權(quán)后,其它協(xié)程才會被調(diào)度執(zhí) 行。

  • coroutine.create - 創(chuàng)建協(xié)程。該函數(shù)接收 function 類型參數(shù),作為新 協(xié)程的主函數(shù);
  • coroutine.resume - 恢復(fù)協(xié)程執(zhí)行。對新創(chuàng)建的協(xié)程調(diào)用該函數(shù)后,協(xié)程才真正開 始運行, 此時, coroutine.resume 的參數(shù)會用做協(xié)程主函數(shù)的參數(shù)。協(xié)程會一直 運行,直到它調(diào)用 coroutine.yield 主動讓出執(zhí)行權(quán)。 coroutine.resume 函 數(shù)在協(xié)程主函數(shù)執(zhí)行完畢、拋出異?;蛘咧鲃幼尦鲋鲃訖?quán)后才會返回:
    • 協(xié)程主函數(shù)正常退出時, coroutine.resume 返回 true 和主函數(shù)的返回 值;
    • 協(xié)程主函數(shù)異常退出時, coroutine.resume 返回 false 和錯誤信息;
    • 協(xié)程調(diào)用了 coroutine.yield 讓出執(zhí)行權(quán)時, coroutine.resume 返回 truecoroutine.yield 的調(diào)用參數(shù);
  • coroutine.yield - 協(xié)程讓出執(zhí)行權(quán)。使用 coroutine.resume 可以恢復(fù)主動 讓出執(zhí)行權(quán)的協(xié)程,從上次中斷的地方繼續(xù)往下執(zhí)行,此時, coroutine.resume 的參數(shù)會作為 coroutine.yield 的返回值使用。
  • coroutine.wrap - 創(chuàng)建協(xié)程的另一種方式。這個調(diào)用返回一個函數(shù),調(diào)用該函數(shù)就 相當(dāng)于顯式調(diào)用 coroutine.resume 。這個函數(shù)和 coroutine.resume 的區(qū)別 是,協(xié)程里拋出異常時, 這個函數(shù)會將異常再次投遞給函數(shù)調(diào)用者(所以, coroutine.resume 函數(shù)用來表示協(xié)程執(zhí)行是否成功的第一個返回值,也會被 coroutine.wrap 忽略)。
  • coroutine.running() - 返回處于運行狀態(tài)的協(xié)程。如果調(diào)用者是 main thread 時,它會返回 nil ;
  • coroutine.status() - 獲取協(xié)程當(dāng)前狀態(tài),返回值是字符串。協(xié)程的狀態(tài)有: running, suspended, normal, dead 。

代碼組織

模塊(module)

Lua 的模塊和其它語言作用類似,用于將一組功能相似的函數(shù)和常量存放一起,方便用戶 共享代碼。

<Programming in Lua 2nd>
From the user point of view, a *module* is a library that can be loaded
through ``require`` and that defines one single global name containing a
table. Everything that the module exports, such as functions and
constants,it defines inside this table, which works as a namespace. A
well-behaved module also arrange for ``require`` to return this table.

Lua 語言實現(xiàn)提供了諸如 math, io, string 等等的標準模塊,用戶可以在 代碼中直接使用這些模塊提供的功能。同時,Lua 也給用戶提供了實現(xiàn)自定義模塊的機制 和方法,用戶可以使用 Lua 代碼或者 C API 開發(fā)自定義模塊。

定義模塊

通常有如下兩種使用 Lua 代碼定義模塊的方法:

  • Lua 5.1 提供的 module() 簡化了 Lua 標準模塊的創(chuàng)建流程。

    ``module(name, ...)`` creates a table and sets it as the value of the
    global ``name`` and the value of ``package.loaded[name]``, so that
    ``require`` returns it.
    
    
    • module() 函數(shù)實際上做了如下幾件工作:

      -- testm.lua (somewhere on the Lua path)
      local print = print
      module("testm")
      
      function export1(s)
          print(s)
      end
      
      function export2(s)
          export1(s)
      end
      
      -- muser.lua
      local testm = require("testm")
      testm.export2("text")
      
      
      1. 首先, module 為其后的函數(shù)構(gòu)造一個 table ,這個 table 會被 require 作為返回值返回給調(diào)用者;
      2. 其次,將該 table 設(shè)為這些函數(shù)的 envrionment :這些該模塊內(nèi)部的函數(shù) 調(diào)用彼此時,就不需要使用 testm 作為前綴;同時,全局環(huán)境被該環(huán)境覆蓋;
      3. 另外,如果使用 package.seeall 作為 module 的參數(shù)時, module 將該 tablemetatable__index 成員設(shè)置為全局環(huán)境 _G , 此時該模塊中定義的函數(shù)就可以訪問全局環(huán)境的變量或者函數(shù)了;
      4. 設(shè)置全局變量 testm ,的值為新創(chuàng)建的 table ;
      5. 設(shè)置 package.loaded["testm"] 的值為新創(chuàng)建的 table ;
    • 如果為 module 使用 varargs 表達式,即 module(...) 的形式時,Lua 會使用模塊所在的文件名,作為模塊名,此時模塊文件可以方便的移動位置;

    • Lua 5.2+ 不再建議使用 module 函數(shù)創(chuàng)建模塊的方式,它有幾個為人所詬病的地 方:

      • package.seeall 參數(shù)將全局環(huán)境暴露在模塊環(huán)境里,用戶可以通過模塊來訪 問全局環(huán)境,比如 testm.io ,造成 leaky encapsulation 問題;

      • module 函數(shù)會將它創(chuàng)建的 table 放到全局環(huán)境中;同時,它會還會自動 向全局環(huán)境導(dǎo)入該模塊的依賴模塊;

        module "hello.world" creates a table ``hello`` (if not already
        present) and ``world`` as a table within that. If ``hello.world``
        requires ``fred`` then ``fred`` becomes automatically available to
        all users of ``hello.world``, who may come to depend on this
        implementation detail and get confused if it changed.
        
        
  • Lua 5.2 建議使用如下方式(Lua 5.1 也支持該方式)創(chuàng)建模塊:

    -- mod.lua
    local M = {}
    
    function M.answer()
        return 42
    end
    
    function M.show()
        print (M.answer())
    end
    
    return M
    
    
使用模塊

Lua 提供的內(nèi)建模塊會解釋器預(yù)加載到全局環(huán)境里,在 Lua 代碼中可以直接使用或者通 過全局環(huán)境引用。

-- main.lua
 for line in io.lines "myfile" do
     ...
 end

 -- or
 for line in _G.io.lines "myfile" do
     ...
 end

而用戶自定義的模塊,在使用前需要通過 require 函數(shù)加載到代碼塊可以直接訪問的 environment 中。

使用 module 實現(xiàn)的模塊在加載時,會將模塊放到全局環(huán)境 _G 中,加載后的模塊 像內(nèi)建模塊一樣可以直接調(diào)用。

-- main.lua
require "socket"
socket.connect(...)

或者,

-- main.lua
local socklib = require "socket"
socklib.connect(...)

而上面提到的 Lua 5.2 的方法實現(xiàn)的模塊,不會在全局環(huán)境定義變量。使用這類模塊時, 只能通過 require 使用該模塊:

-- main.lua
local mymod = require "mymod"
mymod.do_something()

總結(jié)下來,模塊的使用建議如下:

The ``require "name"`` syntax was the one introduced in Lua 5.1; This call
does not always return the module, but it was expected a global would be
created with the name of the library (so, you now have a ``_G.name`` to use
the library with). In new code, you should use ``local name = require
"name"`` syntax; it works in the vast majority of cases, but if you're
working with some older modules. They may not support it, and you'll have
to just use ``require "module"``.

模塊查找

require 函數(shù)負責(zé)查找和加載模塊,這個過程大致步驟如下(以 require "testm" 為例):

  1. 根據(jù) package.preload["testm"] 的值,判斷 testm 是否己經(jīng)加載過:如果 該模塊已經(jīng)加載過, requirepackage.preload["testm"] 的值返回;如 果該模塊未加載過,繼續(xù)第 2 步;
  2. 逐個調(diào)用 package.loaders 設(shè)置的 searcher 函數(shù),選擇一個可以用于加載 testmloader。Lua 默認提供了 4 個 searcher
    • A searcher simply looks for a loader in the package.preload table.
    • A searcher looks for a loader as a Lua library using package.path.
    • A searcher looks for a loader as a C library, using package.cpath.
    • A searcher searches the C path for a library for the root name of the given module.
  3. 調(diào)用 loader 加載和執(zhí)行模塊代碼。如果 loader 有返回值, require 將這個返回值賦與 package.preload["testm"] ;如果 loader 沒有返回 值, requirepackage.loaded["testm"] 賦值為 true
  4. requirepackage.loaded["testm"] 的值返回給調(diào)用者;

上述流程的模擬代碼如下:

function require(name)
    if not package.loaded[name] then
        local loader = findloader(name)
        if loader == nil then
            error("unable to load module " .. name)
        end
        package.loaded[name] = true
        local res = loader(name)
        if res ~= nil then
            package.loaded[name] = res
        end
    end
    return package.loaded[name]
end

從上面的描述可以看到, require 根據(jù) package.path 中設(shè)置的路徑查找 Lua 模塊,根據(jù) package.cpath 中設(shè)置的路徑模式查找 C 模塊。路徑模式是包含了 ?; 的字符串,; 用于分隔文件系統(tǒng)的路徑, ? 會被 require 替換成模塊名。例如:

-- package.path
./?.lua;/usr/share/lua/5.1/?.lua;/usr/share/lua/5.1/?/init.lua

執(zhí)行 require("testm") 時, require 會依次使用下面路徑查找模塊代碼:

./testm.lua
/usr/share/lua/5.1/testm.lua
/usr/share/lua/5.1/testm/init.lua

包(package)

Lua 允行將模塊按照層級結(jié)構(gòu)組織起來,層級之間使用 . 分隔。例如, 模塊 mod.sub 是模塊 mod的子模塊。 package 就是這樣按此形式組織起來的 模塊的集合,同時,它也是 Lua 中用于代碼分發(fā)的單元。

和模塊類似,例如,使用 require 查找和加載子模塊 a.b.c 時, require 通過 package.loaded["a.b.c"] 的值判斷該子模塊是否已經(jīng)被加載過。和模塊不同 的是,如果子模塊未被加載過, require 先將 . 轉(zhuǎn)換成操作系統(tǒng)路徑分隔符, 比如,類 UNIX 平臺上, a.b.c 被轉(zhuǎn)換成 a/b/c ,然后使用 a/b/c 替換 package.pathpackage.cpath 中的 ? 后,查找子模塊文件。

module 函數(shù)也提供了對子模塊的支持,例如,上面的子模塊可以使用 module("a.b.c") 的方式定義。同時, module 會定義全局變量 a.b.c 引用 子模塊:

``module`` puts the environment table into variable ``a.b.c``, that is ,
into a field ``c`` of a table in field ``b`` of a table ``a``. If any of
these intermediate tables do not exist, ``module`` creates them. Otherwise,
it reuses them.

需要注意的一點是,同一個 package 中的子模塊之間,除了上面提到的它們的環(huán)境可 能嵌套存放以外,并沒有顯式的關(guān)聯(lián)。比如, 執(zhí)行 require("a") 時,并不會自動加 載它的子模塊 a.b ;執(zhí)行了 require("a.b") 時,也不會自動加載 a 模塊;

面向?qū)ο?/h4>

Lua 語言并未提供對面向?qū)ο缶幊棠P偷脑С?,但是它提供?table 類型和 metatable ,environment 等機制,可以用來實現(xiàn)類似的面向?qū)ο蠊δ堋?/p>

下面是摘自 PiL 的代碼示例:

--- A base class
Account = {balance= 0}

-- Lua hide `self` when using *colon operator*, a syntactic sugar
function Account:new(o)
    -- A hidden `self` refers to table `Account`
    o = o or {}
    setmetable(o, self)
    self.__index = self
    return o
end

function Account:deposit(v)
    self.balance = self.balance + v
end

function Account.withdraw(self, v)
    if v > self.balance then error "insufficient funds" end
    self.balance = self.balance - v
end

-- creates an instance of Account
a = Account:new{balance = 0}
a:deposit(100.00)   -- syntactic sugar of `a.deposit(a, 100.00)`

--- Inheritance
-- `SpecialAccount` is just an instance of `Account` up to now.
SpecialAccount = Account:new()
s = SpecialAccount:new{limit=1000.00}   -- `self` refers to `SpecialAcount`
-- the metatable of `s` is `SpecialAcccount`.
-- `s` is a table and Lua cannot find a `deposit` field in it, so it look
-- into `SpecialAccount`; it cannot find a `deposit` field there, too, so
-- it looks into `Account` and there it finds the original implementation
-- for a `deposit`
s:deposit(100.00)

-- What makes a `SpecialAccount` special is that we can redefine any method
-- inherited from its superclass.
function SpecialAccount:withdraw(v)
    if v - self.balance >= self:getLimit() then
        error"insufficient funds"
    end
    self.balance = self.balance - v
end

function SpecialAccount:getLimit()
    return self.limit or 0
end

-- Lua does not go to `Account`, because it finds the new `withdraw` method
-- in `SpecialAccount` first.
s:withdraw(200.00)

由于語言所限,使用 Lua 實現(xiàn)的面向?qū)ο竽M,并不能提供隱私控制機制。

語言互操作

luafaq#T4.4 luafaq#T4.5 luafaq#T7

C API

TODO: To be finished.

FFI

其它

命令行參數(shù)

在使用 lua 解釋器運行 lua 腳本文件時,Lua 解釋器會將所有命令行參數(shù)通過全局 table 類型數(shù)組 arg 的方式傳遞給腳本文件:

如下命令行調(diào)用,

% lua -la b.lua t1 t2

Lua 會創(chuàng)建有如下元素的 arg 數(shù)組:

arg = {
    [-2]= "lua", [-1]= "-la",
    [0]= "b.lua", [1]= "t1", [2]= "t2"
}

其中,索引值為 0 的元素是腳本的文件名,索引值從 1 開始的元素是在命令中出現(xiàn)的腳 本文件名后面的命令行參數(shù),索引值小于 0 的是出現(xiàn)在腳本文件名前面的命令行參數(shù)。

在 Lua 代碼中,還可以使用 ... varargs 表達式獲取索引從 1 開始的命令行參數(shù)。

不出意外,Lua 并未提供處理命令行參數(shù)的標準方式。但是開發(fā)者可以參考其它 Lua 程 序,比如 Luarocks ,使用的處理邏輯,或者使用非標準庫 lapp 。

下面的代碼摘自 Luarocks ,它使用 Lua 的字符串匹配函數(shù)進行命令行參數(shù)解析:

--- Extract flags from an argument list.
-- Given string arguments, extract flag arguments into a flags set.
-- For example, given "foo", "--tux=beep", "--bla", "bar", "--baz",
-- it would return the following:
-- {["bla"] = true, ["tux"] = "beep", ["baz"] = True}, "foo", "bar".
function parse_flags(...)
    local args = {...}
    local flags = {}
    for i = #args, 1, -1 do
        local flag = args[i]:match("^%-%-(.*)")
        if flag then
            local var, val = flag:match("([a-z_%-]*)=(.*)")
            if val then
                flags[var] = val
            else
                flags[flag] = true
            end
            table.remove(args, i)
        end
    end
    return flags, unpack(args)
end

裝飾器

lua-users.org/wiki/Decora…

優(yōu)化建議

The first question is, do you actually have a problem? Is the program not
*fast enough? Remember the three basic requirements of a sytem: Correct,
Robust, and Efficient, and the engineering rule of thumb that you may have
to pick only two.

Donald Knuth is often quoted about optimisation: "If you optimise
everything, you will always be unhappy" and "we should forget about small
efficiencies, say about 97% of the time: premature optimisation is the root
of all evil."

Assume a program is correct and (hopefully) robust. There is a definite
cost in optimising that program, both in programmer time and in code
readability. If you don't know what the slow bits are, then you will waste
time making your ugly and maybe a little faster (which is why he says
unhappy).

<Lua Performance Tips>

Nevertheles, we all know that performance is a key ingredient of
programming. It is not by change that problems with exponential time
complexity are called *intractable*. A too late result is a useless result.
So every good programmer should always balance the costs from spending
resources to optimize a piece of code against the gains of saving resources
when running that code.

The first question regarding optimization a good programmer always asks is:
"Does the program needs to be optimized?" If the answer is positive (but
only then), the second question should be: "Where?"

所以,優(yōu)化建議的第一條就是不要輕易嘗試優(yōu)化。如果確實到了非優(yōu)化不可的地步, 也需要先用工具定位需要優(yōu)化的地方,比如,代碼中會被頻繁調(diào)用并且性能不佳的函 數(shù)和內(nèi)循環(huán)里的低效操作等等,對這些地方的優(yōu)化能用較少的工作量換來整體性能的提升。 LuaProfiler 就是一個用于定位代 碼中低效熱點的工具。

我們還可以使用 LuaJIT 替代標準 Lua (Vanilla Lua) 運行代 碼,它可能會帶來 幾十倍 的性能提升。

CPU 密集型的操作可以放到使用 C API 實現(xiàn)的模塊中。如果實現(xiàn)正確的話,整體可以 達到近似原生 C 程序的性能。同時,因為 Lua 語言語法精練,整體代碼也更短小,更易 維護。 另外,通過 LuaJIT 提供的 FFI 等類似接口,甚至可以直接訪問外部庫提供的 C 語言函數(shù)和數(shù)據(jù)結(jié)構(gòu),這樣就省去了使用 C API 編寫模塊的繁雜工作。

下面是幾條可以提高代碼性能的 開發(fā)建議

  • Locals are faster than globals.

    Local variables are very fast as they reside in virtual machine
    registers, and are accessed directly by index. Global variables on the
    other hand, reside in a lua table and as such are accessed by a hash
    lookup.
    -- Thomas Jefferson
    
    
    local next = next
    local i, v = next(t, nil)         -- 10% faster
    while i do i, v = next(t, i) end
    
    
  • Memory allocation from the heap -- e.g. repeatedly creating tables or closures -- can slow things down.

  • Multiplication x*0.5 is faster than division x/2; x*x is faster than x^2;

  • 盡量避免新字符串創(chuàng)建;

  • 盡量重用己有對象;

  • 使用 table.concat 替代 .. 完成字符串拼接;

  • 緩存會被多次使用的中間計算結(jié)果(memoizing);將與循環(huán)無關(guān)的計算挪到循環(huán)外部;

    function memoize(f)
        local mem = {}                    -- memoizing table
        setmetatable(mem, {__mode= "kv"}) -- make it weak
        return function(x)                -- new version of 'f', with
                                          -- memoizing
            local r = mem[x]
            if r == nil then              -- no previous result?
                r = f(x)                  -- calls original function
                mem[x] = r                -- store result for reuse
            end
            return r
        end
    end
    
    -- redefine 'loadstring'
    loadstring = memoize(loadstring)
    -- then use new version 'loadstring' as the original one
    
    
  • Lua 代碼編譯是項比較繁重的工作,所以,盡量避免在運行時編譯(比如,調(diào)用 loadstring);

  • Lua table 分為數(shù)組部分和字典部分兩個部分。當(dāng) table 空間不足時,插入 新元素會觸發(fā) tablerehash 操作,申請更多的內(nèi)存,重新插入原有元素。 rehash 帶來的開銷隨著插入數(shù)據(jù)的增加,會變得不那么顯著,比如,向空 table 中的數(shù)組部分插入 3 個元素時,會觸發(fā) 3 次 rehash ,當(dāng)插入元素達到百萬時,只 需要 20 次 rehash 。但是如果創(chuàng)建了很多元素較少的 table 時,這個開銷就 很明顯了。對 table 來說,最直接的優(yōu)化措施就是按照需要,在表創(chuàng)建時就預(yù)先 分配好內(nèi)存:

    • 可以通過 C API 提供的 lua_createtable 函數(shù)在表創(chuàng)建時指定需要的空間;
    • 使用占位符: {true, true, true} 告訴 Lua 創(chuàng)建可容納 3 個數(shù)組元素的 table ; {x= 1, y= 2, z= 3} 也有類似的作用。
  • 由上面的描述我們知道,Lua 會在 table 空間不足并插入新元素時,對該 table 進行 rehash。這意味著,刪除 table 元素(將元素值置成 nil )并不會立即觸發(fā) table 內(nèi)存回收,內(nèi)存回收會在下一次 rehash 時完 成。所以,想要釋放 table 占用的內(nèi)存,最好直接刪除 table 本身。

  • 根據(jù)不同的使用場景,調(diào)整垃圾回收器配置參數(shù)。

    <Lua Performance Tips>:
    
    Most recycling in Lua is done automatically by the garbage collector. Lua
    uses an incremental garbage collector. That means that the collector
    performs its task in small steps (incrementally) interleaved with the
    program execution. The pace of these steops is proportional to memory
    allocation: for each amount of memory allocated by Lua, the garbage
    collector does some proportional work. The faster the program consumes
    memory, the faster the collector tries to recycle it.
    
    Function ``collectgarbage`` provides several functionalities: it may stop
    and restart the collector, force a full collection cycle, force a
    collection step, get the total memory in use by Lua, and change two
    parameters that affect the pace of the collector.
    
        * ``parse`` - controls how long the collector waits between finishing
          a collection cycle and starting the next one.
        * ``stepmul`` - controls how much work the collector does in each
          step.
    
    Roughly, smaller parses and larger step multipliers increase the
    collector's speed.
    
    
    • 對于批處理類型的程序,由于進程生存周期短,垃圾回收的必要性就不高,可以將其 關(guān)閉;

    • 對于非批處理類型的程序,就不能簡單關(guān)閉垃圾回收了事了。但是可以在進行時效性 要求較高的邏輯時,暫時停止垃圾回收。在必要時候,可以停掉垃圾回收,并且在合 適的時機顯式調(diào)用垃圾回收。

      In Lua 5.1, each time you force some collection when the collector is
      stopped, it automatically restarts. So, to keep it stopped, you must
      call ``collectgarbage("stop")`` immediately after forcing some
      collection.
      
      
    • 根據(jù)需求調(diào)整垃圾回收器的參數(shù)。運行快的垃圾回收邏輯,會消耗更多的 CPU,但是 會降低整體內(nèi)存使用。

常用類庫

Libraries and Bindings Kepler Project

  • 純 Lua 實現(xiàn)的數(shù)據(jù)處理、函數(shù)式編程和操作系統(tǒng)路徑操作等等功能類庫 Penlight
  • 兼容 PCRE 的正則庫 lrexlib
  • 二進制字符串操作庫 struct
  • Socket, HTTP, Mail LuaSocket
  • 配置文件解析庫 pl.config
  • 詞法掃描器 pl.lexer
  • 文件系統(tǒng)操作 luafilesystem
  • 守護進程 luadaemon
  • 異步網(wǎng)絡(luò)庫 copas
  • 類似 gevent 的異步網(wǎng)絡(luò)庫 levent
  • Libuv Binding luv
  • QT Binding lqt
  • GTK Binding lua-gtk

參考資料

瀏覽原文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容