天天看點

小剛帶你深入淺出了解Lua語言前言1、Lua語言的特點2、Lua語言基礎3、參考文獻

前言

這篇文章并不是針對某個知識點深入剖析,而是聚焦在Lua語言的關鍵知識點覆寫和關鍵使用問題列舉描述。能夠讓學習者對Lua整體有個認識(使用一門新的語言不僅僅在用的時候适應它,而是知道怎麼善于使用它),同時也可以作為一個工具文檔在Lua中遇到具體問題的時候能從這裡索引到相應的知識點和Lua的一些原理,得到啟發。

小剛帶你深入淺出了解Lua語言前言1、Lua語言的特點2、Lua語言基礎3、參考文獻

1、Lua語言的特點

簡單的說Lua語言是一個可擴充的嵌入型的腳本語言。它具有以下的特點:

  • 嵌入式語言: 它是ANSI C實作,在大多數 ANSI C 編譯器中無需更改即可編譯,包括 gcc(在 AIX、IRIX、Linux、Solaris、SunOS 和 ULTRIX 上)、Turbo C(在 DOS 上)、Visual C++(在 Windows 3.1/95/NT 上)、Think C (MacOS) 和 CodeWarrior (MacOS)。基本上每種程式設計語言都有調用 C 函數的方法,是以您可以在所有這些語言中使用 Lua。 這包括 C++、Go、Rust、Python、……
  • 解釋型語言:Lua腳本會先編譯成位元組碼,然後在Lua虛拟機上解釋執行這些位元組碼。保證了它的可移植性
  • 動态類型語言:Lua語言本身沒有定義類型,不過語言中的每個值都包含着類型資訊
  • 簡潔輕量,運作速度快:它所有的實作不到6000行 ANSI C代碼。隻包括一個精簡的核心和最基本的庫,較新的5.4.3版本解釋器編譯後283kB(Linux,amd64)。同時Lua通常被稱為 市場上最快的腳本級 HLL 語言
  • 設計原則遵循盡量使用機制來代替規則約定: Lua語言中包含的機制有子產品管理、自動垃圾收集、元表和元方法、引用機制等。這些機制下面會詳細介紹

基于這些特點我們會很願意将Lua嵌入到我們的應用中,用于拓展應用的能力。

1.1、Lua與宿主程式的關系

以下圖顯示了Lua與宿主程式之間的關系:可以嵌入到宿主程式,并為宿主程式提供腳本能力,同時可以幫助拓展宿主程式。另外Lua也提供了一些工具幫助編譯Lua文本(luac),執行lua腳本(lua)

小剛帶你深入淺出了解Lua語言前言1、Lua語言的特點2、Lua語言基礎3、參考文獻

1.1.1、Lua語言的組成

  • Lua C-api:正如上面所說Lua的所有的能力都是在C層實作的,并通過基礎的C-api暴露出來。同時Lua也提供auxlib輔助庫,它是基于基礎C-api的更高一層的抽象封裝。與基礎API不同,基礎API接口的設計力求經濟性和正交性,而auxlib力求對于通用任務的實用性。
  • 标準庫:Lua語言也包含标準庫(io, math, string等),不過語言設計者為了保證Lua盡量的小,這些标準庫是獨立分開的。如果應用不需要用到這些标準庫可以不需要加載,如果需要則可以通過luaopen_io等方法加載具體的庫,或者>=5.1版本時通過luaL_openlibs來加載所有标準庫。
  • 拓展三方庫:此外Lua還可以擴充其他三方庫,方式有3種:
  1. 在lua中 require “子產品名”。require機制下面會介紹,簡單說lua的require加載機制會在package.cpath查找”子產品名”的動态庫并加載,同時找到luaopen_子產品名的函數執行,并把執行結果緩存并傳回
  2. 把C方法添加到Lua标準庫清單中,例如把luaopen_子產品名 添加到由luaL_openlibs打開的标準庫清單中
  3. 使用Lua C api的luaL_requiref方法将子產品添加到package.loaded中,該方法同lua的 require方法
  • Lua内置的機制:Lua内置了很多的機制讓開發過程盡量的簡單,程式盡量的高效。其中包括:子產品加載機制(require),自動垃圾回收機制,元表和元方法的元機制,錯誤處理機制(pcall),引用機制(自動管理table的key)等。下面會詳細介紹這些機制。
  • Lua編譯器 :将Lua腳本編譯成位元組碼
  • Lua虛拟機:Lua虛拟機會維護兩個狀态——global_state和Lua_state。
  1. lua_state:包含兩個棧,Callinfo 棧(方法調用棧) 和  TValue 棧(資料棧,關于TValue的 介紹 )。分别用于緩存函數的調用資訊的連結清單和參數傳遞。在Lua内部,參數的傳遞是通過資料棧,同時Lua與C等外部進行互動的時候也是使用的棧。
  2. global_state: 負責全局的狀态,比如GC相關的,系統資料庫,記憶體統計等等資訊

1.1.1.1、lua_state、call_info調用棧、資料棧之間的關系

參考連結:

連結
小剛帶你深入淺出了解Lua語言前言1、Lua語言的特點2、Lua語言基礎3、參考文獻

圖1.1

callinfo 結構組成一個雙向連結清單,它的結構如下:

小剛帶你深入淺出了解Lua語言前言1、Lua語言的特點2、Lua語言基礎3、參考文獻

圖1.2

其中lua_State的base_ci指向第一層調用,而ci則記錄着目前的調用。

CallInfo會占用棧的一部分,用來儲存函數參數,本地變量,和運算過程的臨時變量。如圖1中callinfo到lua_stack的部分空間映射。

1.1.1.2、global_state全局狀态

從Lua 源碼lstate.h中定義的global_State的結構我們可以了解global_state包含的資訊:

/*
** 'global state', shared by all threads of this state
*/
typedef struct global_State {
  lua_Alloc frealloc;  /* function to reallocate memory */
  void *ud;         /* auxiliary data to 'frealloc' */
  l_mem totalbytes;  /* number of bytes currently allocated - GCdebt */
  l_mem GCdebt;  /* bytes allocated not yet compensated by the collector */
  lu_mem GCmemtrav;  /* memory traversed by the GC */
  lu_mem GCestimate;  /* an estimate of the non-garbage memory in use */
  stringtable strt;  /* hash table for strings */
  TValue l_registry;
  unsigned int seed;  /* randomized seed for hashes */
  lu_byte currentwhite;
  lu_byte gcstate;  /* state of garbage collector */
  lu_byte gckind;  /* kind of GC running */
  lu_byte gcrunning;  /* true if GC is running */
  GCObject *allgc;  /* list of all collectable objects */
  GCObject **sweepgc;  /* current position of sweep in list */
  GCObject *finobj;  /* list of collectable objects with finalizers */
  GCObject *gray;  /* list of gray objects */
  GCObject *grayagain;  /* list of objects to be traversed atomically */
  GCObject *weak;  /* list of tables with weak values */
  GCObject *ephemeron;  /* list of ephemeron tables (weak keys) */
  GCObject *allweak;  /* list of all-weak tables */
  GCObject *tobefnz;  /* list of userdata to be GC */
  GCObject *fixedgc;  /* list of objects not to be collected */
  struct lua_State *twups;  /* list of threads with open upvalues */
  unsigned int gcfinnum;  /* number of finalizers to call in each GC step */
  int gcpause;  /* size of pause between successive GCs */
  int gcstepmul;  /* GC 'granularity' */
  lua_CFunction panic;  /* to be called in unprotected errors */
  struct lua_State *mainthread;
  const lua_Number *version;  /* pointer to version number */
  TString *memerrmsg;  /* memory-error message */
  TString *tmname[TM_N];  /* array with tag-method names */
  struct Table *mt[LUA_NUMTAGS];  /* metatables for basic types */
  TString *strcache[STRCACHE_N][STRCACHE_M];  /* cache for strings in API */
} global_State;           

global_state包含以下資訊:

  1. stringtable:全局字元串表, 字元串池化,使得整個虛拟機中短字元串隻有一份執行個體。
  2. gc相關的資訊
  3. l_registry : 系統資料庫(管理全局資料) ,Registry表可以用debug.getregistry擷取。系統資料庫 就是一個全局的table(即整個虛拟機中隻有一個系統資料庫),它隻能被C代碼通路,通常,它用來儲存 那些需要在幾個子產品中共享的資料。比如通過luaL_newmetatable建立的元表就是放在全局的系統資料庫中。
  4. mainthread:主lua_State。在一個獨立的lua虛拟機裡, global_State是一個全局的結構, 而lua_State可以有多個。 lua_newstate會建立出一個lua_State, 綁在 lua_State *mainthread.可以說是主線程、主執行棧。
  5. 元表相關 :
  • tmname (tag method name) 預定義了元方法名字數組;
  • mt 存儲了基礎類型的元表資訊。每一個Lua 的基本資料類型都有一個元表。 

下圖描述了gloable_state裡面比較主要的一個部分——系統資料庫。

 ENV: 圖中Lua腳本的的上值_ENV就是系統資料庫裡面的全局表_G,它是通過LUA_RIDX_GLOBALS這個索引從系統資料庫裡面索引過來的。Lua腳本中的所有對全局變量的引用都是對_G的引用,不過不是直接操作_G,而是指向_G的另一個參數_ENV。

小剛帶你深入淺出了解Lua語言前言1、Lua語言的特點2、Lua語言基礎3、參考文獻

1.1.1.3、_ENV 和 _G

如上圖所示,Lua腳本中通路全局變量實際上是通路的_ENV 表(table類型), 腳本中對全局變量通路的代碼在編譯後會被Lua編譯器加上_ENV字首。那麼_ENV究竟是什麼呢?在Lua語言裡,Lua會把所有的代碼段都當作匿名函數來處理,而同時也會把_ENV作為該匿名函數的上值綁定到該匿名函數,是以Lua裡面的腳本實際會做如下的轉換:

普通Lua代碼寫法:

x = 10
local y = 20
z = x + y
 
print("z:" .. tostring(z))
 
--output
--[[
    z:30
]]           

經過編譯器實際轉換後會變成這樣:

local _ENV = _G
local func = function(...)
    _ENV.x = 10
    local y = 20
    _ENV.z = _ENV.x + y
    
    print("z:" .. tostring(_ENV.z))
end
 
func()
 
 
--output
--[[
    z:30
]]           

這裡引出另2個概念:上值(upvalues),能支援上值的閉包(closure)。下面會詳細提到。

2、Lua語言基礎

上一章了解了Lua大緻的樣子和它是一門什麼樣的語言,以及它如何為宿主應用提供嵌入式腳本能力的。接下面我們從一個新手開發者的角度去開啟這一門語言吧。

2.1、詞法規範

作為一個開發者在Lua編碼時需要遵循它的詞法規範,保證一緻的編碼風格。這裡列舉一下:

  • 辨別符(或名稱):是由任意字母、數字和下劃線組成的字元串(注意:不能以數字開頭)
  • “下劃線+大寫字母”(例如_VERSION)組成的辨別符通常被Lua語言用作特殊用途
  • Lua語言是大小寫敏感的,例如:And和AND是兩個不同的辨別符

2.1.1、注釋

單行注釋:

-- 這個是注釋内容           

多行注釋:

–[[注釋内容]],或 –[[ 注釋内容 –]]           
--[[
this is multiline annotatiaon
--]]           

或者

--[[
this is multiline annotatiaon
]]           

注釋代碼時建議用這種方式: --[[ 注釋内容 --]] ,這樣在第一行補一個 ‘-’ 字元就可以取消注釋了,會非常友善。例如:

---[[
local f = function()
    print("method in annotation")
end
--]]
-- 這裡可以繼續調用函數 f
f()           

2.1.2、變量

Lua裡面定義的變量預設是全局變量,即直接寫變量名即為全局變量。相反局部變量的定義需要加 local 關鍵字來修飾,例如全局變量g_var,局部變量loc_var:

g_var = "this is glocal variable"
local loc_var = "this is local variabl"
print("g_var:" .. tostring(g_var) .. ", loc_var:" .. tostring(loc_var))
 
--output
--[[
    g_var:this is glocal variable, loc_var:this is local variabl
]]           

2.2、Lua 基本類型

了解了Lua的詞法規範後我們可以着手開始寫Lua代碼了。

Lua是一門動态類型的語言,主要展現在Lua沒有類型定義,不過它的每個值都帶有類型資訊。我們先了解一下Lua有哪些基本類型,Lua有8種基本類型:nil, number, string ,table, function, boolean, userdata, thread。

下面一一介紹一下:

2.2.1、nil

nil代表空,變量定義出來在第一次指派之前是空的,目的用于告訴lua它是沒有初始化的。注意nil在lua裡面隻有指派操作=,和判斷操作==,~=才有效,其它操作符都會報錯,是以這種類型是無法使用的。我們在處理算數運算符或字元串連接配接符号使用時需要特别注意這個類型的檢查,不然程式會出錯。例如我們在處理字元串連接配接建議如下處理:

