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
。
僅 nil
和 false
導致條件為假,其他均為真。
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
+++
break
和 return
break
和 return
只能寫在語句塊的最後一句,如果實在需要寫在語句塊中間,那麼就在兩個關鍵詞外面包圍 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
型的,非 true
即 false
。
支援的操作符有:
< <= ~= == > >=
不支援 !
操作符。
+++
對於 ==
操作,運算時先比較兩個運算元的型別,如果不一致則結果為 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 型
方便標記,-->
代表前面表示式的結果。
+++
userdata
與 table
的比較方式可以通過元方法 eq
進行改變。
大小比較中,數字和字串的比較與 C 語言一致。如果是其他型別的值,Lua會嘗試呼叫元方法 lt
和 le
。
2.5.3 邏輯操作符
and,or,not
僅認為 false
與 nil
為假。
not
取反操作 not
的結果為 boolean
型別。(and
和 or
的結果則不一定為 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
+++
其他
and
與 or
遵循短路原則,第二個運算元僅在需要的時候會進行求值操作。
例子
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 環境表
型別 thread
、function
和 userdata
的物件除了能與元表建立關聯外,還能關聯一個環境表。
關聯線上程上的環境表稱為全域性環境。
全域性環境作為子執行緒及子函式的預設環境。
全域性環境能夠直接被 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 的型別(例如 boolean
,table
,nil
等)。
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使用(介紹了該環境的設定在實際中的一個應用)