Lua 學習筆記(上)

tangyikejun發表於2019-02-16

1 簡介

由 clean C 實現。需要被宿主程式呼叫,可以注入 C 函式。

2 語法

採用基於 BNF 的語法規則。

2.1 語法約定

Lua 對大小寫敏感。

2.1.1 保留關鍵字

C 語言中沒有的關鍵字有:

and elseif function
in nil local not or
repeat then until

規範:全域性變數以下劃線開頭。

2.1.2 操作符

C 語言中沒有的操作符:

^ 
~= 
//  -- 向下取整

Lua 中沒有的操作符:

+=
-=

2.1.3 字串定義

採用轉義符:通過轉義符表示那些有歧義的字元

字元表示

a           -- 代表字元 a
97         -- 代表字元 a
 49        -- 代表數字字元 1 

其他轉義符表示

\n         -- 代表字串 


          -- 代表換行

注意數字字元必須是三位。其他字元則不能超過三位。

採用長括號:長括號內的所有內容都作為普通字元處理。

[[]]        -- 0級長括號
[==[]==]    -- 2級長括號

2.2 值與型別

Lua 是動態語言,變數沒有型別,值才有。值自身攜帶型別資訊。

Lua 有八種基本資料型別:nil, boolean, number, string, function, userdata, thread, table

nilfalse 導致條件為假,其他均為真。

userdata 型別變數用於儲存 C 資料。 Lua 只能對該類資料進行使用,而不能進行建立或修改,保證宿主程式完全掌握資料。

thread 用於實現協程(coroutine)。

table 用於實現關聯陣列。table 允許任何型別的資料做索引,也允許任何型別做 table 域中的值(前述
任何型別 不包含 nil)。table 是 Lua 中唯一的資料結構。
由於函式也是一種值,所以 table 中可以存放函式。

function, userdata, thread, table 這些型別的值都是物件。這些型別的變數都只是儲存變數的引用,並且在進行賦值,引數傳遞,函式返回等操作時不會進行任何性質的拷貝。

庫函式 type() 返回變數的型別描述資訊。

2.2.1 強制轉換

Lua 提供數字字串間的自動轉換。
可以使用 format 函式控制數字向字串的轉換。

2.3 變數

變數有三種型別:全域性變數、區域性變數、表中的域

函式外的變數預設為全域性變數,除非用 local 顯示宣告。函式內變數與函式的引數預設為區域性變數。

區域性變數的作用域為從宣告位置開始到所在語句塊結束(或者是直到下一個同名區域性變數的宣告)。

變數的預設值均為 nil。


a = 5 -- 全域性變數 local b = 5 -- 區域性變數 function joke() c = 5 -- 區域性變數 local d = 6 -- 區域性變數 end print(c,d) --> nil nil do local a = 6 -- 區域性變數 b = 6 -- 全域性變數 print(a,b); --> 6 6 end print(a,b) --> 5 6

方便標記,--> 代表前面表示式的結果。

2.3.1 索引

對 table 的索引使用方括號 []。Lua使用語法糖提供 . 操作。

t[i]
t.i                 -- 當索引為字串型別時的一種簡化寫法
gettable_event(t,i) -- 採用索引訪問本質上是一個類似這樣的函式呼叫

2.3.2 環境表

所有全域性變數放在一個環境表裡,該表的變數名為 _env 。對某個全域性變數 a 的訪問即 _env.a_env_ 只是為了方便說明)。

每個函式作為變數持有一個環境表的引用,裡面包含該函式可呼叫的所有變數。
子函式會從父函式繼承環境表。
可以通過函式 getfenv / setfenv 來讀寫環境表。

2.4 語句 | statement

支援賦值,控制結構,函式呼叫,還有變數宣告。

不允許空的語句段,因此 ;; 是非法的。

2.4.1 語句組 | chuncks

chunck ::= {stat[`;`]}

([`;`] 應該是表示語句組後面 ; 是可選項。)

2.4.2 語句塊 | blocks

block ::= chunck
stat ::= do block end

可以將一個語句塊顯式地寫成語句組,可以用於控制區域性變數的作用範圍。

2.4.3 賦值 | assignment

Lua 支援多重賦值。

多重賦值時,按序將右邊的表示式的值賦值給左值。右值不足補 nil,右值多餘捨棄。

b = 1
a,b = 4 -- a = 4,b = nil 

+++