local a = "aaa" .. tostring(b)           

2.2.2、number

在Lua5.2及之前的版本中,所有的數值都以雙精度浮點格式表示。從Lua5.3版本開始,Lua語言為數值格式提供了兩種選擇:integer ——64位整型和float —— 雙精度浮點類型。我們在編譯Lua庫時也可以将Lua 5.3 編譯為精簡Lua模式,在該模式中使用32位整型和單精度浮點類型。

2.2.3、string

Lua 語言中字元串是一串位元組組成的序列,Lua的核心不關心這些位元組以何種方式編碼文本,它使用8個比特位來存儲。Lua語言中的字元串可以存儲包括空字元在内的所有數值代碼,這意味着我們可以在字元串中存儲任意的二進制資料。就是說也可以使用編碼方法(UTF-8,UTF-16)來存儲unicode字元串。

2.2.3.1、string常使用的方式舉例

> 擷取長度:

Lua中擷取字元串的長度有兩種方式:#字元串和string.len(字元串)。這裡推薦使用 “#” 操作符,因為string.len()實際需要先查找string(table)再找其下的len,然後傳參調用,至少需要 4 條 lua vm bytecode;而#直接被翻譯為LEN指令,一條指令就可以算出來。

以下列舉了幾種擷取字元串長度的方式,源檔案是用的utf-8編碼:

local a = "aaa"
local a_len = utf8.len(a)
 
local b = "你好"
local b_len1 = utf8.len(b)
local b_len2 = #b
local b_len3 = string.len(b)
 
print("a_len:" .. a_len .. ", b_len1:" .. b_len1 .. ", b_len2:" .. b_len2 .. ", b_len3:" .. b_len3)
 
-- 輸出
-- a_len:3, b_len1:2, b_len2:6, b_len3:6           

> 字元串連接配接:

另一個用的比較多的是字元串連接配接操作符—— “ .. ”,這裡需要關注2個點:

  1. .. 操作符不能操作 nil類型。如前面在介紹nil類型時提到的,nil是告訴系統這個變量沒有初始化時的類型,隻有在使用判斷符号和指派符号不會出錯,其他操作符号都會出錯。是以在使用 “..” 連接配接操作符時務必要檢查它不為nil
  2. 字元串連接配接操作符在連接配接多個操作符時實際會建立字元串的多個副本,會大量使用該操作符時會帶來記憶體和CPU的開銷,在Lua中也可以像其他語言一樣使用字元串緩存的方式來處理(例如:Java中的StringBuilder,Lua中可以用table.concat)。代碼示例如下:
local str = ""
 
local begin_time = os.time()
for i=1, 300000 do
    str = str  .. "[xxxxxxxxxxx],"
end
local delay = os.time() - begin_time
 
print("first delay:" .. delay)
 
str = ""
begin_time = os.time()
local buffer = {}
for i=1, 300000 do
    table.insert(buffer, "[xxxxxxxxxxx]")
end
 
str = table.concat(buffer, ",")
delay = os.time() - begin_time
print("second delay:" .. delay)
 
--output
--[[
first delay:87
second delay:1
]]           

從以上代碼的執行結果來看table.concat的方式處理字元串連接配接非常高效的,普通連接配接符的方式花了87秒,而table.concat隻花了1秒。在實際的開發過程中,我們也需要牢記這一點。

> 長文本字元串定義

在其他語言例如Java中定義長文本需要關注文本中的換行和字元轉義,Lua提供了一種非常友善的定義方式—— [[長字元串]]。

代碼示例如下:

local a = [[
adfadfadfadf,
hello world
{
    "conio":1,
    "b":2,
    "c": [
        1,2,3
    ]
 
}
]]
 
print(a)           

也就是說Lua作為一門嵌入式腳本語言它在處理資料上提供了很多便利的文法糖的。

2.2.3.2、字元串标準庫常用的方法

string标準庫中公開的一些常用的方法包括:string.rep, string.reverse, string.lower, string.upper,string.len等。這裡主要介紹一下string.gsub, string.pack, string.unpack,主要是因為gsub在一些關鍵的邏輯使用比較多,string.pack對于二進制字元串打包用于傳輸的場景使用比較多。

> gsub

是字元串替換函數,它的第3個參數可以是一個表(table)或者一個function,用于查找和處理替換内容。接下來舉一個:

下面的示例展示了利用gsub的第3個參數為function時可以将一個字元串解析為另一種table查找的表達方式。這種用法在一些結合查找table元素的場景确實非常有用。這也正是Lua的強大之處。

local share_module = {
    qq = {
        qzone = {
            img = function()
            end,
            text = function(message)
                print("share with content:" .. tostring(message))
            end
        },
        chat = {
 
        }
    }
}
 
local share_protocal = function(module, method, text)
    local invoke_fun = module
    string.gsub(method,'[^\\.]+',function(w)
        invoke_fun=invoke_fun[w]
    end)
 
    print("share_protocal after gsub:" .. tostring(invoke_fun))
    invoke_fun(text)
end
 
share_protocal(share_module, "qq.qzone.text", "hello world")
 
-- output
--[[
    share with content:hello world
]]           

> string.pack和string.unpack

這兩個函數用于在二進制資料和Lua的基本類型值之間進行轉換的函數。string.pack會把值“打包”成二進制字元串,而函數string.unpack是從二進制字元串中提取這些值。關于這個API的參數介紹參考

這裡

。舉個栗子:

local s = string.pack("s1", "hello")
for i=1, #s do
    print(string.unpack("B", s, i))
end
 
-- output
--[[
5   2
104 3
101 4
108 5
108 6
111 7  
]]           

對于函數裡面的format字段的介紹,可以參考這篇文章:

s[n]: 長度加内容的字元串,其長度編碼為一個 n 位元組(預設是個 size_t) 長的無符号整數。上面的示例中 s1 中的 1 代表用一個位元組來存放字元串的長度。輸出中看出在我們逐個列印每個位元組時,第一行是是 "5 2",表示長度是 "5",并且 " 2 "代表下一個未讀的位元組的索引,因為這裡是逐個位元組讀取,是以下一個未讀的位元組的索引是 2 了。

string.pack和string.unpack在網絡傳輸中打包傳輸位元組數組經常使用到,另外在不同的系統之間傳遞資料也經常使用。将各種類型的資料打包成位元組數組來傳遞,非常的高效和友善。

> utf8庫

從Lua5.3開始,Lua語言引入了一個用于操作UTF-8編碼的Unicode字元串的标準庫。當然,在引入這個标準庫之前,Lua語言也提供了對UTF-8字元串的合理支援。

還是以算字元串長度的代碼為例:

local a = "aaa"
local a_len = utf8.len(a)
 
local b = "你好"
local b_len1 = utf8.len(b)
local b_len2 = #b
local b_len3 = string.len(b)
 
print("a_len:" .. a_len .. ", b_len1:" .. b_len1 .. ", b_len2:" .. b_len2 .. ", b_len3:" .. b_len3)
 
-- 輸出
-- a_len:3, b_len1:2, b_len2:6, b_len3:6           

utf8庫能正确計算出utf8字元串中字元的個數。

> 模式比對

談到模式比對還是挺有意思,為啥Lua搞個自己的模式比對,為啥不用業内公認的正規表達式呢?原因還是考慮到Lua庫的大小。一個典型的POSIX正規表達式實作超過4000行代碼,比所有lua語言标準庫的總大小的一半還大,lua語言的模式比對實作代碼不到600行。别小看區區600行代碼,基本功能一應俱全,已經非常強大了。

下面挑了2個我覺得挺不錯的點介紹一下:

  • 最短比對符 “-”

Lua裡面使用了較簡單的符号“-”來相對應“+”實作了最短比對。這個很有用,而在正規表達式中這個字元不是這個含義。這個最短比對可以用在一段字元串裡面出現相同的多個比對時,比對最短的場景。例如下面這個例子:

local s = "int x; /* 需要去掉的注釋内容 */ int y; /* 需要去掉的注釋内容 */"
local result = string.gsub(s, "/%*.-%*/", "")
print(result)
 
-- output:
--[[
    int x;  int y; 
]]           

當文本中有多個注釋塊需要比對時不會竄。

  • 比對捕獲

捕獲在正規表達式裡面常用“()”括号圈出來。在Lua中會經常用到,例如:

  • 比對并擷取到捕獲的分組内容
  • 比對并替換捕獲到的分組内容

下面分别舉這兩個例子:

擷取捕獲的分組示例:

我們将通過分組捕獲到的内容逐個指派給多個變量分别儲存起來

-- 捕獲的副本
local s = [[
    I say to everybody: "hello"
]]
 
local quote_begin, quoted_content, quote_end = string.match(s, "([\"'])(.-)(%1)")
print("quote_end:" .. tostring(quote_begin) .. ", what you say:" .. tostring(quoted_content) .. ", quote_end:" .. tostring(quote_end))
 
-- output:
--[[
    quote_end:", what you say:hello, quote_end:"
]]           

Lua語言中支援傳回多個結果的特性真的非常友善,省去了像其他語言中需要對資料打包一下,然後用的地方解包的過程。上面的例子中把捕獲的結果直接擷取并直接指派給多個結果,代碼是不是非常簡潔易懂~。

替換捕獲分組示例:

Lua中的字元串替換函數是 gsub,下面的例子用到了gsub,且用到了 %n 這樣的比對具體的第幾個捕獲,可以使用的場景非常多。例如下面這個将查找到的字元串換成另外一種表達形式:

local s = [[
    "lily-girl". "Tom-boy", we are twins
]]
 
local result = string.gsub(s, "\"([%a]+)-([%a]+)\"", "Hello my name is %1 and I'm %2")
print(result)
 
-- output
--[[
    Hello my name is lily and I'm girl. Hello my name is Tom and I'm boy, we are twins
]]           

另外我們通過gsub再來實作string.trim的功能。trim方法用于去除字元串前後多餘的空格,在這裡我們也用到了 %n 這樣的比對捕獲的方式,将捕獲到的不包含前後空格字元的内容替換之前的内容,這樣實作了trim的功能。代碼示例如下:

local s = " adfafadfadfad    "
 
local result = string.gsub(s, "^%s*(.-)%s*$", "%1")
print(result)
 
--output:
--[[
adfafadfadfad
]]           

再舉一個使用場景的例子,替換字元串中的占位符。這裡的做法是将文本中$(%a+)出現的内容視為占位符,然後捕獲括号裡面的%a+表示的占位符名稱。接下來用到了gsub的第三個參數為table類型的情況,gsub會用捕獲到的值作為參數在table(這裡是variable)裡面查找,将查找到的值作為待替換的内容去替換gsub裡面第二個參數比對到的内容。

local s = [[
    Hello, my name is $name, and I like $food
]]
 
local variable = {
    name = "Tom",
    food = "Pizza"
}
 
local result = string.gsub(s, "$(%a+)", variable)
print(result)
 
--output:
--[[
    Hello, my name is Tom, and I like Pizza
]]           

2.2.4、table

這個類型十分強大,它是Lua裡面主要的資料結構,他可以用來實作其它語言中的常見的資料結構:Map,集合,數組,記錄等。下面列舉了table使用中我們需要留意的幾個點:

2.2.4.1、table中的key

table的key可以是除了nil意外的任意值,也就是說它可以是字元串,數值,甚至是table。當用到table作為key時,就出現了一個非常有用的唯一key的用法。為了避免table中的key沖突,可以使用table對象作為key(因為table對象的位址是唯一的),代碼示例如下:

local a = {}
 
local unique_key = {}
a[true] = 2
a[unique_key] = 3
 
for key, val in pairs(a) do
    print("key:" .. tostring(key) .. ", val:" .. tostring(val))
end
 
--output
--[[
key:table: 0x7ffd9a4096f0, val:3
key:true, val:2
]]           

我們還可以通過把值設定為nil來從table中删除一條記錄。

2.2.4.2、table中元素的安全通路

在其它語言中在通路某個可能為空的對象裡面的屬性時會用到三目運算符,但是Lua中沒有這個運算符。Lua作者推薦我們另一種方式來實作類似的操作:

local a = {
    b = {
        c = {}
    }
}
 
-- 安全通路 a.b.c.d
 
local d = (((a or {}).b or {}).c or {}).d
 
print("d:" .. tostring(d))
-- output
-- d:nil           

2.2.4.3、table的長度

計算table的長度也可以用計算string長度的”#”操作符來完成,不過需要記住的是對于table計算長度,這個table最好是個數組,而不是包含其他非連續整形類型的集合,如果對這樣的集合算長度會得到不可預知的結果。下面列舉了3個計算table長度的示例:

print("---------------seperator------------------")
a = {}
a[1] = 1
a[2] = nil
a[3] = 2
a[4] = 3
a[5] = nil
 
print("table len:" .. #a)
 
-- output::
-- table len:4
 
print("---------------seperator------------------")
a = {}
a[1] = 1
a[10000] = 1
print("table2 len:" .. #a)
 
-- output:
-- table2 len:1
 
print("---------------seperator------------------")
a = {}
a[1] = 1
a["b"] = 2
a[3] = 2
a[4] = 3
print("table3 len:" .. #a)
 
for _index, value in ipairs(a) do
    print("_index:" .. _index .. ", value:" .. tostring(value))
end
 
-- output:
-- table3 len:4
--_index:1, value:1           

我們知道table裡面把值設定為nil代表從table中删除該元素,對于上面示例中的table2場景,究竟是之前有10000個值,後面把數組中間的值設定為了nil,還是本身這個table就隻有包含2個key的元素呢,我們看到的結果是這個table的長度傳回了1,是不是比較難了解。是以還是那句話計算table的長度時,我們最好能确認這個table是一個包含連續整形索引的數組。Lua中的數組是從1開始的,不過你可以定義任意數值的key。

2.2.4.4、使用pairs和ipairs周遊一個table

Lua中周遊一個table可以用 in pairs 或者 in ipairs,還可以用下面這個,具體怎麼使用參考

for var=exp1,exp2,exp3 do  
    <執行體>  
end           

這裡我主要介紹一下pairs和ipairs,因為lua裡面的table結構比較強大,可以用來表示其它語言裡面多種結構,包括:數組,map,集合,記錄等。

當我們的table表示一個數組時可以用ipairs,不過使用ipairs有一些限制:

  • ipairs的key需要是數值類型,非數值類型會被忽略
  • 索引的順序是确定的從1開始的連續增序分布

是以需要注意:不要用ipairs周遊一個非連續整數索引的table

當一個table為一個集合或map時,可以使用pairs。它沒有ipairs這些限制,不過也要注意:使用pairs周遊集合裡面的nil記錄會被跳過。

下面這個示例是分别使用ipairs和pairs周遊一個包含任意類型的集合:

a = {
    [0]="zero", "a", 1, [5] = {}, nil, 3
}
 
for _index, value in ipairs(a) do
    print("_index:" .. _index .. ", value:" .. tostring(value))
end
 
-- 輸出
--[[
_index:1, value:a
_index:2, value:1
]]
 
print("---------------seperator------------------")
for _key, value in pairs(a) do
    print("key:" .. tostring(_key) .. ", value:" .. tostring(value))
end
 
-- 輸出
--[[
key:1, value:a
key:2, value:1
key:4, value:3
key:0, value:zero
key:5, value:table: 0x7fb8ecc098b0
]]           

示例中我們使用pairs時能周遊到table中的除了nil的其他所有值。但是周遊的順序并不是table中原始的順序。

同時上面的示例也印證了我上面提到的限制。這說明對于一個自由度較高的table我們也并非可以任意發揮,我們要知道table是什麼樣的結構,我們可以采用什麼方式來周遊。

2.2.4.5、table的标準庫

lua也提供了标準庫用于操作table,常用的api有:table.insert, table.remove, table.move, table.sort等,這些api對于table的操作都非常有用。這裡列舉一個table排序的例子,我們知道table結構的自由度很高,是以對個table排序可以自定義:

local a = {
    {
        name = "jack",
        score = 100
    },
    {
        name = "jack2",
        score = 98
    },
    {
        name = "sam",
        score = 35
    },
    {
        name = "tom",
        score = 78
    }
}
 
table.sort(a, function(_a, _b)
    return _a.score > _b.score
end)
 
for index, t in ipairs(a) do
    print("name:" .. tostring(t.name) .. ", score:" .. tostring(t.score))
end
 
-- output
--[[
name:jack, score:100
name:jack2, score:98
name:tom, score:78
name:sam, score:35
]]           

另外想重點介紹一下另外兩個api: table.pack 和 table.unpack,為什麼想介紹這兩個api,因為他們在其它語言是沒有的,是以比較特别,但是它們在Lua中應運而生。我了解由于Lua的函數入參支援可變參數,函數的傳回也支援傳回多個,因為這些強大的能力,是以孕育出了table.pack和table.unpack這樣的api。它們的主要用途是對一個包含任意類型的集合拆成單個項,也可以反過來打包成一個集合結構。

以一個示例來描述:

local f1 = function(...)
    local params_table = table.pack(...)
    local params_table2 = params_table
    return params_table2
end
 
local packed_data = f1(nil, 1, {}, nil, function() end, 3)
local a, b, c, d, e, f = table.unpack(packed_data)
print("params a:" .. tostring(a) .. ", b:" .. tostring(b) .. ", c:" .. tostring(c) .. ", d:" .. tostring(d) .. ", e:" .. tostring(e) .. ", f:" .. tostring(f))
 
--output
--params a:nil, b:1, c:table: 0x7f945dc09940, d:nil, e:function: 0x7f945dc07ca0, f:3
print("---------------seperator------------------")
 
packed_data = f1(nil, 2, nil)
local a,b,c = table.unpack(packed_data)
print("params a:" .. tostring(a) .. ", b:" .. tostring(b) .. ", c:" .. tostring(c))
 
--output
-- params a:nil, b:2, c:nil           

上面這個示例展示了包含任意類型的集合可以打散成一個個的項,也可以反過來把這些項打包到一個table裡面。這個api适用于lua語言的可變參數和多結果傳回的特性。

另外上面的示例中local params_table = table.pack(…)的寫法也可以簡單的換位local params_table = {…},這樣的文法糖對于程式設計是非常便利的。這裡也可以看出Lua在處理程式設計細節方面考慮了很多,這也展現出它對這個語言設計的初衷:簡潔輕量,機制重于規則限制。

2.2.4.6、table.pack和unpack的使用特别注意

這裡标紅一下,在使用table的pack和unpack時容易踩到一個坑,我們看一個例子:

local table2 = {
    [1] = "http://www.baidu.com",
    [2] = {
    },
    [3] = {
       ["product_name"] = "1",
       ["cp_order_id"] = "123",
       ["app_name"] = "demo1",
       ["order_amount"] = "0.01"
    },
    [5] = function() end,
    [6] = function() end
}
 
local table3 = {
    "http://www.baidu.com",
    {},
    {
       ["product_name"] = "2",
       ["cp_order_id"] = "123",
       ["app_name"] = "demo2",
       ["order_amount"] = "0.01"
    },
    nil,
    function() end,
    function() end
}
 
local v1, v2, v3, v4, v5, v6 = table.unpack(table2)
print("table2: v1:" .. tostring(v1) .. ", v2:" .. tostring(v2) .. ", v3:" .. tostring(v3) .. ", v4:" .. tostring(v4) .. ", v5:" .. tostring(v5) .. ", v6:" .. tostring(v6))
 
v1, v2, v3, v4, v5, v6 = table.unpack(table3)
print("table3: v1:" .. tostring(v1) .. ", v2:" .. tostring(v2) .. ", v3:" .. tostring(v3) .. ", v4:" .. tostring(v4) .. ", v5:" .. tostring(v5) .. ", v6:" .. tostring(v6))
 
--output
--[[
table2: v1:http://www.baidu.com, v2:table: 0x7f8dfb40b3f0, v3:table: 0x7f8dfb40b430, v4:nil, v5:nil, v6:nil
table3: v1:http://www.baidu.com, v2:table: 0x7f8dfb40b5b0, v3:table: 0x7f8dfb40b5f0, v4:nil, v5:function: 0x7f8dfb40b690, v6:function: 0x7f8dfb40b6b0
]]           

上面的示例中table2和table3其實表達的意思相同,但是unpack的結果卻不一樣。主要計算table的長度時我們需要明确的知道它是一個數組,中間不要有nil,不然結果會匪夷所思。可以從這篇文章了解一下:

推薦做法:

将key明确的設定為字元串可以避免上面的問題

-- 避免數組出現nil,參數最多個數為8個
local function param_encode(v1, v2, v3, v4, v5, v6, v7, v8, ...)
    assert(#{...} == 0, "參數不能大于8")
    return {
        v1 = v1,
        v2 = v2,
        v3 = v3,
        v4 = v4,
        v5 = v5,
        v6 = v6,
        v7 = v7,
        v8 = v8
    }
end
local function param_decode(tb)
    return tb.v1, tb.v2, tb.v3, tb.v4, tb.v5, tb.v6, tb.v7, tb.v8
end           

2.2.4.6、元表和元方法

Lua設計了元表和元方法,它的目的在于:用于拓展任意值在面對一個未知操作時的行為。這個機制不僅僅可以作用于table,甚至可以作用于基本類型,但是在Lua腳本中我們改變不了基本類型的元操作行為,不過可以在C層實作。這裡先提一下,抛出這個概念,讓我們提前有個印象。因為table除了結構強大,他還能實作類似其他腳本或面向對象語言的方法原型、繼承等概念,這些概念依賴元機制,用到table必然會用到元機制。更詳細的我們在下面的元機制中介紹。

下面展示了元機制實作的兩個table相加,table和其它類型數值相加的示例:

local tb1 = {1,2,3}
local tb2 = {'a','b','c'}
 
local mt = {
    __add = function(a, b)
        local result = {}
        for _index, value in ipairs(a) do
            table.insert(result, value)
        end
 
        if type(b) == "table" then
            for _index, value in ipairs(b) do
                table.insert(result, value)
            end
        else
            table.insert(result, b)
        end
        
        return result
    end
}
setmetatable(tb1, mt)
setmetatable(tb2, mt)
 
local b = tb1 + tb2
for _ind, val in ipairs(b) do
    print("index:" .. _ind .. ", value:" .. tostring(val))
end
 
--output
--[[
index:1, value:1
index:2, value:2
index:3, value:3
index:4, value:a
index:5, value:b
index:6, value:c
]]           

上面的示例中為table添加了元表(setmetatable),這個元表裡面添加了元方法_add(Lua語言裡面定義了一些操作符的元方法名稱例如_div表示除法)。該元方法的入參是對應”+”的兩個操作數,我們可以自定義實作我們想要怎麼做,例如這裡我們假設第一個操作數是table, 希望如果第二個操作數是table則合并到第一個table,第二個操作數是其他類型則直接插入到第一個table中。看到輸出結果能滿足我們這個想法。我們再試試将一個table和一個number類型的資料相加會怎麼樣:

local result2 = tb1 + 111
for _ind, val in ipairs(result2) do
    print("index:" .. _ind .. ", value:" .. tostring(val))
end
 
--output
--[[
index:1, value:1
index:2, value:2
index:3, value:3
index:4, value:111
]]           

是不是也是符合預期的。

這裡簡單的過了一下元機制,以及它在table結構中的應用。詳細了解元機制可以看下面的元機制的介紹。

2.2.5、function

function在Lua語言是lua是“第一類值”,Lua中所有的函數都是匿名的,不僅可以存儲在全局變量中,還可以存儲在表字段和局部變量中。同時上面介紹全局變量的時候有提到Lua語言把所有的代碼段都當作匿名函數,而同時也會把_ENV作為該匿名函數的上值綁定到該匿名函數。

lua中有三類函數:大類都是LUA_TFUNCTION,變體分别是LUA_VLCL(Lua closure)、LUA_VLCF(light C function)、LUA_VCCL(C closure)

lua的closure是下面這個樣子的,它包括:函數原型(編譯後的包含指令資訊的位元組碼塊)和上下文環境。上下文環境包括:upvalues(上值)和 env(所在環境)。

小剛帶你深入淺出了解Lua語言前言1、Lua語言的特點2、Lua語言基礎3、參考文獻

env就是上面提到的_ENV,它可以在loadfile時指定,不指定的時候預設會設定為全局表_G。想要指定某個closure的_env可以這麼做:

env = {}
loadfile("xxx.lua", "t", env)()           

loadfile會傳回一個function類型的closure,是以會再加上一個()用于執行該closure。在上面這個示例中我們指定了環境為一個空table,修改了closure的環境。這個在某種意義上起到了沙盒的作用,也就是這個加載的lua腳本的環境是空的,它無法通路C層的庫,也就是腳本隻能執行自己的業務。

那上值是啥?簡單的說它是closure特有的,當建立一個closure時可以給他綁定一些值,這些值在該closure内可以通路,這些值就是上值。其實看c function的宏定義可以知道它其實也是closure類型,隻是上值固定為0。是以作者說lua中隻有閉包沒有函數,總結來說閉包 = 函數+若幹個上值(up value)。更多上值相關的介紹看官方的這篇文章:

2.2.5.1、lua function和 c function的差別

lua function腳本會預編譯成位元組碼,然後存儲為function類型值,而c_function需要通過lua c api注冊給lua使用。詳細見這篇文章:

對于CClosure資料結構(C閉包函數):

  • lua_CFunction f:函數指針,指向自定義的C函數
  • TValue upvalue[1]:C的閉包中,使用者綁定的任意數量個upvalue

對于LClosure資料結構(Lua腳本函數):

  • Proto *p:Lua的函數原型,在下面會有詳細說明
  • UpVal *upvals:Lua的函數upvalue,這裡的類型是UpVal,這個資料結構下面會詳細說明,這裡之是以不直接用TValue是因為具體實作需要一些額外資料。

2.2.5.2、重定義函數

了解了Lua中的function是什麼,我們了解一下function在Lua語言中使用上的一些特點。從上面我們了解到我們Lua腳本中引用的全局變量和全局方法實際是從上下文環境_ENV的table中擷取的。我們可以把這個全局table中的方法替換成任意其它值,這裡介紹一種在Lua中經常用的hook方式程式設計。以下示例展示了腳本hook了全局方法,并在方法裡面額外做了一些事情:

local origin_print = print
print = function(message)
    if type(message) == "table" then
        local buff = {}
        for k, v in pairs(message) do
            table.insert(buff, k .. ":" .. v)
        end
        
        local result_msg = table.concat(buff, ",")
        origin_print("hooked print:" .. tostring(result_msg))
    else
        origin_print("hooked print:" .. tostring(message))
    end
end
 
print({a=1, b=2, c=3})
 
-- not forget to set back
print = origin_print
 
-- output:
--[[
    hooked print:b:2,c:3,a:1
]]           

2.2.5.3、多參數傳回和函數可變入參

Lua的function可以有多個傳回值,入參支援可變參數。這個特性挺巧妙的,在使用方面會帶來很多的便利,主要展現在以下幾個方面:

  • 多傳回值省去了對結果的包裝:在Java語言中多個傳回結果我們會用Pair, Object, Map, Json等進行包裝,Lua中隻要直接傳回多個資料值就好。
  • 變參入參巧妙的達到了方法重載的目的。如果一個同名方法想要再拓展一個或多個參數,在Java語言中需要用重載多個方法來實作。Lua語言中可以直接往後面加。如果有不确定的多個參數,那麼Lua可以讓你使用”…”來替代參數定義。
  • 多個參數傳回和可變參數的函數入參這兩個特性結合在一起,又省去了處理函數傳回解析邏輯的這一開發工作,提升了效率。這些語言中的細節點,或者文法糖,展現出了作者從語言設計之初到完成整個設計過程中的始終不忘的初心——簡潔和輕量。程式員能不愛不釋手嗎?

下面對這幾個特性分别列舉幾個示例:

  1. 多參數傳回:
local test2 = function()
    return 1, "hello", true, { name = "Jack"}
end
 
local a1 = test2()
print("a1:" .. tostring(a1))
local a1, a2, a3 = test2()
print("a1:" .. tostring(a1) .. ", a2:" .. tostring(a2) .. ", a3:" .. tostring(a3)) 
local a1, a2, a3, a4, a5= test2()
print("a1:" .. tostring(a1) .. ", a2:" .. tostring(a2) .. ", a3:" .. tostring(a3) .. ",a4:" .. tostring(a4) .. ",a5:" .. tostring(a5))
 
--output
--[[
a1:1
a1:1, a2:hello, a3:true
a1:1, a2:hello, a3:true,a4:table: 0x7fc52bd087b0,a5:nil
]]           

這個示例展現出的一個使用方法是,你可以根據你關心的某幾個傳回值來決定定義的容納這些數值的變量,其它的不取則預設丢棄。在其他的一些語言中你要首先解析出所有的參數,然後再決定想取某幾個。

2. 可變入參

local handle_result = function(succ, ...)
    if succ then
        local ret_data = ...
        print("result:" .. tostring(succ) .. ", data:" .. ret_data)
    else
        local code, msg = ...
        print("result:" .. tostring(succ) .. ", code:" .. tostring(code) .. ", msg:" .. tostring(msg))
    end
end
 
handle_result(true, "this is true result")
handle_result(false, 404, "not found")
 
--output
--[[
result:true, data:this is true result
result:false, code:404, msg:not found
]]           

根據參數的不同值來決定其他資料是什麼,在可變參數這個特性出現後變得更容易實作了。

3. 兩者的結合

lua子產品加載方法load,标準庫裡面的file,io等提供的API傳回值都遵循共同的一個約定,如果成功則傳回對應内容,如果失敗則傳回nil + 錯誤資訊。看的出來這個約定會根據不同的情況傳回不同的内容,那麼就是作為函數傳回值是動态的,并可能有多個傳回值,作為另一個函數的入參它是動态的,并可能有多個參數傳入。而assert函數的執行會先判斷第一個參數如果不為true(下一節會介紹Lua中除了nil和false的值其它都是true),則會認為第二個參數為錯誤資訊。如果第一個參數校驗為true則傳回函數的執行結果,不作處理。這個assert方法就巧妙的結合了函數的多結果傳回,和函數的可變參數入參實作了斷言。設計細節如此精妙,不經讓人拍案叫絕~

function dofile (filename)
    local f, err = assert(loadfile(filename))
    if f then
        f()
    end
  end
 
  dofile("helloxxx.lua")
  
  --output
  --[[
lua: function_test2.lua:43: cannot open helloxxx.lua: No such file or directory
stack traceback:
    [C]: in function 'assert'
    function_test2.lua:43: in function 'dofile'
    function_test2.lua:49: in main chunk
    [C]: in ?
  ]]           

2.2.5.4、Lua C function

所有在Lua中注冊的函數都必須使用一個相同的原型,該原型就是定義在 lua.h 中lua_CFunction:

/*
** Type for C functions registered with Lua
*/
typedef int (*lua_CFunction) (lua_State *L);           

從C語言的角度看,這個函數隻有一個指向Lua狀态類型的指針作為參數,傳回值是一個整型數,代表壓入棧中的傳回值的個數。是以,該函數在壓入結果前無需清空棧。在該函數傳回後,Lua會自動儲存傳回值的并清空整個棧。

如果我們有多個lua c function,我們希望把這些lua c function放在一個table裡面一起傳回給Lua。Lua語言中提供了luaL_Reg類型,該類型是由兩個字段組成的結構體,這兩個字段分别是函數名(字元串)和函數指針。

luaL_Reg reg[] = {
            {"log", _log},
            {NULL, NULL} /* 哨兵 */
    };           

在上面的例子中,隻聲明了一個函數(log)。數組的最後一個元素永遠是{NULL, NULL},并以此辨別數組的結尾。最後,我們使用函數luaL_newlib聲明一個主函數:

int luaopen_mylib(lua_State *L) {
  luaL_newlib(L,reg);
  return 1;
}           

對函數luaL_newlib的調用會新建立一個表,并使用由數組reg指定的“函數名-函數指針”填充這個新建立的表。當luaL_newlib傳回時,它把這個新建立的表留在了棧中。然後,函數 luaopen_mylib傳回1,表示将這個表傳回給lua。

2.2.6、boolean

boolean類型中true在Lua裡面是除false和nil之外的值,例如:0和空字元串是真

2.2.7、userdata

userdata 表示一個原始的記憶體塊,可以有自己的metatable,這個metatable可以綁定到該userdata(記憶體塊),然後在Lua腳本中通過元表來操作這個userdata。本篇文章下面介紹C接口時會有一個userdata的示例,想了解的可以繼續往下閱讀。

2.2.8、thread

在Lua 中thread代表獨立執行的任務,在Lua中是通過協程來實作的。它和作業系統的線程不同,協程可以在所有的系統上面支援協程,即使是那些不支援線程的系統。這裡就不做詳細的介紹了,想了解的可以可以參考官方的介紹:

2.3、Lua常用到的文法

2.3.1、goto

可以從定義的标簽的地方繼續執行,雖然說goto在程式設計語言中不被人推薦,主要是因為它破壞代碼邏輯的結構。這裡介紹它在Lua腳本中的一個有用的使用方法。

我們知道Lua中的疊代器沒有continue字段,我們可以通過goto來實作continue。Lua中的标簽是通過被兩個“::”包圍的标簽名字元串來表示的,代碼示例如下,他是通過goto讓Lua的代碼執行直接切換到該标簽處:

local arr = {
    "a_value",
    "b_value",
    "c_value"
}
 
local f1 = function()
    for _index, value in ipairs(arr) do
        print("loop key:" .. value)
        print("begin compare:" .. _index)
        if value == 'b_value' then
            print("find b then goto find_task")
            goto find_task
        else
            print("not find and continue1")
            goto continue
        end
    
        ::find_task:: do
            print("find b and return")
            return
        end
 
        ::continue:: do
            print("not find and continue2")
        end
    end
end
 
f1()
 
-- output:
--[[
loop key:a_value
begin compare:1
not find and continue1
not find and continue2
loop key:b_value
begin compare:2
find b then goto find_task
find b and return
]]           

2.3.2、三目運算

Lua中沒有三目運算符,也就是它沒有提供諸如Java中這樣的寫法:String s = a > b ? “s1” : “s2”; 但是Lua有一種替代的寫法:

local s = (a>b) and "s1" or "s2"           

之是以能這樣做是因為Lua中的and和or邏輯運算符也遵循最短路徑。例如這樣的判斷邏輯是不會出錯的(不會因為i的值為0了導緻算數運算符出現除0的錯誤):

i ~=0 and a/i > b --正常運作           

在Lua中使用上面的三目運算方式需要注意一點——lua中nil和false值會判斷為false,其它的任意值會判斷為true,是以我們需要留意and 前面這個判斷不能出現歧義。例如下面這種寫法就不應該用這樣方式實作三目運算符:

local b = 2 > 1 and nil or "here"
print(b)
 
--output
--[[
    here
]]           

2.4、Lua虛拟機

前面1.1.1章節介紹了Lua虛拟機在Lua語言中的依賴關系以及它自身的組成。我們再回顧一下,它包括2個部分:global_state(全局狀态)和lua_state(負責Lua腳本和Lua c function的執行,包括它們之間的資料交換)。 lua_state包括方法調用棧和資料棧。那麼Lua虛拟機在Lua語言中的角色是什麼呢?

首先我們先了解什麼是虛拟機,”虛拟機”就是使用代碼實作的用于模拟計算機運作的程式. 每一門腳本語言都會有自己定義的opcode(operation code,中文一般翻譯為”操作碼”),可以了解為這門程式自己定義的”彙編語言”。我們了解到對于編譯型語言,比如C等,經過編譯器編譯之後生成的都是與目前硬體環境相比對的彙編代碼;而腳本型的語言,經過編譯器的處理之後,生成的就是opcode,再将該opcode放在這門語言的虛拟機中逐個執行.。可見,虛拟機是個中間層,它處于腳本語言前端和硬體之間的一個程式(有些虛拟機是作為單獨的程式獨立存在,例如Java。而Lua由于是一門嵌入式的語言是附着在宿主環境中的,它的Lua虛拟機是C代碼實作的,被編譯在宿主程式中,由宿主程式加載和啟動).

Lua虛拟機的依賴關系圖:

lua對外通過lua.h, lauxlib.h對外公開API接口。虛拟機部分主要包括global_state和 lua_state兩個部分,其中lua_state包括stack資料棧和base_ci、ci指向的函數調用鍊。

lua虛拟機的主要職責是執行位元組碼中的指令,管理全局狀态(global_state)和函數調用鍊狀态(base_ci, ci),在stack中處理指令執行過程中的資料(TValue數組)。

小剛帶你深入淺出了解Lua語言前言1、Lua語言的特點2、Lua語言基礎3、參考文獻

Lua虛拟機具體的工作流程:

步驟一:從C層開始,我們通常加載一個Lua腳本是通過luaL_dofile來執行一個腳本檔案。這個dofile操作包括load_file和pcall兩部分組成。我們看到它的宏定義是這樣的:

(lauxlib.h)
#define luaL_dofile(L, fn) \
  (luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))           

load_file實際是解析Lua腳本檔案,轉換成位元組碼chunk并作為一個C的closure傳回,源碼如下(ldo.c):

static void f_parser (lua_State *L, void *ud) {
  int i;
  Proto *tf;
  Closure *cl;
  struct SParser *p = cast(struct SParser *, ud);
  int c = luaZ_lookahead(p->z);
  luaC_checkGC(L);
  tf = ((c == LUA_SIGNATURE[0]) ? luaU_undump : luaY_parser)(L, p->z,
                                                             &p->buff, p->name);
  cl = luaF_newLclosure(L, tf->nups, hvalue(gt(L)));
  cl->l.p = tf;
  for (i = 0; i < tf->nups; i++)  /* initialize eventual upvalues */
    cl->l.upvals[i] = luaF_newupval(L);
  setclvalue(L, L->top, cl);
  incr_top(L);
}           

步驟二:執行前面步驟通過setclvalue壓在棧裡面的closure

LUA_API int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc) {
  struct CallS c;
  int status;
  ptrdiff_t func;
  lua_lock(L);
  api_checknelems(L, nargs+1);
  checkresults(L, nargs, nresults);
  if (errfunc == 0)
    func = 0;
  else {
    StkId o = index2adr(L, errfunc);
    api_checkvalidindex(L, o);
    func = savestack(L, o);
  }
  c.func = L->top - (nargs+1);  /* function to be called */
  c.nresults = nresults;
  status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func);
  adjustresults(L, nresults);
  lua_unlock(L);
  return status;
}           

步驟三:接下來luaD_call會執行luaD_precall,再到luaV_execute;

luaD_precall: 會檢查CallInfo調用連結清單,如果首次調用,那麼該連結清單為空會建立第一個CallInfo

luaV_execute: 這裡是虛拟機執行代碼的主函數。這裡會一直循環從ci的指令清單中取出指令逐個執行

整體的流程如下:

小剛帶你深入淺出了解Lua語言前言1、Lua語言的特點2、Lua語言基礎3、參考文獻

2.4.1、棧

為啥會有棧呢?它是C和Lua之間調用的通道。當我們想在Lua和C之間交換資料時,會面對兩個問題:

  1. 動态類型和靜态類型體系之間不比對
  2. 自動記憶體管理和手動記憶體管理之間不比對

此外Lua語言不僅能友善的與C/C++互動,而且還能與Java、Fortran、C#等其他語言友善的互動。其次,Lua會做垃圾收集,由于Lua語言引擎并不知道Lua中的一個表可能被儲存在一個C語言變量中,因為它可能會錯誤的認為這個表可以被回收。

棧的存在形式是做為一個StkId,實際是一個TValue的指針。TValue是個包含type資訊和union類型的一個結構。用于表示lua中的資料類型。

CAPI使用索引來引用棧中的元素,第一個被壓入棧的元素索引為1, 第二個被壓入的元素索引為2,依次類推。我們還可以以棧頂為參照,使用負數索引來通路棧中的元素。此時,-1表示棧頂元素(即被最後壓入的元素),-2表示在它之前被壓入棧的元素,依此類推。例如:調用lua_tostring(L, -1)會将棧頂的值作為字元串傳回。

當Lua調用C注冊的方法時,Lua的參數會先壓棧,例如一個Lua調用C的例子:

lua示例:

該Lua調用C方法時傳入了4個參數,分别對應3個string類型,第4個是位元組數組(在lua裡面的字元串可以存儲任意編碼的位元組數組)。在調用到C方法時Lua會配置設定一個新的棧,并把這些參數按照順序逐個壓棧,例如:M.JAVA_CALL_STATIC_CLASS會壓入到棧索引1的位置,type字元串會放在棧索引2的位置,依此類推。

function M.invoke(type, params, chunk)
    params = params or {}
    chunk = chunk or ''
    local json_str = JSON.encode(params)
    local ret = conio.invoke(M.JAVA_CALL_STATIC_CLASS, type, json_str, chunk)
    return ret
end           

在C方法接收到調用時我們想要擷取傳入的參數,可以從棧裡面按照索引逐個彈出參數,例如下面方法體的前5行代碼:

C示例:

static int _invoke(lua_State* L) {
    const char* cls = luaL_checkstring(L, 1);
    const char* call_ = luaL_checkstring(L, 2);
    const char* params_ = luaL_checkstring(L, 3);
    size_t chunk_len;
    const char* chunk_ = luaL_checklstring(L, 4, &chunk_len);
    ...
    lua_pushnumber(L, 1);
    return 1;
}           

我們注意到方法的最後兩行是壓入了一個number類型的數值,同時傳回了1。這是告訴Lua在收到C方法傳回時棧裡面留下了1個傳回結果。Lua腳本可以直接從C方法調用傳回值指派給lua變量。

2.4.1.1、棧平衡

編寫C代碼時保持棧平衡是一個非常好的習慣,不然操作完棧後由于一些資料不小心留在了棧中,在其他邏輯處理時從棧中pop出來的資料就不是我們意料之中的,那麼會出現奇奇怪怪的問題。下面舉一個保持棧平衡的例子:

void onLuaError(lua_State *L) {
    int top = lua_gettop(L);
    const char *msg = lua_tostring(L, 1);
    if (msg == NULL) {  /* is error object not a string? */
        if (luaL_callmeta(L, 1, "__tostring") &&  /* does it have a metamethod */
            lua_type(L, -1) == LUA_TSTRING)  /* that produces a string? */
            ;  /* that is the message */
        else
            msg = lua_pushfstring(L, "(error object is a %s value)",
                                  luaL_typename(L, 1));
 
        error_stat("lua", "exception", msg, lua_tostring(L, -1), NULL, NULL);
        lua_settop(L, top);
        return;
    }
 
    luaL_traceback(L, L, msg, 1);  /* append a standard traceback */
    const char * stack_trace = lua_tostring(L, -1);
    error_stat("lua", "exception", msg, stack_trace, NULL, NULL);
    lua_settop(L, top);
}           

方法調用開始前我們獲得了棧的目前位置,處理了一系列的壓棧操作後我們通過lua_settop方法設定回之前的棧的位置,保持了棧平衡。

2.5、Lua中的機制

正如Lua提到它的設計原則遵循盡量使用機制來代替規則約定,Lua内置了一些非常有用的機制來幫助管理代碼子產品,記憶體配置設定等這些繁瑣,且容易出錯的内容。

2.5.1、子產品和包管理機制

談到子產品和包,我們一般想到的是我們對代碼邏輯的劃分,然後這些劃分之後的獨立子產品,能支援不同路徑動态查找、按需加載和解除安裝、更新替換等。Lua中的子產品包括:Lua腳本子產品和使用C接口擴充3方子產品。用一個圖描述一下如下:

小剛帶你深入淺出了解Lua語言前言1、Lua語言的特點2、Lua語言基礎3、參考文獻

流程描述:

  1. 首先,函數require在表package.loaded中檢查子產品是否已經被加載。如果子產品已經加載,函數require就傳回相應的值。是以,一旦一個子產品被加載過,後續對于同一子產品的所有require調用都将傳回同一個值,而不會再運作子產品裡面的代碼。
  2. 如果package.loaded裡面沒有(即沒有加載),那麼函數require會去檢查package.preload,這個裡面定義了子產品名和加載函數的映射(這裡再統一一下概念加載函數是一個函數類型,可以被執行,能傳回子產品需要傳回給外部的公開資料,由子產品自己決定傳回内容,通常會是一個table,也可以是其他值。如果傳回nil,為了保證package.loaded裡面能記錄該子產品被加載過,會填補一個true值,例如:package.loaded[子產品名]=true )。使用到的場景有:當我們使用靜态連結到Lua的C庫,可以将其luaopen_函數注冊到preload裡面,這樣luaopen_函數隻有當使用者加載這個子產品時才會被調用。
  3.  前面2部沒有找到時,Lua會去搜尋該子產品的檔案,首先嘗試搜尋lua檔案路徑。該搜尋路徑是由變量package.path指定。通過該搜尋路徑如果找到了相應的檔案,那麼就用函數loadfile進行加載,該函數的傳回結果是加載函數。接下來簡單介紹一下package.path,在ISO C并沒有目錄的概念。是以package.path使用的是一組模版(template),例如:
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua           

自定義package.path示例:

static void register_lua_search_path(lua_State* L, const std::string& path)
{
  lua_getglobal(L, "package");
  lua_getfield(L, -1, "path");
  std::string cur_path = luaL_checkstring(L, -1);
  cur_path.append(";");
  cur_path.append(path);
  lua_pop(L, 1);
  lua_pushstring(L, cur_path.c_str());
  lua_setfield(L, -2, "path");
  lua_pop(L, 1);
}
 
static int init_lua_path(lua_State* L)
{
  register_lua_search_path(L,  ".\\..\\..\\my_lua\\?.lua" + ";" +  + ".\\..\\..\\my_lua\\?\\?.lua");
  return 0;
}           
  1. 如果找不到指定子產品的Lua檔案,那麼它就會搜尋相應名稱的C标準庫(此時,搜尋路徑由變量package.cpath指定)。如果找到一個C标準庫,則會使用底層函數package.loadlib來進行加載,這個底層函數會查找名為luaopen_子產品名的函數。此時的加載函數就是loadlib的執行結果,也就是一個被表示為Lua函數的C語言函數luaopen_子產品名。
  2. 以上都是Lua系統的預設的查找方式,如果程式有一些特别的查找方式,例如需要解開一個zip包後拿到檔案等,Lua提供了一個搜尋器的概念。Lua提供的搜尋器是由package.searchers提供。函數require會傳入子產品名并調用該清單中的每一個搜尋器直到它們其中的一個找到了指定子產品的加載器(即能正常傳回加載函數)。如果所有的搜尋器都被調用完後還是找不到,那麼函數require就抛出一個異常。自定義searcher搜尋器比較簡單,我們隻需要往package.searchers表裡面插入一個function,然後該function的實作需要接收一個子產品名的參數,并能傳回一個加載函數。示例如下:
-- Lua 5.2 package.searchers
    local function loader(name)
        local ret, content, modpath = pcall(read_with_module_name, name)
        assert(ret, "file not found " .. modpath .. '@' .. name)
        local ret2 = assert(load(content, modpath, 'bt'), "source error ".. modpath)(name)
        if ret2 then
            return ret2
        end
    end
 
    table.insert(package.searchers, function(_name)
        return loader
    end)           

2.5.1.1、Lua子產品和C子產品

我們先搞清楚子產品是啥。從使用者的觀點來看,一個子產品(module)就是一些代碼(要麼是Lua語言編寫的,要麼是C語言編寫的),這些代碼可以通過函數require加載,然後建立和傳回一個表。這個表就像是某個命名空間,其中定義的内容是子產品中導出的東西,比如函數和常量。

以Lua舉例,通常Lua的子產品是什麼樣的:

lua module例子:

local a = 0
local M = {}
M.get_a = function()
    a = a + 1
return a
end
 
return M           

外部調用該子產品示例:

local a = require "test_module.module1"
print(a.get_a())           

上面例子可以看出我們在module的代碼中定義了一個table M,這個table用于定義該子產品可以公開的一些内容,例如子產品中定義了get_a方法,然後該子產品将M表作為require函數加載過程中的加載函數執行結果傳回,按照上面講解的require機制的流程,該結果也會存儲一份到package.loaded,用于其他代碼require時能直接傳回,而不需要再次執行子產品代碼。

這種Lua子產品的包裝方式是Lua推薦的一種比較簡單的方式,從子產品的包裝我們可以清晰的知道定義在M表結構裡面的方法和常量是子產品希望對外的,而沒有在M裡面local變量則可以認為是私有變量,他們是不希望對外的。例如上面示例中的local a。

另外有時我們遇到一種場景,就是M中定義的函數通路另一個私有函數時,該函數已經定義在檔案的末尾,也就是沒有提前申明,該M裡面的函數就找不到那個私有函數,如果把這個私有函數挪動位置放在最前面那麼會動到之前的代碼,這樣就不是很好。我們可以把私有函數前面也加上M.這樣可以不挪動私有函數的位置進而被定義在前面的M.的函數通路。但是為了表明該函數是私有的,我們可以在私有名稱的最前面或者最後加上一個下劃線,用于約定區分全局名稱。

接下來介紹一下C子產品的包裝示例:

Lua提供了4種方式:

方式1: 動态連結庫的方式。路徑中添加“子產品名”.so(Android環境),so中定義了函數luaopen_子產品名,這樣Lua可以在腳本中直接require “子產品名”得到luaopen_子產品名傳回的值。下面舉個例子:

local cjson = require "cjson"
local json_str = [[{"a":"hello lua lib decode!"}]]
local decode_ret = cjson.decode(json_str)
print(decode_ret.a)
 
local json_tbl = { a = "hello lua lib encode!"}
local encode_str = cjson.encode(json_tbl)
print(encode_str)
 
--output:
--[[
hello lua lib decode!
{"a":"hello lua lib encode!"}
]]           

上面的例子中我們将Lua的cjson庫下載下傳下來(下載下傳位址:

),編譯後生成cjson.so檔案,然後放在Lua工程的目錄下面命名也是cjson.so,這命名和require “cjson”的cjson是相同的,從上面的加載機制我們了解到require函數會通過模版的方式在目前的路徑下面找到cjson.so,并加載進來,同時查找luaopen_cjson方法并執行它得到的結果傳回Lua同時存放在package.loaded。從上面的示例我們可以看到Lua腳本就能正确調用到cjson子產品暴露的decode和encode方法了。

我們簡單看一下cjson的luaopen_cjson方法:

int luaopen_cjson(lua_State *l)
{
    lua_cjson_new(l);
 
#ifdef ENABLE_CJSON_GLOBAL
    /* Register a global "cjson" table. */
    lua_pushvalue(l, -1);
    lua_setglobal(l, CJSON_MODNAME);
#endif
 
    /* Return cjson table */
    return 1;
}           

我們看到luaopen_cjson方法傳回了一個table,這個table就是lua腳本獲得的值。lua中調用decode和encode就是調用的這個table裡面的對應的兩個方法了。

此時我們把子產品名改成cjson2會發生什麼呢?就會報一個找不到的錯誤了:

--[[
ua: error loading module 'cjson2' from file './cjson2.so':
    dlsym(0x7fda99c09c90, luaopen_cjson2): symbol not found
stack traceback:
    [C]: in ?
    [C]: in function 'require'
    test_so.lua:1: in main chunk
    [C]: in ?
]]           

方式二:添加到lua标準庫清單。

如果解釋器不支援動态連結,就必須連同新庫一起從新編譯Lua語言。除了重新編譯,還需要以某種方式告訴獨立解釋器,它應該在打開一個新的狀态時打開這個庫。一個簡單的做法是把luaopen_子產品名 添加到由luaL_openlibs打開的标準庫清單中,這個清單位于檔案linit.c中。我們看到linit.c的代碼很少,直接貼出來:

static const luaL_Reg loadedlibs[] = {
  {"_G", luaopen_base},
  {LUA_LOADLIBNAME, luaopen_package},
  {LUA_COLIBNAME, luaopen_coroutine},
  {LUA_TABLIBNAME, luaopen_table},
  {LUA_IOLIBNAME, luaopen_io},
  {LUA_OSLIBNAME, luaopen_os},
  {LUA_STRLIBNAME, luaopen_string},
  {LUA_MATHLIBNAME, luaopen_math},
  {LUA_UTF8LIBNAME, luaopen_utf8},
  {LUA_DBLIBNAME, luaopen_debug},
#if defined(LUA_COMPAT_BITLIB)
  {LUA_BITLIBNAME, luaopen_bit32},
#endif
  {NULL, NULL}
};
 
 
LUALIB_API void luaL_openlibs (lua_State *L) {
  const luaL_Reg *lib;
  /* "require" functions from 'loadedlibs' and set results to global table */
  for (lib = loadedlibs; lib->func; lib++) {
    luaL_requiref(L, lib->name, lib->func, 1);
    lua_pop(L, 1);  /* remove lib */
  }
}           

它的實作是将将一些标準庫的加載函數的傳回值通過luaL_requiref(同lua腳本中的require)儲存到package.loaded中。這樣lua中require時能直接得到緩存的傳回值了。這裡的傳回值是公開的表結構。

方式三:使用luaL_requiref

從方式二中我們已經知道可以使用luaL_requiref将子產品添加到package.loaded中。那麼我們可以不用那麼麻煩去修改lua的c api了。我們可以直接使用luaL_requiref方法來達到此目的。舉個例子:

luaL_requiref(mL, "_mylib", luaopen__mylib, 1);
lua_pop(mL, 1);           

我們在C方法的某個初始化階段調用一下上面的方法,把自己定義的lua c function通過table注冊到package.loaded裡面,讓lua能直接使用。

C代碼中注冊lua c function,用到了luaL_Reg結構,它的結構如下,包含字元串類型的name和一個lua_CFunction。

typedef struct luaL_Reg {
  const char *name;
  lua_CFunction func;.
} luaL_Reg;           

方式四:儲存到全局表

我們再回顧一下cjson的luaopen_cjson加載函數的實作:

int luaopen_cjson(lua_State *l)
{
    lua_cjson_new(l);
 
#ifdef ENABLE_CJSON_GLOBAL
    /* Register a global "cjson" table. */
    lua_pushvalue(l, -1);
    lua_setglobal(l, CJSON_MODNAME);
#endif
 
    /* Return cjson table */
    return 1;
}           

我們看到有個ENABLE_CJSON_GLOBAL宏定義,在這個宏裡面它先通過lua_pushvalue将位置-1(即棧頂)的值複制了一份并壓棧,然後通過lua_setglobal從棧頂pop出那個複制的表然後設定到全局表中(即_G中)。我們從上面的講解中知道_G會指派給Lua匿名函數的上值_ENV中,所有對全局變量的通路會通路到_ENV,是以Lua就可以直接通過鍵值CJSON_MODNAME通路到剛剛push到全局表的那個表了。

在Lua5.1版本也可以使用luaL_register,按照官方對該方法的描述,如果帶了libname參數,那麼會在全局表和package.loaded裡面注冊那些方法。

2.5.1.2、子子產品

Lua腳本也是支援具有層次結構的子產品名的,例如當我們 require “a.b”時,lua會自動将點符号自動轉化為作業系統的分隔符,這裡之前package.path的模版會被替換為以下的路徑搜尋清單:

模版:./?.lua;/usr/local/lua/?.lua;/usr/local/lua/?/init.lua

搜尋清單:

  1. ./a/b.lua
  2. /usr/local/lua/a/b.lua
  3. /usr/local/lua/a/b/init.lua

C 子子產品:舉個例子,我們require C層的a.b.c,會寫成 require “a.b.c”,搜尋器會搜尋檔案a(例如a.so),然後Lua會在該庫中搜尋對應的加載函數luaopen_a_b_c。

2.5.1.3、子產品的不同版本

我們可以為每個so的不同版本指令不同so,例如我們把cjson的so改名為cjson-v5.so,然後require時可以用這個帶版本的so的名字來查找so檔案,不過會用不帶版本号的luaopen_cjson來查找加載函數。是以我們可以通過這種方式來釋出cjson的不同版本的so庫了。上面方式一加載動态庫so檔案的代碼修改後示例如下:

local cjson = require "cjson-v5"
 
local json_str = [[{"a":"hello lua lib decode!"}]]
local decode_ret = cjson.decode(json_str)
print(decode_ret.a)
 
--output
--[[
    hello lua lib decode!
]]           

2.5.1.4、解除安裝子產品

參考這篇

,我們無法簡單的解除安裝一個C子產品。不過我們可以簡單的解除安裝一個lua子產品。我們知道lua子產品的加載函數結果儲存在package.loaded中,我們可以顯示的将package.loaded[子產品名] = nil的方式來實作解除安裝。這樣在下一次require這個子產品時會重新查找和執行該子產品的加載函數了,并将新的傳回值儲存在package.loaded中。

2.5.2、元機制

通常,Lua語言中的每種類型的值都有一套可預見的操作集合。例如,我們可以将數字相加,可以連接配接字元串,還可以在表中插入鍵值對等。但是,我們無法将兩個表相加,無法對函數作比較,除非使用元表。

元表可以修改一個值在面對一個未知操作時的行為,例如,假設a和b都是表,那麼可以通過元表定義來實作如何計算表達式 a + b。Lua中識别到試圖将兩個表相加時,它會檢查兩者之一是否有元表(metatable),且該元表中是否有_add字段。如果Lua語言找到了該字段,就調用該字段對應的值,即所謂的元方法(metamethod),用于計算表的和。

注意:lua 腳本中的setmetatable隻能給表設定元表,如果需要改變其他類型的元表隻能在C語言中實作

下面就以兩個表相加寫一個代碼示例,兩個表相加時我們想把他們兩個集合合并在一起:

local tb1 = {1,2,3}
local tb2 = {'a','b','c'}
 
local mt = {
    __add = function(a, b)
        local result = {}
        for _index, value in ipairs(a) do
            table.insert(result, value)
        end
 
        if type(b) == "table" then
            for _index, value in ipairs(b) do
                table.insert(result, value)
            end
        else
            table.insert(result, b)
        end
        
        return result
    end
}
setmetatable(tb1, mt)
setmetatable(tb2, mt)
 
local b = tb1 + tb2
for _ind, val in ipairs(b) do
    print("index:" .. _ind .. ", value:" .. tostring(val))
end
 
--output
--[[
index:1, value:1
index:2, value:2
index:3, value:3
index:4, value:a
index:5, value:b
index:6, value:c
]]           

同樣的上面的實作也滿足一個表和一個數值相加:

local result2 = tb1 + 111
for _ind, val in ipairs(result2) do
    print("index:" .. _ind .. ", value:" .. tostring(val))
end
 
--output
--[[
index:1, value:1
index:2, value:2
index:3, value:3
index:4, value:111
]]           

Lua預置了一些元方法定義,包括:

  • 算術運算元方法:__add, __div(除法), __mod(取模)等
  • 關系運算相關的元方法:__eq(等于),__lt(小于), __le(小于等于)
  • 庫定義相關的元方法:__tostring, __metatable(保護元表)等

Lua語言會按照如下的步驟來查找元方法:如果第一個值有元表且元表中存在所需的元方法,那麼Lua語言就使用這個元方法,與第二個值無關(如上面的代碼示例);如果第二個值有元表且元表中存在所需的元方法,Lua語言就使用這個元方法;否則Lua語言就抛出異常。

接下來我們介紹一下_metatable元方法,它是用于保護元表。假設想要保護我們的集合,就要使使用者既不能看到也不能修改集合的元表。如果在元表中設定__metatable字段,那麼getmetatable會傳回這個字段的值,而setmetatable則會引發一個錯誤,我們繼續舉一個例子:

local protected_tbl = {}
 
mt = {
    __metatable = "it's protected table"
}
 
setmetatable(protected_tbl, mt)
 
-- modify metatable
setmetatable(protected_tbl, {})
 
--output
--[[
    lua: meta_methods_test.lua:66: cannot change a protected metatable
stack traceback:
    [C]: in function 'setmetatable'
    meta_methods_test.lua:66: in main chunk
    [C]: in ?
]]           

2.5.2.1、元表的關鍵字__index和__newindex

lua中當通路一個表中不存在的字段時會傳回nil。這是正确的,但不是完整的真相。實際上,這些通路會引發解釋器查找一個名為__index的元方法。如果沒有這個元方法,那麼像一般情況下一樣,結果就是nil; 否則,則由這個元方法來提供最終的結果。

如果我們希望在通路一個表時不調用__index元方法,那麼可以使用函數rawget,調用rawget(t, i)會對表t 進行原始通路,即在不考慮元表的情況下對表進行簡單的通路。

元方法__newindex與__index類似,不同之處在于前者用于表的更新而後者用于表的查詢。當對一個表中不存在的索引指派時,解釋器就會查找__newindex元方法。同樣的它也有一個原始函數允許我們繞過元方法:rawset(t, k, v),它等價于t[k] = v,但不涉及任何元方法。

在隻讀表的應用:

以一個例子來實踐一下元表的__index和__newindex操作,熟悉一下lua中強大的元機制:

local readonly_table = {
    ["a"] = 1, ["b"] = 2
}
 
local mt = {
    __index = readonly_table,
    __newindex = function(t, k, v)
        error("attempt to update a readonly table with key:" .. tostring(k) .. ", value:" .. tostring(v))
    end
}
 
local readonly_t = setmetatable({}, mt)
 
print(readonly_t["a"])
readonly_t["a"] = 123
 
--output
--[[
1
lua: table_readonly.lua:8: attempt to update a readonly table with key:a, value:123
stack traceback:
    [C]: in function 'error'
    table_readonly.lua:8: in metamethod 'newindex'
    table_readonly.lua:15: in main chunk
    [C]: in ?
 
]]           

這個示例中readonly_t本身是一張空表,這樣對空表的取值和指派操作都不會找到對應的鍵值key,那麼必然會走到元表的__index和__newindex,而我們在示例中對__index放開了,存放了一些資料,這樣readonly_t就可以讀取到資料裡面的值。但是當我們想要修改readonly_t的值時,也隻會觸發到__newindex,而這裡我們會直接傳回一個錯誤。從示例的執行結果看出達到了這個目的。我們看到readonly_t[“a”]能正确傳回1,而readonly_t[“a”] = 123會提示錯誤

2.5.2.2、userdata類型設定元表

Lua裡面userdata類型表示的是一個C指針。我們以一個示例示範如何在Lua中使用C代碼中的一個結構體,要知道Lua中沒有結構體這樣的直接對應的數值類型,但是有userdata,可以表示任意一塊C的記憶體空間,要操作這樣一個記憶體空間,Lua提供了一種方法可以為一個數值設定元表,然後我們在元表中可以定義方法來操作這個記憶體空間了。先了解幾個關鍵的方法:

void lua_setmetatable (lua_State *L, int index);           

這個方法是從棧頂彈出一個table類型的數值,然後把它設定為指定index索引位置的數值作為它的元表。我們前面介紹過Lua資料棧裡面的存放的都是TValue類型的結構,這個TValue實際是一個union類型,裡面表示了Lua裡面所有基本類型。是以也就是說lua_setmetatable可以為任意的Lua數值類型設定一個元表。在元表中我們可以定義_index鍵值,這裡示例中是一個table(示例代碼參考的這篇

文章

)。

// foo.c
 
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <limits.h>
 
#define BITS_PER_WORD (CHAR_BIT * sizeof(int))
#define I_WORD(i)     ((unsigned int)(i))/BITS_PER_WORD
#define I_BIT(i)      (1 << ((unsigned int)(i)%BITS_PER_WORD))
 
typedef struct NumArray {
    int size;
    unsigned int values[1];
} NumArray;
 
int newArray(lua_State* L)
{
    int i, n;
 
    n = luaL_checkint(L,1);
 
    luaL_argcheck(L, n >= 1, 1, "invalid size.");
 
    size_t nbytes = sizeof(NumArray) + I_WORD(n - 1) * sizeof(int);
 
    NumArray* a = (NumArray*) lua_newuserdata(L,nbytes);
 
    a->size = n;
 
    for (i = 0; i < I_WORD(n - 1); ++i)
        a->values[i] = 0;
 
    luaL_getmetatable(L, "myarray");
 
    lua_setmetatable(L, -2);
 
    return 1;
}
 
int setArray(lua_State* L)
{
    //1. Lua傳給該函數的第一個參數必須是userdata,該對象的元表也必須是系統資料庫中和myarray關聯的table。
    //否則該函數報錯并終止程式。
    NumArray* a = (NumArray*)luaL_checkudata(L,1,"myarray");
    int index = luaL_checkint(L,2) - 1;
 
    luaL_checkany(L,3);     // there are 3 arguments
    luaL_argcheck(L,a != NULL,1,"'array' expected.");
    luaL_argcheck(L,0 <= index && index < a->size,2,"index out of range.");
 
    if (lua_toboolean(L,3))
        a->values[I_WORD(index)] |= I_BIT(index);
    else
        a->values[I_WORD(index)] &= ~I_BIT(index);
 
    return 0;
}
 
int getArray(lua_State* L)
{
    NumArray* a = (NumArray*)luaL_checkudata(L,1,"myarray");
    int index = luaL_checkint(L,2) - 1;
    luaL_argcheck(L, a != NULL, 1, "'array' expected.");
    luaL_argcheck(L, 0 <= index && index < a->size,2,"index out of range");
    lua_pushboolean(L,a->values[I_WORD(index)] & I_BIT(index));
    return 1;
}
 
int getSize(lua_State* L)
{
    NumArray* a = (NumArray*)luaL_checkudata(L,1,"myarray");
    luaL_argcheck(L,a != NULL,1,"'array' expected.");
    lua_pushinteger(L,a->size);
    return 1;
}
 
int array2string(lua_State* L)
{
    NumArray* a = (NumArray*)luaL_checkudata(L,1,"myarray");
    lua_pushfstring(L,"array(%d)",a->size);
    return 1;
}
 
static luaL_Reg arraylib_f [] = {
    {"new", newArray},
    {NULL, NULL}
};
 
static luaL_Reg arraylib_m [] = {
    {"set", setArray},
    {"get", getArray},
    {"size", getSize},
    {"__tostring", array2string}, //print(a)時Lua會調用該元方法。
    {NULL, NULL}
};
 
int luaopen_foo(lua_State* L)
{
    //1. 建立元表,并将該元表指定給newArray函數新建立的userdata。在Lua中userdata也是以table的身份表現的。
    //這樣在調用對象函數時,可以通過驗證其metatable的名稱來确定參數userdata是否合法。
    luaL_newmetatable(L,"myarray");
    lua_pushvalue(L,-1);
 
    //2. 為了實作面對對象的調用方式,需要将元表的__index字段指向自身,同時再将arraylib_m數組中的函數注冊到
    //元表中,之後基于這些注冊函數的調用就可以以面向對象的形式調用了。
    //lua_setfield在執行後會将棧頂的table彈出。
    lua_setfield(L, -2, "__index");
 
    //将這些成員函數注冊給元表,以保證Lua在尋找方法時可以定位。NULL參數表示将用棧頂的table代替第二個參數。
    luaL_register(L, NULL, arraylib_m);
 
    //這裡隻注冊的工廠方法。
    luaL_register(L,"testuserdata",arraylib_f);
 
    return 1;
}           

luaL_checkudata用于校驗userdata是否是我們預想的類型。Lua中通過為userdata綁定一個元表,然後通過C方法檢查userdata是否有指定的元表的方式來實作的。

lua中使用

require "foo"
 
local array = testuserdata.new(100)
 
print(array:size())     -- 100
 
for i=1,100 do
    array:set(i, i%5 == 0)
end
 
for i=1,100 do
    print(array:get(i))
end           

下面解釋一下這個代碼:

  1. require “foo”,會走Lua的子產品加載機制(前面介紹了),找到foo動态庫,然後執行luaopen_foo加載函數。
  2. c 中 luaopen_foo函數調用luaL_register(L,”testuserdata”,arraylib_f);在全局表中注冊了testuserdata鍵值,以及和它對應的lua c function清單
  3. lua 中 testuserdata. new(100) 中testuserdata在lua轉換成位元組碼時被識别為一個全局變量(前面介紹過)。是以會自動添加字首,結果為_ENV.testuserdata.new(100)。我們現在知道了_ENV對應全局表中的_G。那麼這段代碼實際的意思是會到全局表中查找testuserdata關鍵字,那麼此時的new方法就是C裡面的newArray方法了
  4. newArray方法會建構一個NumArray結構體,并綁定元表myarray
  5. lua代碼中接下來array:size()方法調用又是怎麼樣呢?“:”符号的方式和 “.” 使用方式不同, 它轉化為”.”的方式會變為array.size(array), 也就是它的第一個參數為調用的table它自己。那麼這段代碼的意思是調用array這個userdata類型數值的 size方法,該方法會在元表中_index中查找于是找到了鍵值size對應的之前注冊的getSize 這個 lua c function。
  6. getSize C function中首先會在棧頂找到userdata它自己(前面介紹的array.size(array)),然後轉換為 NumArray 指針指派給變量a。對應的代碼是 —— NumArray* a = (NumArray*)luaL_checkudata(L,1,”myarray”);
  7. 最後從該結構執行個體中取出size成員,并把該結果壓棧傳回給lua。return 1 是告訴Lua棧裡面留了一個值給它,lua中調用傳回後可以直接指派給Lua變量。

注意:Lua調用C時會在Lua虛拟機的資料棧中配置設定一塊新的範圍作為它們之間互動的棧空間。調用到C時棧裡面的索引從1開始依次存放了Lua調用時傳遞過來的參數,c function中可以直接從棧中pop出對應的參數使用。

2.5.2.3、繼承

Lua中的table和元機制的确很強大。它們結合在一起可以實作其它語言中的繼承的行為。不過它的繼承和Java語言的繼承不同,它和javascript類似,是基于原型的。簡單說就是隻有對象,沒有類;對象繼承對象,而不是類繼承類。

“原型對象”是基于原型語言的核心概念。原型對象是新對象的模闆,它将自身的屬性共享給新對象。一個對象不但可以享有自己建立時和運作時定義的屬性,而且可以享有原型對象的屬性。

寫一個Lua中繼承的典型的例子:

local Account
Account = {
}
 
function Account:get_name()
    print("return name: " .. tostring(self.name))
end
 
function Account:new(o)
    o = o or {}
    self.__index = self
    setmetatable(o, self)
    return o
end
 
local obj1 = Account:new({name = "Tom"})
local obj2 = Account:new({name = "Jerry"})
obj1:get_name()
obj2:get_name()
--output
--[[
return name: Tom
return name: Jerry
]]           

這個示例中定義了一個table Account作為父對象,裡面定義了一個公共方法get_name,用于列印name屬性。然後在Account裡面定義了new方法,這個方法裡面給傳進來的table執行個體(子對象)設定一個元表(元表就是Account自己),這個元表的__index也設定為Account自己,這樣在子對象調用get_name時(obj1:get_name()和obj2:get_name())會通路到父對象的方法了。這個示例展示了繼承的一個特性——多個對象共享行為。

作為一個“類”,除了多對象共享行為,它還需要具備其它的特點,這裡總結為3個:

  • 多個對象共享行為
  • 繼承
  • 私有性

Lua是基于原型的繼承,可以實作對象繼承對象,代碼示例如下:

-- 基于原型的繼承,對象繼承對象
local TomClass = Account:new({name = "Tom"})
 
function TomClass:get_name()
    print("Tom say his full name is :" .. tostring(self.name))
end
 
local tom = TomClass:new({name = "Tomcat"})
tom:get_name()
 
--output
--[[
    Tom say his full name is :Tomcat
]]           

上面這個示例中,基于Account我們建構了TomClass執行個體, 然後tom執行個體是基于TomClass。我們看到在這個示例中TomClass執行個體複寫了Account的get_name方法,并修改了print列印的内容,當我們使用TomClass new一個新的執行個體時此時調用get_name方法不再使用Account中的get_name,而是找到了TomClass裡面的get_name了。

2.5.2.3.1、多重繼承

再來看Lua中一個多重繼承的示例:

local function search(k, plist) 
for i=1, #plist do
    local v = plist[i][k]
    if v then return v end
end
end
 
function createClass(...)
    local c = {}
    local parents = {...}
 
    setmetatable(c, {__index = function(t, k)
        return search(k, parents)
    end})
 
    -- c 做為其執行個體的元表
    c.__index = c
 
    function c:new(o)
        o = o or {}
        setmetatable(o, c)
        return o
    end
 
    return c
end
 
local Account = {}
function Account:get_account()
    return self.account
end
 
function Account:set_account(account)
    self.account = account
end
 
local Named = {}
function Named:get_name()
    return self.name
end
 
function Named:set_name(name)
    self.name = name
end
 
local NamedAccount = createClass(Account, Named)
local account = NamedAccount:new({name='Tom', account='tom123'})
print("account name:" .. account:get_name() .. ", account:" .. account:get_account())
 
--output
--[[
    account name:Tom, account:tom123
]]           

在這個示例中我們定義了兩個table——Account, Named,用于多重繼承的2個“父類”,它們分别提供了方法操作各自的屬性account和name。

提供了一個createClass方法,這個方法傳回一個table,它包含這些父類的清單,同時它提供了new方法用于建構新的子對象。子對象設定該table為它的元表,并指定__index也為這個table,讓子對象和多個父對象關聯起來。這個table也設定了一個元方法,它的__index我們設定了一個function,這個函數會調用search方法在父類清單中(parents)查找對應的方法名。通過這種方式實作了多重繼承的目的。

我們看到子對象account在使用方法get_name和get_account時分别使用到父對象的方法來執行的。

2.5.2.3.2、私有性

繼承中另一個重要的特性是私有性。總結起來有以下3種方式來保證私有性:

  • 在一個table傳回時隻傳回需要公開的方法或字段。可以是兩個table的方式,一個table放對象狀态,一個table放操作;也可以是傳回一個方法(單方法對象)。
  • 繼承出來的table它的元表不實作__newindex,則自己的屬性修改不會影響到公共的父類,實作了私有性
  • 對于共享的資料,可以用對偶表示法實作私有性,即用對象唯一位址來存儲資料,隻有持有該對象才能通路對應的資料

示例一:傳回公開方法和字段,屏蔽私有字段的方式:

function newAccount(initName, initage)
    local _self = {name = initName, age = initage}
 
    local print_name = function()
        print("my name is :" .. tostring(_self.name))
    end
 
    local add_age = function()
        _self.age = _self.age + 1
        print("now my age is:" .. tostring(_self.age))
    end
 
    return {
        printName = print_name,
        addAge = add_age
    }
end
 
local account = newAccount("Tom", 18)
account.printName()
account.addAge()
 
--output
--[[
my name is :Tom
now my age is:19
]]           

上面示例中newAccount方法傳回了一個新的對象,它公開了兩個方法:printName和addAge,這個新的對象綁定了一個上值(upvalue)

_self,它也是一個table,裡面放了2個屬性——name和age,外部在使用這個對象時隻能通過公開的方法來操作這個私有table(_self)的值。這樣有效的保證了_self的私有性。

示例二:對偶表示法

local my_companys = {}
 
local Account = {
    
}
 
function Account:new(o, company)
    o = o or {}
    self.__index = self
    setmetatable(o, self)
    my_companys[o] = company
 
    return o
end
 
function Account:print()
    print("my name is:" .. tostring(self.name) .. ", company:" .. tostring(my_companys[self]))
end
 
local a1 = Account:new({name = "Tom"}, "alibaba")
local a2 = Account:new({name = "Jerry"}, "Tencent")
 
a1:print()
a2:print()
 
--output
--[[
my name is:Tom, company:alibaba
my name is:Jerry, company:Tencent
]]           

對偶表示法将對象私有屬性值儲存在以該對象為鍵值的一張表中統一管理。後續需要通路這些私有屬性時必須持有相應的對象才能通路。這樣有效的保證了私有性。

2.5.3、lua中的錯誤處理機制

由于Lua是一門可擴充的嵌入型的腳本語言,所有的Lua操作起源于宿主的C代碼,通過lua_pcall來調用的,任何的Lua執行錯誤都會傳回到C層,我們可以在這一層做一些錯誤的處理。下面會介紹一種全局捕獲Lua異常的做法。

lua中關于錯誤的處理有以下的方法:

  • 産生一個錯誤:Lua腳本中使用 error ,在C層可以用 lua_error ,也有一個輔助庫的 luaL_error (可以列印格式化的字元串)
  • assert(v [, message]): 斷言,如果v是false(前面提到在lua裡面,false和nil都認為是false)就會産生一個錯誤。不然的話就會傳回v的所有傳回資訊。message是出錯時的錯誤資訊,如果沒有,則預設為“assertion failed”
  • pcall xpcall :  pcall (f, arg1, ···)和xpcall (f, err)。都是在保護模式下執行function。這意味着如果f中執行有異常pcall會抓住這個異常然後傳回錯誤資訊。xpcall和pcall的差異在于xpcall可以帶一個err handler function。如果f中有異常,xpcall能捕獲異常,并把錯誤資訊傳給err function來處理
  • lua_atpanic : 如果應用調用了Lua API中的函數,就可能發生錯誤。Lua語言通常通過長跳轉來提示錯誤,但是如果沒有相應的setjmp, 解釋器就無法進行長跳轉。此時,API中的任何錯誤都會導緻Lua調用緊急函數(panic function),當這個函數傳回後,應用就會退出。我們可以通過函數lua_atpanic來設定自己的緊急函數,但作用不大。可以用做統計收集錯誤資訊。
  • debug.traceback LuaL_traceback : 會傳回一個崩潰時調用棧的字元串類型資訊。
  • luaL_argerror, luaL_check*,luaL_typeerror, luaL_typeerror: 這些方法也是用于抛出一個錯誤,不過是關于參數或類型檢查的。

2.5.3.1、C語言的setjmp機制

C語言沒有C++或Java的異常機制,但可以通過setjmp/longjmp實作類似的效果,Lua的pcall就是利用的setjmp來實作異常捕獲的。

舉個C代碼實作的例子:

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
 
jmp_buf env;
 
int my_func(int a, int b) {
    if (b == 0) {
        printf("do not allow division by 0\n");
        longjmp(env, 1);
    }
    return a / b;
}
 
int main(int argc, char const *argv[]) {
    int res = setjmp(env);
    if (res == 0) {
        printf("return from setjmp\n");
        my_func(10, 0);
    } else {
        printf("return from longjmp: %d\n", res);
    }
    return 0;
}           

輸出結果為:

return from setjmp
do not allow division by 0
return from longjmp: 1           

代碼執行流程如下:

  • 使用setjmp儲存目前執行環境到jmp_buf,然後預設傳回0。
  • 程式繼續執行,到某個地方調用longjmp,傳入上面儲存的jmp_buf,以及另一個值。
  • 此時執行點又回到調用setjmp的傳回處,且傳回值變成longjmp設定的值。

在Lua中使用pcall時也是同樣的效果,任意在pcall中的異常最終會通過longjmp跳轉到之前調用pcall的位置,然後帶上出錯的資訊。我們代碼可以在pcall傳回時判斷執行的結果來處理異常時的邏輯。

2.5.3.2、Lua中的全局錯誤捕獲

有了上面的知識作為鋪墊我們不難實作Lua腳本中的全局錯誤捕獲了。我們知道所有的Lua操作起源于宿主的C代碼,是以如果要全局捕獲lua腳本的異常當然要在C層來處理了。

開始着手前我們再自己看下C層的lua_pcall 函數定義(

):

int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc);           