Lua 在進行賦值操作時,會一次性把右邊的表示式都計算出來後進行賦值。

i = 5
i,a[i] = i+1, 7 -- i = 6 ,a[5] = 7

特別地,有

x,y = y,x -- 交換 x,y 的值

+++

對全域性變數以及表的域的賦值操作含義可以在元表中更改。

2.4.4 控制結構

條件語句

if [exp]
    [block]
elseif [exp]
    [block]
else
    [block]
end

迴圈語句

while [exp]
    [block]
end

+++

repeat
    [block]
until [exp]

注意,由於 repeat 語句到 until 還未結束,因此在 until 之後的表示式中可以使用 block 中定義的區域性變數。

例如:

a = 1
c = 5
repeat
    b = a + c
    c = c * 2
until b > 20
print(c)            -->     40

+++

breakreturn

breakreturn 只能寫在語句塊的最後一句,如果實在需要寫在語句塊中間,那麼就在兩個關鍵詞外面包圍 do end 語句塊。

do break end

2.4.5 For 迴圈

for 迴圈的用法比較多,單獨拎出來講。

for 中的表示式會在迴圈開始前一次性求值,在迴圈過程中不再更新。

數字形式

for [Name] = [exp],[exp],[exp] do [block] end

三個 exp 分別代表初值,結束值,步進。exp 的值均需要是一個數字。
第三個 exp 預設為 1,可以省略。

a = 0

for i = 1,6,2 do
    a = a + i
end

等價於

int a = 0;
for (int i = 1; i <= 6;i += 2){ // 取到等號,如果步進是負的,那麼會取 i >= 6
    a += i;
}

迭代器形式

迭代器形式輸出一個表時,如果表中有函式,則輸出的順序及個數不確定(筆者測試得出的結果,具體原因未知)。

迭代器形式的 for 迴圈的實質

-- 依次返回 迭代器、狀態表、迭代器初始值
function mypairs(t)

    function iterator(t,i)
        i = i + 1
        i = t[i] and i      -- 如果 t[i] == nil 則 i = nil;否則 i = i
        return i,t[i]
    end

    return iterator,t,0

end

-- 一個表
t = {[1]="1",[2]="2"}

-- 迭代形式 for 語句的 等價形式
do
local f, s, var = mypairs(t)
    while true do
        local var1, var2 = f(s, var)
        var = var1
        if var == nil then break end

        -- for 迴圈中新增的語句
        print(var1,var2)

    end
end

-- 迭代形式 for 語句
for var1,var2 in mypairs(t) do
    print(var1,var2)
end

--> 1   1
--> 2   2
--> 1   1
--> 2   2
陣列形式
ary = {[1]=1,[2]=2,[5]=5}
for i,v in ipairs(ary) do
    print(v)                    --> 1 2
end

從1開始,直到數值型下標結束或者值為 nil 時結束。

表遍歷
table = {[1]=1,[2]=2,[5]=5}
for k,v in pairs(table) do
    print(v)                    --> 1 2 5
end

遍歷整個表的鍵值對。

關於迭代器的更多內容,可參考Lua 迭代器和泛型 for

2.5 表示式

2.5.1 數學運算操作符

% 操作符

Lua 中的 % 操作符與 C 語言中的操作符雖然都是取模的含義,但是取模的方式不一樣。
在 C 語言中,取模操作是將兩個運算元的絕對值取模後,在新增上第一個運算元的符號。
而在 Lua 中,僅僅是簡單的對商相對負無窮向下取整後的餘數。

+++

在 C 中,

a1 = abs(a);
b1 = abs(b);
c = a1 % b1 = a1 - floor(a1/b1)*b1;

a % b = (a >= 0) ? c : -c;

在 Lua 中,

a % b == a - math.floor(a/b)*b

Lua 是直接根據取模定義進行運算。 C 則對取模運算做了一點處理。

+++

舉例:

在 C 中

int a = 5 % 6;
int b = 5 % -6;
int c = -5 % 6;
int d = -5 % -6;

printf("a,b,c,d");--5,5,-5,-5

在 Lua 中

a = 5 % 6
b = 5 % -6
c = -5 % 6
d = -5 % -6

x = {a,b,c,d}

for i,v in ipairs(x) do
    print(i,v)
end


--> 5
--> -1
--> 1
--> -5

可以看到,僅當運算元同號時,兩種語言的取模結果相同。異號時,取模結果的符號與數值均不相等。