我們重點看下第四個參數errfunc,這個值是個整形值它代表錯誤處理函數在資料棧中的位置。我們可以利用這個錯誤處理函數來接收pcall傳回的錯誤資訊。

下面是一個Android裡面通過JNI調用到C層,然後調用Lua API的lua_pcall來執行Lua二進制chunk位元組碼,并捕獲異常的代碼。

Java_com_conio_mysdk_nativeDoBuffer(JNIEnv *env, jclass _type, jlong luaState, jbyteArray buff , jlong sz , jstring n)
{
    lua_State* L = (lua_State*)luaState;
 
    lua_pushcfunction(L, msghandler);
    jbyte * cBuff = ( *env )->GetByteArrayElements( env , buff, NULL );
    const char * name = ( * env )->GetStringUTFChars( env , n , NULL );
    int status = luaL_loadbuffer( L , ( const char * ) cBuff, ( int ) sz, name );
    if (status == 0) {
        status = lua_pcall(L, 0, LUA_MULTRET, 1);
    }
 
    const char* ret = "";
 
    if (status != 0) {
        ret = lua_tostring(L, -1);
    }
    lua_settop(L, 0);
    ( *env )->ReleaseStringUTFChars( env , n , name );
 
    ( *env )->ReleaseByteArrayElements( env , buff , cBuff , 0 );
    return (*env)->NewStringUTF(env, ret);
}           

上面的代碼中lua_pcall的第四個參數我們傳了1,代表資料棧中索引1的位置我們放了一個錯誤處理函數。這個函數就是通過lua_pushcfunction push到棧中的,代碼中push了一個msghandler的lua c function。

我們在看下msghandler的實作:

static int msghandler (lua_State *L) {
  onLuaError(L);
  const char *msg = lua_tostring(L, 1);
  if (msg == NULL) {  /* is error object not a string? */
    if (luaL_callmeta(L, 1, "__tostring") &&  /* does it have a metamethod */
        lua_type(L, -1) == LUA_TSTRING)  /* that produces a string? */
      return 1;  /* that is the message */
    else
      msg = lua_pushfstring(L, "(error object is a %s value)",
                               luaL_typename(L, 1));
  }
  luaL_traceback(L, L, msg, 1);  /* append a standard traceback */
  return 1;  /* return the traceback */
}           

這段代碼捕獲了pcall中的異常資訊,它位于棧索引1的位置,不過這個異常資訊不一定是字元串類型,如果不是我們嘗試從它的元表中調用__tostring方法來獲得錯誤資訊。拿到錯誤資訊我們把它通過luaL_traceback拼接到棧回溯資訊前,并通過return 1将結果傳回。

這個地方我們可以把這些錯誤資訊通過自己的異常上傳邏輯收集起來,進而實作了全局Lua異常捕獲、收集和上傳了。