在 Lua 中的取模運算總結為:a % b,如果 a,b 同號,結果取 a,b 絕對值的模;異號,結果取 b 絕對值與絕對值取模後的差。取模後值的符號與 b 相同。

2.5.2 比較操作符

比較操作的結果是 boolean 型的,非 truefalse

支援的操作符有:

< <= ~= == > >=

不支援 ! 操作符。

+++

對於 == 操作,運算時先比較兩個運算元的型別,如果不一致則結果為 false。此時數值與字串之間並不會自動轉換。

比較兩個物件是否相等時,僅當指向同一記憶體區域時,判定為 true。·

a = 123
b = 233
c = "123"
d = "123"
e = {1,2,3}
f = e
g = {1,2,3}

print(a == b)       --> false
print(a == c)       --> false      -- 數字與字串作為不同型別進行比較
print(c == d)       --> true       
print(e == f)       --> true       -- 引用指向相同的物件
print(e == g)       --> false      -- 雖然內容相同,但是是不同的物件
print(false == nil) --> false      -- false 是 boolean,nil 是 nil 型

方便標記,--> 代表前面表示式的結果。

+++

userdatatable 的比較方式可以通過元方法 eq 進行改變。

大小比較中,數字和字串的比較與 C 語言一致。如果是其他型別的值,Lua會嘗試呼叫元方法 ltle

2.5.3 邏輯操作符

and,or,not

僅認為 falsenil 為假。

not

取反操作 not 的結果為 boolean 型別。(andor 的結果則不一定為 boolean)

b = not a           -- a 為 nil,b 為 true
c = not not a       -- c 為 false

and

a and b,如果 a 為假,返回 a,如果 a 為真, 返回 b

注意,為什麼 a 為假的時候要返回 a 呢?有什麼意義?這是因為 a 可能是 false 或者 nil,這兩個值雖然都為假,但是是有區別的。

or

a or b,如果 a 為假,返回 b,如果 a 為真, 返回 a。與 and 相反。

+++

提示: 當邏輯操作符用於得出一個 boolean 型結果時,不需要考慮邏輯運算後返回誰的問題,因為邏輯操作符的操作結果符合原本的邏輯含義。

舉例

if (not (a > min and a < max)) then  -- 如果 a 不在範圍內,則報錯
    error() 
end

+++

其他

andor 遵循短路原則,第二個運算元僅在需要的時候會進行求值操作。

例子


a = 5 x = a or jjjj() -- 雖然後面的函式並沒有定義,但是由於不會執行,因此不會報錯。 print(a) -->5 print(x) -->5

通過上面這個例子,我們應當對於邏輯操作有所警覺,因為這可能會引入一些未能及時預料到的錯誤。

2.5.4 連線符

..
連線兩個字串(或者數字)成為新的字串。對於其他型別,呼叫元方法 concat

2.5.5 取長度操作符

#

對於字串,長度為字串的字元個數。

對於表,通過尋找滿足t[n] 不是 nil 而 t[n+1] 為 nil 的下標 n 作為表的長度。

~~對於其他型別呢?~~

例子