2.5.4、引用機制

一些情況下C函數需要儲存一些非局部資料,即生存時間超出C函數執行時間的資料。在C語言中,我們通常使用全局變量(extern)或靜态變量來滿足這種需求。然而,當我們為Lua編寫庫函數時,這并不是一個好辦法。首先,我們無法在一個C語言變量中儲存普通的Lua值。其次,使用這類變量的庫無法用于多個Lua狀态。Lua的CAPI提供了兩個類似的地方來存儲非局部資料,即系統資料庫(registry)和上值(upvalue)。

然而系統資料庫是一個普通的Lua表,它的鍵很有可能沖突。引用機制主要是解決在Lua中拓展多個C庫時可能會在全局表中定義相同的key,帶來潛在的問題。這個機制簡單說引用機制就是Lua自動管理key,開發者不應該手動指定key,否則就可能破壞内部的Key重用機制。

引用機制提供了兩個方法:

int luaL_ref (lua_State *L, int t)
void luaL_unref (lua_State *L, int t, int ref)           

luaL_ref會從棧中彈出一個值,然後配置設定一個新的整形的鍵,使用這個鍵将從棧中彈出的值儲存到系統資料庫中,最後傳回該整形鍵,而這個鍵就被稱為引用(reference)。最後想要釋放值和引用,我們可以調用 luaL_unref。

2.5.5、垃圾回收機制

雲風大神有一篇關于Lua垃圾回收機制的講解,非常深入,詳細了解可以參考這篇文章——

。這裡簡單介紹一下Lua的垃圾收集器。

一直到Lua 5.0,Lua語言使用的都是一個簡單的标記-清除(mark-and-sweep)式垃圾收集器。這種收集器又被稱為“stop-the-world”(全局暫停)式的收集器,意味着Lua語言會時不時的停止主程式的運作來執行一次完整的垃圾收集周期。每一個垃圾收集周期由四個階段組成:标記(mark)、清理(cleaning)、清除(sweep)和析構(finalization)。

标記階段會把根結點集合标記為活躍,根節點就是由Lua語言可以直接通路的對象組成。在Lua語言中,這個集合隻包括C系統資料庫(主線程和全局環境都是在這個系統資料庫中預定義的元素)。

Lua5.1使用了增量式垃圾收集器。這種垃圾收集起像老版的垃圾收集器一樣執行相同的步驟,但是不需要在垃圾收集期間停止主程式的運作。相反,它與解釋器一起交替運作。每當解釋器配置設定了一定數量的記憶體時,垃圾收集器也執行一小步(這意味着,在垃圾收集器工作期間,解釋器可能會改變一個對象的可達性。為了保證垃圾收集器的正确性,垃圾收集器中的有些操作具有發現危險改動和糾正所涉及的對象标記的

記憶體屏障

Lua 5.2引入了緊急垃圾收集。當記憶體配置設定失敗時,Lua語言會強制進行一次完整的垃圾收集,然後再次嘗試配置設定。

2.5.5.1、一些輔助垃圾回收機制的工具

2.5.5.1.1、弱引用表

所謂弱引用(weak reference)是一種不在垃圾收集器考慮範圍内的對象引用。如果對一個對象的所有引用都是弱引用,那麼垃圾收集器将會回收這個對象并删除這些弱引用。

一個表是否為弱引用表是由其元表中的__mode字段所決定的。當這個字段存在時,其值應為一個字元串:如果這個字元串為“v”,那麼代表這個表的值是弱引用的;如果這個字元串是“kv”,那麼這個表的鍵和值都是弱引用的。舉個栗子:

a = {}
mt = {__mode = "k"}
setmetatable(a, mt) --現在'a'的鍵是弱引用的了
key = {} -- 建立第一個鍵
a[key] = 1
key = {} -- 建立第二個鍵
a[key] = 2
 
collectgarbage() --強制進行垃圾回收
for k, v in pairs(a) do 
print(v)
end
 
--output
--[[
2
]]           

上面的示例中雖然我們在table a 中建立了兩個key,分别對應值1和2。但是由于設定了a的元表中的__mode的弱引用表的模式為“k”。我們看到當key被指派2次時,第一次賦給key的值就不再被強引用了,這樣在強制垃圾回收後a 表中第一個鍵值對就被垃圾回收器從表中移除了。

談到弱引用表,這裡需要提一下“瞬表”的概念:

一種棘手的情況是,一個具有弱引用鍵的表中的值又引用了對應的鍵。一個典型的示例是敞亮函數工廠。這種工廠的參數是一個對象,傳回值是一個被調用時傳回傳入對象的函數:

do
    local mem = {}
    setmetatable(mem, {__mode = "k"})
    function factory(o)
        local res = mem[o]
        if not res then
            res = (function() return o end)
            mem[o] = res
        end 
 
        return res
    end
end           

不過,這裡另有玄機。請注意,表mem中與一個對象關聯的值(函數)回指向了它自己的鍵(對象本身)。雖然表中的鍵是弱引用的,但是表中的值卻不是弱引用的。從一個弱引用表的标準了解看,這個表裡面沒有任何東西會被移除。由于值不是弱引用的,是以對于每一個函數來說都存在一個強引用。每一個函數都指向其對應的對象,因而對于每一個鍵來說都存在一個強引用。是以,即使有弱引用的鍵,這些對象也不會被回收。

Lua語言通過瞬表的概念來解決這個問題。在Lua語言中,一個具有弱引用鍵和強引用值的表是一個瞬表。在一個瞬表中,一個鍵的可通路性控制着對應值的可通路性。更确切的說,考慮瞬表中的一個元素(k, v),指向的v的引用隻有當存在某些指向k的其他外部引用存在時才是強引用,否則即使v (直接或間接地)引用了k,垃圾收集器最終會收集k并把元素從表中移除。

2.5.5.1.2、析構器

垃圾收集器不僅可以回收對象,也可以幫助程式釋放資源。出于這個目的,幾種程式設計語言都提供了析構器。析構器是一個與對象關聯的函數,當該對象即将被回收時該函數會被調用。

Lua 語言通過元方法__gc實作析構器,例如:

o = {x = 'hi'}
setmetatable(o, {__gc = function(o) print("finalize o" .. o.x) end})
o = nil
 
collectgarbage() 
 
--output
--[[
    hi
]]           

在本例中,我們首先建立一個帶有__gc元方法元表的表,然後把這個變量o指派為nil,也就是抹去了與這個表的唯一聯系(全局變量),再強制進行一次完整的垃圾回收。在垃圾回收期間,Lua 語言發現表已經不再是可通路的了,是以會調用表的析構函數,也就是元方法__gc。

3、參考文獻

繼續閱讀