-- 字串取長
print(#"abc ")                         --> 4
-- 表取長
print(#{[1]=1,[2]=2,[3]=3,x=5,y=6})     --> 3
print(#{[1]=1,[2]=nil,[3]=3,x=5,y=6})   --> 1

2.5.6 優先順序

由低到高:

or
and
 <     >     <=    >=    ~=    ==
 ..
 +     -
 *     /     %
 not   #     - (unary)
 ^

冪運算>單目運算>四則運算>連線符>比較操作符>and>or

2.5.7 Table 構造

Table 構造的 BNF 定義

tableconstructor ::= `{´ [fieldlist] `}´
fieldlist ::= field {fieldsep field} [fieldsep]
field ::= `[´ exp `]´ `=´ exp | Name `=´ exp | exp
fieldsep ::= `,´ | `;´

舉例:

a = {}
b = {["price"] = 5; cost = 4; 2+5}
c = { [1] = 2+5, [2] = 2, 8, price = "abc", ["cost"] = 4} -- b 和 c 構造的表是等價的


print(b["price"])   --> 5
print(b.cost)       --> 4
print(b[1])         --> 7       -- 未給出鍵值的,按序分配下標,下標從 1 開始

print(c["price"])   --> abc
print(c.cost)       --> 4
print(c[1])         --> 8       
print(c[2])         --> 2       

注意:

  • 未給出鍵值的,按序分配下標,下標從 1 開始
  • 如果表中有相同的鍵,那麼以靠後的那個值作為鍵對應的值

上面這兩條的存在使得上面的例子中 c1 的輸出值為 8。

+++

如果表中有相同的鍵,那麼以靠後的那個值作為鍵對應的值。

a = {[1] = 5,[1] = 6} -- 那麼 a[1] = 6

+++

如果表的最後一個域是表示式形式,並且是一個函式,那麼這個函式的所有返回值都會加入到表中。

a = 1
function order()
    a = a + 1
    return 1,2,3,4
end

b = {order(); a; order(); }

c = {order(); a; (order());}

print(b[1])                     --> 1       
print(b[2])                     --> 2       -- 表中的值並不是一次把表示式都計算結束後再賦值的
print(b[3])                     --> 1       
print(b[4])                     --> 2       -- 表示式形式的多返回值函式

print(#b)                       --> 6       -- 表的長度為 6                 
print(#c)                       --> 3       -- 函式新增括號後表的長度為 3

2.5.8 函式定義

函式是一個表示式,其值為 function 型別的物件。函式每次執行都會被例項化。

Lua 中實現一個函式可以有以下三種形式。

f = function() [block] end
local f; f = function() [block] end
a.f = function() [block] end

Lua 提供語法糖分別處理這三種函式定義。

function f() [block] end
local function f() [block] end
function a.f() [block] end

+++

上面 local 函式的定義之所以不是 local f = function() [block] end,是為了避免如下錯誤:

local f = function()
    print("local fun")
    if i==0 then 
        f()             -- 編譯錯誤:attempt to call global `f` (a nil value)
        i = i + 1
    end
end

函式的引數

形參會通過實參來初始化為區域性變數。

引數列表的尾部新增 ... 表示函式能接受不定長引數。如果尾部不新增,那麼函式的引數列表長度是固定的。

f(a,b)
g(a,b,...)
h(a,...,b)              -- 編譯錯誤
f(1)                    --> a = 1, b = nil
f(1,2)                  --> a = 1, b = 2
f(1,2,3)                --> a = 1, b = 2

g(1,2)                  --> a = 1, b = 2, (nothing)
g(1,2,3)                --> a = 1, b = 2, (3)
g(1,f(4,5),3)           --> a = 1, b = 4, (3)
g(1,f(4,5))             --> a = 1, b = 4, (5)

+++

還有一種形參為self的函式的定義方式:

a.f = function (self, params) [block] end

其語法糖形式為:

function a:f(params) [block] end

使用舉例:

a = {name = "唐衣可俊"}
function a:f()
    print(self.name)
end
a:f()                       --> 唐衣可俊   -- 如果這裡使用 a.f(),那麼 self.name 的地方會報錯 attempt to index local `self`;此時應該寫為 a.f(a)

: 的作用在於函式定義與呼叫的時候可以少寫一個 self 引數。這種形式是對方法的模擬

2.5.9 函式呼叫

Lua 中的函式呼叫的BNF語法如下:

functioncall ::= prefixexp args

如果 prefixexp 的值的型別是 function, 那麼這個函式就被用給出的引數呼叫。 否則 prefixexp 的元方法 “call” 就被呼叫, call 的第一個引數就是 prefixexp 的值,接下來的是 args 引數列表(參見 2.8 元表 | Metatable)。

函式呼叫根據是否傳入 self 引數分為 . 呼叫和 : 呼叫。
函式呼叫根據傳入引數的型別,可以分為引數列表呼叫、表呼叫、字串呼叫

[待完善]

2.5.10 函式閉包

如果一個函式訪問了它的外部變數,那麼它就是一個閉包。

由於函式內部的變數均為區域性變數,外界無法對其進行訪問。這時如果外界想要改變區域性變數的值,那麼就可以使用閉包來實現這一目的。
具體的實現過程大致是這樣,函式內部有能夠改變區域性變數的子函式,函式將這個子函式返回,那麼外界就可以通過使用這個子函式來操作區域性變數了。

例子:利用閉包來實現對區域性變數進行改變

-- 實現一個迭代器

function begin(i)
    local cnt = i

    return function ()      -- 這是一個匿名函式,實現了自增的功能;同時它也是一個閉包,因為訪問了外部變數 cnt
        cnt = cnt + 1
        return cnt
    end
end


iterator = begin(2)     -- 設定迭代器的初值為 2 ,返回一個迭代器函式

print(iterator())           -- 執行迭代
print(iterator())

提示: 關於閉包的更多說明可參考JavaScript 閉包是如何工作的?——StackOverflow

2.6 可視規則

即變數的作用域,見 2.3 變數 部分。

2.7 錯誤處理

[待補充]

2.8 元表 | Metatable

我們可以使用操作符對 Lua 的值進行運算,例如對數值型別的值進行加減乘除的運算操作以及對字串的連線、取長操作等(在 2.5 表示式 這一節中介紹了許多類似的運算)。元表正是定義這些操作行為的地方。

元表本質上是一個普通 Lua 表。元表中的鍵用來指定操作,稱為“事件名”;元表中鍵所關聯的值稱為“元方法”,定義操作的行為。

2.8.1 事件名與元方法

僅表(table)型別值對應的元表可由使用者自行定義。其他型別的值所對應的元表僅能通過 Debug 庫進行修改。

元表中的事件名均以兩條下劃線 __ 作為字首,元表支援的事件名有如下幾個:

__index     -- `table[key]`,取下標操作,用於訪問表中的域
__newindex  -- `table[key] = value`,賦值操作,增改表中的域
__call      -- `func(args)`,函式呼叫,參見 [2.5.9 函式呼叫](#2-5-9)

-- 數學運算操作符
__add       -- `+`
__sub       -- `-`
__mul       -- `*`
__div       -- `/`
__mod       -- `%`
__pow       -- `^`
__unm       -- `-`

-- 連線操作符
__concat    -- `..`

-- 取長操作符
__len       -- `#`

-- 比較操作符
__eq        -- `==`
__lt        -- `<`      -- a > b 等價於 b < a
__le        -- `<=`     -- a >= b 等價於 b <= a 

還有一些其他的事件,例如 __tostring__gc 等。

下面進行詳細介紹。

2.8.2 元表與值

每個值都可以擁有一個元表。對 userdata 和 table 型別而言,其每個值都可以擁有獨立的元表,也可以幾個值共享一個元表。對於其他型別,一個型別的值共享一個元表。例如所有數值型別的值會共享一個元表。除了字串型別,其他型別的值預設是沒有元表的。

使用 getmetatable 函式可以獲取任意值的元表。getmetatable (object)
使用 setmetatable 函式可以設定表型別值的元表。setmetatable (table, metatable)

例子

只有字串型別的值預設擁有元表:

a = "5"
b = 5
c = {5}
print(getmetatable(a))      --> table: 0x7fe221e06890
print(getmetatable(b))      --> nil
print(getmetatable(c))      --> nil

2.8.3 事件的具體介紹

事先提醒 Lua 使用 raw 字首的函式來操作元方法,避免元方法的迴圈呼叫。

例如 Lua 獲取物件 obj 中元方法的過程如下:

rawget(getmetatable(obj)or{}, "__"..event_name)

元方法 index

index 是元表中最常用的事件,用於值的下標訪問 — table[key]

事件 index 的值可以是函式也可以是表。當使用表進行賦值時,元方法可能引發另一次元方法的呼叫,具體可見下面偽碼介紹。
當使用者通過鍵值來訪問表時,如果沒有找到鍵對應的值,則會呼叫對應元表中的此事件。如果 index 使用表進行賦值,則在該表中查詢傳入鍵的對應值;如果 index 使用函式進行賦值,則呼叫該函式,並傳入表和鍵。

Lua 對取下標操作的處理過程用偽碼錶示如下:

function gettable_event (table, key)
    -- h 代表元表中 index 的值
    local h     
    if type(table) == "table" then

        -- 訪問成功
        local v = rawget(table, key)
        if v ~= nil then return v end

        -- 訪問不成功則嘗試呼叫元表的 index
        h = metatable(table).__index

        -- 元表不存在返回 nil
        if h == nil then return nil end
    else

        -- 不是對錶進行訪問則直接嘗試元表
        h = metatable(table).__index

        -- 無法處理導致出錯
        if h == nil then
            error(···);
        end
    end

    -- 根據 index 的值型別處理
    if type(h) == "function" then
        return h(table, key)            -- 呼叫處理器
    else 
        return h[key]                   -- 或是重複上述操作
    end
end

例子:

使用表賦值:

t = {[1] = "cat",[2] = "dog"}
print(t[3])             --> nil
setmetatable(t, {__index = {[3] = "pig", [4] = "cow", [5] = "duck"}})
print(t[3])             --> pig

使用函式賦值:

t = {[1] = "cat",[2] = "dog"}
print(t[3])             --> nil
setmetatable(t, {__index = function (table,key)
    key = key % 2 + 1
    return table[key]
end})
print(t[3])             --> dog

元方法 newindex

newindex 用於賦值操作 — talbe[key] = value

事件 newindex 的值可以是函式也可以是表。當使用表進行賦值時,元方法可能引發另一次元方法的呼叫,具體可見下面偽碼介紹。

當操作型別不是表或者表中尚不存在傳入的鍵時,會呼叫 newindex 的元方法。如果 newindex 關聯的是一個函式型別以外的值,則再次對該值進行賦值操作。反之,直接呼叫函式。

~~不是太懂:一旦有了 “newindex” 元方法, Lua 就不再做最初的賦值操作。 (如果有必要,在元方法內部可以呼叫 rawset 來做賦值。)~~

Lua 進行賦值操作時的偽碼如下:

function settable_event (table, key, value)
    local h
    if type(table) == "table" then

        -- 修改表中的 key 對應的 value
        local v = rawget(table, key)
        if v ~= nil then rawset(table, key, value); return end

        -- 
        h = metatable(table).__newindex

        -- 不存在元表,則直接新增一個域
        if h == nil then rawset(table, key, value); return end
    else
        h = metatable(table).__newindex
        if h == nil then
            error(···);
        end
    end

    if type(h) == "function" then
        return h(table, key,value)    -- 呼叫處理器
    else 


        h[key] = value             -- 或是重複上述操作
    end
end

例子:

元方法為表型別:

t = {}
mt = {}

setmetatable(t, {__newindex = mt})
t.a = 5
print(t.a)      --> nil
print(mt.a)     --> 5

通過兩次呼叫 newindex 元方法將新的域新增到了表 mt 。

+++

元方法為函式:

-- 對不同型別的 key 使用不同的賦值方式
t = {}
setmetatable(t, {__newindex = function (table,key,value)
    if type(key) == "number" then
        rawset(table, key, value*value)
    else
        rawset(table, key, value)
    end
end})
t.name = "product"
t[1] = 5
print(t.name)       --> product
print(t[1])         --> 25

元方法 call

call 事件用於函式呼叫 — function(args)

Lua 進行函式呼叫操作時的虛擬碼:

function function_event (func, ...)

  if type(func) == "function" then
      return func(...)   -- 原生的呼叫
  else
      -- 如果不是函式型別,則使用 call 元方法進行函式呼叫
      local h = metatable(func).__call

      if h then
        return h(func, ...)
      else
        error(···)
      end
  end
end

例子:

由於使用者只能為表型別的值繫結自定義元表,因此,我們可以對錶進行函式呼叫,而不能把其他型別的值當函式使用。

-- 把資料記錄到表中,並返回資料處理結果
t = {}

setmetatable(t, {__call = function (t,a,b,factor)
  t.a = 1;t.b = 2;t.factor = factor
  return (a + b)*factor
end})

print(t(1,2,0.1))       --> 0.3

print(t.a)              --> 1
print(t.b)              --> 2
print(t.factor)         --> 0.1

運算操作符相關元方法

運算操作符相關元方法自然是用來定義運算的。

以 add 為例,Lua 在實現 add 操作時的偽碼如下:

function add_event (op1, op2)
  -- 引數可轉化為數字時,tonumber 返回數字,否則返回 nil
  local o1, o2 = tonumber(op1), tonumber(op2)
  if o1 and o2 then  -- 兩個運算元都是數字?
    return o1 + o2   -- 這裡的 `+` 是原生的 `add`
  else  -- 至少一個運算元不是數字時
    local h = getbinhandler(op1, op2, "__add") -- 該函式的介紹在下面
    if h then
      -- 以兩個運算元來呼叫處理器
      return h(op1, op2)
    else  -- 沒有處理器:預設行為
      error(···)
    end
  end
end

程式碼中的 getbinhandler 函式定義了 Lua 怎樣選擇一個處理器來作二元操作。 在該函式中,首先,Lua 嘗試第一個運算元。如果這個運算元所屬型別沒有定義這個操作的處理器,然後 Lua 會嘗試第二個運算元。

 function getbinhandler (op1, op2, event)
   return metatable(op1)[event] or metatable(op2)[event]
 end

+++

對於一元操作符,例如取負,Lua 在實現 unm 操作時的偽碼:

function unm_event (op)
  local o = tonumber(op)
  if o then  -- 運算元是數字?
    return -o  -- 這裡的 `-` 是一個原生的 `unm`
  else  -- 運算元不是數字。
    -- 嘗試從運算元中得到處理器
    local h = metatable(op).__unm
    if h then
      -- 以運算元為引數呼叫處理器
      return h(op)
    else  -- 沒有處理器:預設行為
      error(···)
    end
  end
end

例子:

加法的例子:

t = {}
setmetatable(t, {__add = function (a,b)
  if type(a) == "number" then
      return b.num + a
  elseif type(b) == "number" then
      return a.num + b
  else
      return a.num + b.num
  end
end})

t.num = 5

print(t + 3)  --> 8

取負的例子:

t = {}
setmetatable(t, {__unm = function (a)
  return -a.num
end})

t.num = 5

print(-t)  --> -5

其他事件的元方法

對於連線操作,當運算元中存在數值或字串以外的型別時呼叫該元方法。

對於取長操作,如果運算元不是字串型別,也不是表型別,則嘗試使用元方法(這導致自定義的取長基本沒有,在之後的版本中似乎做了改進)。

對於三種比較類操作,均需要滿足兩個運算元為同型別,且關聯同一個元表時才能使用元方法。

對於 eq (等於)比較操作,如果運算元所屬型別沒有原生的等於比較,則呼叫元方法。

對於 lt (小於)與 le (小於等於)兩種比較操作,如果兩個運算元同為數值或者同為字串,則直接進行比較,否則使用元方法。

對於 le 操作,如果元方法 “le” 沒有提供,Lua 就嘗試 “lt”,它假定 a <= b 等價於 not (b < a) 。

對於 tostring 操作,元方法定義了值的字串表示方式。

例子:

取長操作:

t = {1,2,3,"one","two","three"}
setmetatable(t, {__len = function (t)
  local cnt = 0
  for k,v in pairs(t) do
    if type(v) == "number" then 
      cnt = cnt + 1
      print(k,v)
    end
  end
  return cnt
end})

-- 結果是 6 而不是預期中的 3
print(#t)   --> 6 

等於比較操作:

t = {name="number",1,2,3}
t2 = {name = "number",4,5,6}
mt = {__eq = function (a,b)
    return a.name == b.name
end}
setmetatable(t,mt)              -- 必須要關聯同一個元表才能比較
setmetatable(t2,mt)

print(t==t2)   --> true

tostring 操作:

t = {num = "a table"}
print(t)              --> table: 0x7f8e83c0a820

mt = {__tostring = function(t)
  return t.num
end}
setmetatable(t, mt)

print(tostring(t))    --> a table
print(t)              --> a table

2.9 環境表

型別 threadfunctionuserdata 的物件除了能與元表建立關聯外,還能關聯一個環境表。

關聯線上程上的環境表稱為全域性環境。
全域性環境作為子執行緒及子函式的預設環境。
全域性環境能夠直接被 C 呼叫。

關聯在 Lua 函式上的環境表接管函式對全域性變數的所有訪問。並且作為子函式的預設環境。

關聯在 C 函式上的環境能直接被 C 呼叫。

關聯在 userdata 上的環境沒有實際的用途,只是為了方便程式設計師把一個表關聯到 userdata 上。

2.10 垃圾回收

2.10.1 垃圾收集的元方法

[待補充]

2.10.2 弱表

弱表是包含弱引用的表。

弱表的弱引用方式有三種。鍵弱引用,值弱引用,鍵和值均弱引用

可以通過元表中的 __mode 域來設定一個表是否有弱引用,以及弱引用的方式。

a = {}
b = { __mode = "k"}  -- 引號中新增 k 表示 key 弱引用,v 表示 value 弱引用, kv 表示均弱引用。
setmetable(a,b)     -- b 是 a 的元表,繫結後就不能在更改 __mode 的值。

垃圾回收機制會把弱引用的部分回收。但是不論是哪種弱引用,回收機制都會把整個鍵值對從弱表中移除。

3 程式介面 (API)

這部分描述 Lua 的 C API,即用來與 Lua 進行通訊的 C 函式,所有的函式和常量都定義在 lua.h 標頭檔案裡面。

有一部分 C 函式是用巨集來實現的。~~為什麼?:由於所有的巨集只會使用他們的引數一次(除了第一個引數,即 Lua 狀態機),所以不必擔心巨集展開帶來的副作用。~~

預設情況下 Lua 在進行函式呼叫時不會檢查函式的有效性和堅固性,如果想要進行檢查,則使用 luaconf.h 中的 luai_apicheck() 函式開啟。

3.1 堆疊

Lua 呼叫 C API 時使用一個虛擬棧來傳遞引數,棧中的所有元素都是 Lua 的型別(例如 booleantablenil等)。

Lua 呼叫 C 函式的時候都會新建一個虛擬棧,而不是使用舊棧或者其他的棧。同時在 C 函式中,對 Lua API 呼叫時,只能使用當前呼叫所對應棧中的元素,其他棧的元素是無法訪問的。
虛擬棧中包含 C 函式所需的所有引數,函式的返回值也都放在該棧中。

這裡所謂的棧概念並不是嚴格意義上的棧,可以通過下標對棧中的元素進行訪問。1表示棧底,-1表示棧頂,又例如 3 表示從棧底開始的第三個元素。

3.2 堆疊尺寸

由於 Lua 的 C API 預設不做有效性和堅固性(魯棒性)檢測,因此開發人員有責任保證堅固性。特別要注意的是,不能讓堆疊溢位。Lua 只保證棧大小會大於 LUA_MINSTACK(一般是 20)。開發人員可以使用 lua_checkstack 函式來手動設定棧的大小。

3.3 偽索引

除了用索引訪問函式堆疊的 Lua 元素,C 程式碼還可以使用偽索引來訪問堆疊以外的 Lua 元素,例如執行緒的環境、登錄檔、函式的環境 以及 C函式的 upvalue(上值)。可以通過特別宣告來禁用偽索引。

執行緒的環境放在偽索引 LUA_GLOBALSINDEX 處,函式的環境放在偽索引 LUA_ENVIRONINDEX 處。

訪問環境的方式跟訪問表的方式是一致的,例如要訪問全域性變數的值,可以使用:

lua_getfield(L,LUA_GLOBALSINDEX,varname)

3.4 C 閉包

當我們把建立出來的函式和一些值關聯在一起,就得到了一個閉包。那些關聯起來的值稱為 upvalue (上值)。

函式的上值都放在特定的偽索引處,可以通過 lua_upvalueindex 獲取上值的偽索引。例如 lua_upvalueindex(3) 表示獲取第三個關聯值(按照關聯順序排列)對應的偽索引。

3.5 登錄檔

Lua 提供了一個登錄檔,C 程式碼可以用來存放想要存放的 Lua 值。登錄檔用偽索引 LUA_REGISTRYINDEX 定位。

為了避免命名衝突,一般採用包含庫名的字串作為鍵名。~~什麼東西?:或者可以取你自己 C 程式碼 中的一個地址,以 light userdata 的形式做鍵。~~

登錄檔中的整數鍵有特定用途(用於實現補充庫的引用系統),不建議用於其他用途。

3.6 C 中的錯誤處理

[待補充]

3.7 函式和型別

本節介紹 C API 中的函式和型別。

餘下部分見 Lua 學習筆記(下)


參考連結

BNF正規化簡介 (簡要介紹 BNF)
Lua入門系列-果凍想(對Lua進行了較為全面的介紹)
Lua快速入門(介紹 Lua 中最為重要的幾個概念,為 C/C++ 程式設計師準備)
Lua 5.1 中文手冊(全面的 Lua5.1 中文手冊)
Lua 5.3 中文手冊(雲風花了6天寫的,天哪,我看都要看6天的節奏呀)
Lua迭代器和泛型for(介紹 Lua 迭代器的詳細原理以及使用)
How do JavaScript closures work?——StackOverflow(詳細介紹了 Javascript 中閉包的概念)
Lua模式匹配(參考了此文中對 %b 的使用)
LuaSocket(LuaSocket 官方手冊)
Lua loadfile的用法, 與其他函式的比較(loadfile的介紹部分引用了此文)
Lua 的元表(對元表的描述比較有條理,通俗易懂,本文元表部分參考了此文)
設定函式環境——setfenv(解釋瞭如何方便地設定函式的環境,以及為什麼要那樣設定)
lua5.1中的setfenv使用(介紹了該環境的設定在實際中的一個應用)

相關文章