由 clean C 實(shí)現(xiàn)。需要被宿主程序調(diào)用,可以注入 C 函數(shù)。
采用基于 BNF 的語法規(guī)則。
Lua 對大小寫敏感。
C 語言中沒有的關(guān)鍵字有:
and
elseif
function
in
nil
local
not
or
repeat
then
until
規(guī)范:全局變量以下劃線開頭。
C 語言中沒有的操作符:
^ ~= // -- 向下取整
Lua 中沒有的操作符:
+=-=
字符表示
a -- 代表字符 a\97 -- 代表字符 a\049 -- 代表數(shù)字字符 1
其他轉(zhuǎn)義符表示
\\n -- 代表字符串 \n\n -- 代表換行
注意數(shù)字字符必須是三位。其他字符則不能超過三位。
[[]] -- 0級長括號[==[]==] -- 2級長括號
Lua 是動態(tài)語言,變量沒有類型,值才有。值自身攜帶類型信息。
Lua 有八種基本數(shù)據(jù)類型:nil, boolean, number, string, function, userdata, thread, table
。
僅 nil
和 false
導(dǎo)致條件為假,其他均為真。
userdata
類型變量用于保存 C 數(shù)據(jù)。 Lua 只能對該類數(shù)據(jù)進(jìn)行使用,而不能進(jìn)行創(chuàng)建或修改,保證宿主程序完全掌握數(shù)據(jù)。
thread
用于實(shí)現(xiàn)協(xié)程(coroutine)。
table
用于實(shí)現(xiàn)關(guān)聯(lián)數(shù)組。table
允許任何類型的數(shù)據(jù)做索引,也允許任何類型做 table
域中的值(前述任何類型
不包含 nil)。table
是 Lua 中唯一的數(shù)據(jù)結(jié)構(gòu)。
由于函數(shù)也是一種值,所以 table
中可以存放函數(shù)。
function, userdata, thread, table
這些類型的值都是對象。這些類型的變量都只是保存變量的引用,并且在進(jìn)行賦值,參數(shù)傳遞,函數(shù)返回等操作時不會進(jìn)行任何性質(zhì)的拷貝。
庫函數(shù) type()
返回變量的類型描述信息。
Lua 提供數(shù)字與字符串間的自動轉(zhuǎn)換。
可以使用 format 函數(shù)控制數(shù)字向字符串的轉(zhuǎn)換。
變量有三種類型:全局變量、局部變量、表中的域。
函數(shù)外的變量默認(rèn)為全局變量,除非用 local 顯示聲明。函數(shù)內(nèi)變量與函數(shù)的參數(shù)默認(rèn)為局部變量。
局部變量的作用域為從聲明位置開始到所在語句塊結(jié)束(或者是直到下一個同名局部變量的聲明)。
變量的默認(rèn)值均為 nil。
a = 5 -- 全局變量local b = 5 -- 局部變量function joke() c = 5 -- 局部變量 local d = 6 -- 局部變量endprint(c,d) --> nil nildo local a = 6 -- 局部變量 b = 6 -- 全局變量 print(a,b); --> 6 6endprint(a,b) --> 5 6
方便標(biāo)記,-->
代表前面表達(dá)式的結(jié)果。
對 table 的索引使用方括號 []
。Lua使用語法糖提供 .
操作。
t[i]t.i -- 當(dāng)索引為字符串類型時的一種簡化寫法gettable_event(t,i) -- 采用索引訪問本質(zhì)上是一個類似這樣的函數(shù)調(diào)用
所有全局變量放在一個環(huán)境表里,該表的變量名為 _env
。對某個全局變量 a
的訪問即 _env.a
(_env_
只是為了方便說明)。
每個函數(shù)作為變量持有一個環(huán)境表的引用,里面包含該函數(shù)可調(diào)用的所有變量。
子函數(shù)會從父函數(shù)繼承環(huán)境表。
可以通過函數(shù) getfenv / setfenv
來讀寫環(huán)境表。
支持賦值,控制結(jié)構(gòu),函數(shù)調(diào)用,還有變量聲明。
不允許空的語句段,因此 ;;
是非法的。
chunck ::= {stat[';']}
([';']
應(yīng)該是表示語句組后面 ;
是可選項。)
block ::= chunckstat ::= do block end
可以將一個語句塊顯式地寫成語句組,可以用于控制局部變量的作用范圍。
Lua 支持多重賦值。
多重賦值時,按序?qū)⒂疫叺谋磉_(dá)式的值賦值給左值。右值不足補(bǔ) nil,右值多余舍棄。
b = 1a,b = 4 -- a = 4,b = nil
+++
Lua 在進(jìn)行賦值操作時,會一次性把右邊的表達(dá)式都計算出來后進(jìn)行賦值。
i = 5i,a[i] = i+1, 7 -- i = 6 ,a[5] = 7
特別地,有
x,y = y,x -- 交換 x,y 的值
+++
對全局變量以及表的域的賦值操作含義可以在元表中更改。
if [exp] [block]elseif [exp] [block]else [block]end
while [exp] [block]end
+++
repeat [block]until [exp]
注意,由于 repeat
語句到 until
還未結(jié)束,因此在 until
之后的表達(dá)式中可以使用 block
中定義的局部變量。
例如:
a = 1c = 5repeat b = a + c c = c * 2until b > 20print(c) --> 40
+++
break
和 return
break
和 return
只能寫在語句塊的最后一句,如果實(shí)在需要寫在語句塊中間,那么就在兩個關(guān)鍵詞外面包圍do end
語句塊。
do break end
For
循環(huán)for
循環(huán)的用法比較多,單獨(dú)拎出來講。
for
中的表達(dá)式會在循環(huán)開始前一次性求值,在循環(huán)過程中不再更新。
for [Name] = [exp],[exp],[exp] do [block] end
三個 exp 分別代表初值,結(jié)束值,步進(jìn)。exp 的值均需要是一個數(shù)字。
第三個 exp 默認(rèn)為 1,可以省略。
a = 0for i = 1,6,2 do a = a + iend
等價于
int a = 0;for (int i = 1; i <= 6;i += 2){ // 取到等號,如果步進(jìn)是負(fù)的,那么會取 i >= 6 a += i;}
迭代器形式輸出一個表時,如果表中有函數(shù),則輸出的順序及個數(shù)不確定(筆者測試得出的結(jié)果,具體原因未知)。
迭代器形式的 for 循環(huán)的實(shí)質(zhì)
-- 依次返回 迭代器、狀態(tài)表、迭代器初始值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,0end-- 一個表t = {[1]="1",[2]="2"}-- 迭代形式 for 語句的 等價形式dolocal f, s, var = mypairs(t) while true do local var1, var2 = f(s, var) var = var1 if var == nil then break end -- for 循環(huán)中添加的語句 print(var1,var2) endend-- 迭代形式 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 2end
從1開始,直到數(shù)值型下標(biāo)結(jié)束或者值為 nil 時結(jié)束。
table = {[1]=1,[2]=2,[5]=5}for k,v in pairs(table) do print(v) --> 1 2 5end
遍歷整個表的鍵值對。
關(guān)于迭代器的更多內(nèi)容,可參考Lua 迭代器和泛型 for。
%
操作符Lua 中的 %
操作符與 C 語言中的操作符雖然都是取模的含義,但是取模的方式不一樣。
在 C 語言中,取模操作是將兩個操作數(shù)的絕對值取模后,在添加上第一個操作數(shù)的符號。
而在 Lua 中,僅僅是簡單的對商相對負(fù)無窮向下取整后的余數(shù)。
+++
在 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 是直接根據(jù)取模定義進(jìn)行運(yùn)算。 C 則對取模運(yùn)算做了一點(diǎn)處理。
+++
舉例:
在 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 % 6b = 5 % -6c = -5 % 6d = -5 % -6x = {a,b,c,d}for i,v in ipairs(x) do print(i,v)end--> 5--> -1--> 1--> -5
可以看到,僅當(dāng)操作數(shù)同號時,兩種語言的取模結(jié)果相同。異號時,取模結(jié)果的符號與數(shù)值均不相等。
在 Lua 中的取模運(yùn)算總結(jié)為:a % b,如果 a,b 同號,結(jié)果取 a,b 絕對值的模;異號,結(jié)果取 b 絕對值與絕對值取模后的差。取模后值的符號與 b 相同。
比較操作的結(jié)果是 boolean
型的,非 true
即 false
。
支持的操作符有:
< <= ~= == > >=
不支持 !
操作符。
+++
對于 ==
操作,運(yùn)算時先比較兩個操作數(shù)的類型,如果不一致則結(jié)果為 false。此時數(shù)值與字符串之間并不會自動轉(zhuǎn)換。
比較兩個對象是否相等時,僅當(dāng)指向同一內(nèi)存區(qū)域時,判定為 true
?!?/p>
a = 123b = 233c = "123"d = "123"e = {1,2,3}f = eg = {1,2,3}print(a == b) --> falseprint(a == c) --> false -- 數(shù)字與字符串作為不同類型進(jìn)行比較print(c == d) --> true print(e == f) --> true -- 引用指向相同的對象print(e == g) --> false -- 雖然內(nèi)容相同,但是是不同的對象print(false == nil) --> false -- false 是 boolean,nil 是 nil 型
方便標(biāo)記,-->
代表前面表達(dá)式的結(jié)果。
+++
userdata
與 table
的比較方式可以通過元方法 eq
進(jìn)行改變。
大小比較中,數(shù)字和字符串的比較與 C 語言一致。如果是其他類型的值,Lua會嘗試調(diào)用元方法 lt
和 le
。
and,or,not
僅認(rèn)為 false
與 nil
為假。
not
取反操作 not
的結(jié)果為 boolean
類型。(and
和 or
的結(jié)果則不一定為 boolean
)
b = not a -- a 為 nil,b 為 truec = not not a -- c 為 false
and
a and b
,如果 a
為假,返回 a
,如果 a
為真, 返回 b
。
注意,為什么 a
為假的時候要返回 a
呢?有什么意義?這是因為 a
可能是 false
或者 nil
,這兩個值雖然都為假,但是是有區(qū)別的。
or
a or b
,如果 a
為假,返回 b
,如果 a
為真, 返回 a
。與 and
相反。
+++
提示: 當(dāng)邏輯操作符用于得出一個 boolean
型結(jié)果時,不需要考慮邏輯運(yùn)算后返回誰的問題,因為邏輯操作符的操作結(jié)果符合原本的邏輯含義。
舉例
if (not (a > min and a < max)) then -- 如果 a 不在范圍內(nèi),則報錯 error() end
+++
and
與 or
遵循短路原則,第二個操作數(shù)僅在需要的時候會進(jìn)行求值操作。
例子
a = 5x = a or jjjj() -- 雖然后面的函數(shù)并沒有定義,但是由于不會執(zhí)行,因此不會報錯。print(a) -->5print(x) -->5
通過上面這個例子,我們應(yīng)當(dāng)對于邏輯操作有所警覺,因為這可能會引入一些未能及時預(yù)料到的錯誤。
..
連接兩個字符串(或者數(shù)字)成為新的字符串。對于其他類型,調(diào)用元方法 concat
。
#
對于字符串,長度為字符串的字符個數(shù)。
對于表,通過尋找滿足t[n] 不是 nil 而 t[n+1] 為 nil 的下標(biāo) n 作為表的長度。
~~對于其他類型呢?~~
-- 字符串取長print(#"abc\0") --> 4-- 表取長print(#{[1]=1,[2]=2,[3]=3,x=5,y=6}) --> 3print(#{[1]=1,[2]=nil,[3]=3,x=5,y=6}) --> 1
由低到高:
orand < > <= >= ~= == .. + - * / % not # - (unary) ^
冪運(yùn)算>單目運(yùn)算>四則運(yùn)算>連接符>比較操作符>and>or
Table 構(gòu)造的 BNF 定義
tableconstructor ::= `{′ [fieldlist] `}′fieldlist ::= field {fieldsep field} [fieldsep]field ::= `[′ exp `]′ `=′ exp | Name `=′ exp | expfieldsep ::= `,′ | `;′
舉例:
a = {}b = {["price"] = 5; cost = 4; 2+5}c = { [1] = 2+5, [2] = 2, 8, price = "abc", ["cost"] = 4} -- b 和 c 構(gòu)造的表是等價的print(b["price"]) --> 5print(b.cost) --> 4print(b[1]) --> 7 -- 未給出鍵值的,按序分配下標(biāo),下標(biāo)從 1 開始print(c["price"]) --> abcprint(c.cost) --> 4print(c[1]) --> 8 print(c[2]) --> 2
注意:
上面這兩條的存在使得上面的例子中 c1 的輸出值為 8。
+++
如果表中有相同的鍵,那么以靠后的那個值作為鍵對應(yīng)的值。
a = {[1] = 5,[1] = 6} -- 那么 a[1] = 6
+++
如果表的最后一個域是表達(dá)式形式,并且是一個函數(shù),那么這個函數(shù)的所有返回值都會加入到表中。
a = 1function order() a = a + 1 return 1,2,3,4endb = {order(); a; order(); }c = {order(); a; (order());}print(b[1]) --> 1 print(b[2]) --> 2 -- 表中的值并不是一次把表達(dá)式都計算結(jié)束后再賦值的print(b[3]) --> 1 print(b[4]) --> 2 -- 表達(dá)式形式的多返回值函數(shù)print(#b) --> 6 -- 表的長度為 6 print(#c) --> 3 -- 函數(shù)添加括號后表的長度為 3
函數(shù)是一個表達(dá)式,其值為 function 類型的對象。函數(shù)每次執(zhí)行都會被實(shí)例化。
Lua 中實(shí)現(xiàn)一個函數(shù)可以有以下三種形式。
f = function() [block] endlocal f; f = function() [block] enda.f = function() [block] end
Lua 提供語法糖分別處理這三種函數(shù)定義。
function f() [block] endlocal function f() [block] endfunction a.f() [block] end
+++
上面 local
函數(shù)的定義之所以不是 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 endend
形參會通過實(shí)參來初始化為局部變量。
參數(shù)列表的尾部添加 ...
表示函數(shù)能接受不定長參數(shù)。如果尾部不添加,那么函數(shù)的參數(shù)列表長度是固定的。
f(a,b)g(a,b,...)h(a,...,b) -- 編譯錯誤
f(1) --> a = 1, b = nilf(1,2) --> a = 1, b = 2f(1,2,3) --> a = 1, b = 2g(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的函數(shù)的定義方式:
a.f = function (self, params) [block] end
其語法糖形式為:
function a:f(params) [block] end
使用舉例:
a = {name = "唐衣可俊"}function a:f() print(self.name)enda:f() --> 唐衣可俊 -- 如果這里使用 a.f(),那么 self.name 的地方會報錯 attempt to index local 'self';此時應(yīng)該寫為 a.f(a)
:
的作用在于函數(shù)定義與調(diào)用的時候可以少寫一個 self
參數(shù)。這種形式是對方法
的模擬
Lua 中的函數(shù)調(diào)用的BNF語法如下:
functioncall ::= prefixexp args
如果 prefixexp 的值的類型是 function, 那么這個函數(shù)就被用給出的參數(shù)調(diào)用。 否則 prefixexp 的元方法 "call" 就被調(diào)用, call 的第一個參數(shù)就是 prefixexp 的值,接下來的是 args 參數(shù)列表(參見 2.8 元表 | Metatable)。
函數(shù)調(diào)用根據(jù)是否傳入 self
參數(shù)分為 .
調(diào)用和 :
調(diào)用。
函數(shù)調(diào)用根據(jù)傳入?yún)?shù)的類型,可以分為參數(shù)列表調(diào)用、表調(diào)用、字符串調(diào)用。
[待完善]
如果一個函數(shù)訪問了它的外部變量,那么它就是一個閉包。
由于函數(shù)內(nèi)部的變量均為局部變量,外界無法對其進(jìn)行訪問。這時如果外界想要改變局部變量的值,那么就可以使用閉包來實(shí)現(xiàn)這一目的。
具體的實(shí)現(xiàn)過程大致是這樣,函數(shù)內(nèi)部有能夠改變局部變量的子函數(shù),函數(shù)將這個子函數(shù)返回,那么外界就可以通過使用這個子函數(shù)來操作局部變量了。
例子:利用閉包來實(shí)現(xiàn)對局部變量進(jìn)行改變
-- 實(shí)現(xiàn)一個迭代器function begin(i) local cnt = i return function () -- 這是一個匿名函數(shù),實(shí)現(xiàn)了自增的功能;同時它也是一個閉包,因為訪問了外部變量 cnt cnt = cnt + 1 return cnt endenditerator = begin(2) -- 設(shè)置迭代器的初值為 2 ,返回一個迭代器函數(shù)print(iterator()) -- 執(zhí)行迭代print(iterator())
提示: 關(guān)于閉包的更多說明可參考JavaScript 閉包是如何工作的?——StackOverflow
即變量的作用域,見 2.3 變量 部分。
[待補(bǔ)充]
我們可以使用操作符對 Lua 的值進(jìn)行運(yùn)算,例如對數(shù)值類型的值進(jìn)行加減乘除的運(yùn)算操作以及對字符串的連接、取長操作等(在 2.5 表達(dá)式 這一節(jié)中介紹了許多類似的運(yùn)算)。元表正是定義這些操作行為的地方。
元表本質(zhì)上是一個普通 Lua 表。元表中的鍵用來指定操作,稱為“事件名”;元表中鍵所關(guān)聯(lián)的值稱為“元方法”,定義操作的行為。
僅表(table)類型值對應(yīng)的元表可由用戶自行定義。其他類型的值所對應(yīng)的元表僅能通過 Debug 庫進(jìn)行修改。
元表中的事件名均以兩條下劃線 __
作為前綴,元表支持的事件名有如下幾個:
__index -- 'table[key]',取下標(biāo)操作,用于訪問表中的域__newindex -- 'table[key] = value',賦值操作,增改表中的域__call -- 'func(args)',函數(shù)調(diào)用,參見 [2.5.9 函數(shù)調(diào)用](#2-5-9)-- 數(shù)學(xué)運(yùn)算操作符__add -- '+'__sub -- '-'__mul -- '*'__div -- '/'__mod -- '%'__pow -- '^'__unm -- '-'-- 連接操作符__concat -- '..'-- 取長操作符__len -- '#'-- 比較操作符__eq -- '=='__lt -- '<' -- a > b 等價于 b < a__le -- '<=' -- a >= b 等價于 b <= a
還有一些其他的事件,例如 __tostring
和 __gc
等。
下面進(jìn)行詳細(xì)介紹。
每個值都可以擁有一個元表。對 userdata 和 table 類型而言,其每個值都可以擁有獨(dú)立的元表,也可以幾個值共享一個元表。對于其他類型,一個類型的值共享一個元表。例如所有數(shù)值類型的值會共享一個元表。除了字符串類型,其他類型的值默認(rèn)是沒有元表的。
使用 getmetatable 函數(shù)可以獲取任意值的元表。getmetatable (object)
使用 setmetatable 函數(shù)可以設(shè)置表類型值的元表。setmetatable (table, metatable)
只有字符串類型的值默認(rèn)擁有元表:
a = "5"b = 5c = {5}print(getmetatable(a)) --> table: 0x7fe221e06890print(getmetatable(b)) --> nilprint(getmetatable(c)) --> nil
事先提醒 Lua 使用 raw
前綴的函數(shù)來操作元方法,避免元方法的循環(huán)調(diào)用。
例如 Lua 獲取對象 obj 中元方法的過程如下:
rawget(getmetatable(obj)or{}, "__"..event_name)
index 是元表中最常用的事件,用于值的下標(biāo)訪問 -- table[key]
。
事件 index 的值可以是函數(shù)也可以是表。當(dāng)使用表進(jìn)行賦值時,元方法可能引發(fā)另一次元方法的調(diào)用,具體可見下面?zhèn)未a介紹。
當(dāng)用戶通過鍵值來訪問表時,如果沒有找到鍵對應(yīng)的值,則會調(diào)用對應(yīng)元表中的此事件。如果 index 使用表進(jìn)行賦值,則在該表中查找傳入鍵的對應(yīng)值;如果 index 使用函數(shù)進(jìn)行賦值,則調(diào)用該函數(shù),并傳入表和鍵。
Lua 對取下標(biāo)操作的處理過程用偽碼表示如下:
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 -- 訪問不成功則嘗試調(diào)用元表的 index h = metatable(table).__index -- 元表不存在返回 nil if h == nil then return nil end else -- 不是對表進(jìn)行訪問則直接嘗試元表 h = metatable(table).__index -- 無法處理導(dǎo)致出錯 if h == nil then error(···); end end -- 根據(jù) index 的值類型處理 if type(h) == "function" then return h(table, key) -- 調(diào)用處理器 else return h[key] -- 或是重復(fù)上述操作 endend
例子:
使用表賦值:
t = {[1] = "cat",[2] = "dog"}print(t[3]) --> nilsetmetatable(t, {__index = {[3] = "pig", [4] = "cow", [5] = "duck"}})print(t[3]) --> pig
使用函數(shù)賦值:
t = {[1] = "cat",[2] = "dog"}print(t[3]) --> nilsetmetatable(t, {__index = function (table,key) key = key % 2 + 1 return table[key]end})print(t[3]) --> dog
newindex 用于賦值操作 -- talbe[key] = value
。
事件 newindex 的值可以是函數(shù)也可以是表。當(dāng)使用表進(jìn)行賦值時,元方法可能引發(fā)另一次元方法的調(diào)用,具體可見下面?zhèn)未a介紹。
當(dāng)操作類型不是表或者表中尚不存在傳入的鍵時,會調(diào)用 newindex 的元方法。如果 newindex 關(guān)聯(lián)的是一個函數(shù)類型以外的值,則再次對該值進(jìn)行賦值操作。反之,直接調(diào)用函數(shù)。
~~不是太懂:一旦有了 "newindex" 元方法, Lua 就不再做最初的賦值操作。 (如果有必要,在元方法內(nèi)部可以調(diào)用 rawset 來做賦值。)~~
Lua 進(jìn)行賦值操作時的偽碼如下:
function settable_event (table, key, value) local h if type(table) == "table" then -- 修改表中的 key 對應(yīng)的 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) -- 調(diào)用處理器 else h[key] = value -- 或是重復(fù)上述操作 endend
例子:
元方法為表類型:
t = {}mt = {}setmetatable(t, {__newindex = mt})t.a = 5print(t.a) --> nilprint(mt.a) --> 5
通過兩次調(diào)用 newindex 元方法將新的域添加到了表 mt 。
+++
元方法為函數(shù):
-- 對不同類型的 key 使用不同的賦值方式t = {}setmetatable(t, {__newindex = function (table,key,value) if type(key) == "number" then rawset(table, key, value*value) else rawset(table, key, value) endend})t.name = "product"t[1] = 5print(t.name) --> productprint(t[1]) --> 25
call 事件用于函數(shù)調(diào)用 -- function(args)
。
Lua 進(jìn)行函數(shù)調(diào)用操作時的偽代碼:
function function_event (func, ...) if type(func) == "function" then return func(...) -- 原生的調(diào)用 else -- 如果不是函數(shù)類型,則使用 call 元方法進(jìn)行函數(shù)調(diào)用 local h = metatable(func).__call if h then return h(func, ...) else error(···) end endend
例子:
由于用戶只能為表類型的值綁定自定義元表,因此,我們可以對表進(jìn)行函數(shù)調(diào)用,而不能把其他類型的值當(dāng)函數(shù)使用。
-- 把數(shù)據(jù)記錄到表中,并返回數(shù)據(jù)處理結(jié)果t = {}setmetatable(t, {__call = function (t,a,b,factor) t.a = 1;t.b = 2;t.factor = factor return (a + b)*factorend})print(t(1,2,0.1)) --> 0.3print(t.a) --> 1print(t.b) --> 2print(t.factor) --> 0.1
運(yùn)算操作符相關(guān)元方法自然是用來定義運(yùn)算的。
以 add 為例,Lua 在實(shí)現(xiàn) add 操作時的偽碼如下:
function add_event (op1, op2) -- 參數(shù)可轉(zhuǎn)化為數(shù)字時,tonumber 返回數(shù)字,否則返回 nil local o1, o2 = tonumber(op1), tonumber(op2) if o1 and o2 then -- 兩個操作數(shù)都是數(shù)字? return o1 + o2 -- 這里的 '+' 是原生的 'add' else -- 至少一個操作數(shù)不是數(shù)字時 local h = getbinhandler(op1, op2, "__add") -- 該函數(shù)的介紹在下面 if h then -- 以兩個操作數(shù)來調(diào)用處理器 return h(op1, op2) else -- 沒有處理器:缺省行為 error(···) end endend
代碼中的 getbinhandler 函數(shù)定義了 Lua 怎樣選擇一個處理器來作二元操作。 在該函數(shù)中,首先,Lua 嘗試第一個操作數(shù)。如果這個操作數(shù)所屬類型沒有定義這個操作的處理器,然后 Lua 會嘗試第二個操作數(shù)。
function getbinhandler (op1, op2, event) return metatable(op1)[event] or metatable(op2)[event] end
+++
對于一元操作符,例如取負(fù),Lua 在實(shí)現(xiàn) unm 操作時的偽碼:
function unm_event (op) local o = tonumber(op) if o then -- 操作數(shù)是數(shù)字? return -o -- 這里的 '-' 是一個原生的 'unm' else -- 操作數(shù)不是數(shù)字。 -- 嘗試從操作數(shù)中得到處理器 local h = metatable(op).__unm if h then -- 以操作數(shù)為參數(shù)調(diào)用處理器 return h(op) else -- 沒有處理器:缺省行為 error(···) end endend
例子:
加法的例子:
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 endend})t.num = 5print(t + 3) --> 8
取負(fù)的例子:
t = {}setmetatable(t, {__unm = function (a) return -a.numend})t.num = 5print(-t) --> -5
對于連接操作,當(dāng)操作數(shù)中存在數(shù)值或字符串以外的類型時調(diào)用該元方法。
對于取長操作,如果操作數(shù)不是字符串類型,也不是表類型,則嘗試使用元方法(這導(dǎo)致自定義的取長基本沒有,在之后的版本中似乎做了改進(jìn))。
對于三種比較類操作,均需要滿足兩個操作數(shù)為同類型,且關(guān)聯(lián)同一個元表時才能使用元方法。
對于 eq (等于)比較操作,如果操作數(shù)所屬類型沒有原生的等于比較,則調(diào)用元方法。
對于 lt (小于)與 le (小于等于)兩種比較操作,如果兩個操作數(shù)同為數(shù)值或者同為字符串,則直接進(jìn)行比較,否則使用元方法。
對于 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 cntend})-- 結(jié)果是 6 而不是預(yù)期中的 3print(#t) --> 6
等于比較操作:
t = {name="number",1,2,3}t2 = {name = "number",4,5,6}mt = {__eq = function (a,b) return a.name == b.nameend}setmetatable(t,mt) -- 必須要關(guān)聯(lián)同一個元表才能比較setmetatable(t2,mt)print(t==t2) --> true
tostring 操作:
t = {num = "a table"}print(t) --> table: 0x7f8e83c0a820mt = {__tostring = function(t) return t.numend}setmetatable(t, mt)print(tostring(t)) --> a tableprint(t) --> a table
類型 thread
、function
和 userdata
的對象除了能與元表建立關(guān)聯(lián)外,還能關(guān)聯(lián)一個環(huán)境表。
關(guān)聯(lián)在線程上的環(huán)境表稱為全局環(huán)境。
全局環(huán)境作為子線程及子函數(shù)的默認(rèn)環(huán)境。
全局環(huán)境能夠直接被 C 調(diào)用。
關(guān)聯(lián)在 Lua 函數(shù)上的環(huán)境表接管函數(shù)對全局變量的所有訪問。并且作為子函數(shù)的默認(rèn)環(huán)境。
關(guān)聯(lián)在 C 函數(shù)上的環(huán)境能直接被 C 調(diào)用。
關(guān)聯(lián)在 userdata
上的環(huán)境沒有實(shí)際的用途,只是為了方便程序員把一個表關(guān)聯(lián)到 userdata
上。
[待補(bǔ)充]
弱表是包含弱引用的表。
弱表的弱引用方式有三種。鍵弱引用,值弱引用,鍵和值均弱引用。
可以通過元表中的 __mode
域來設(shè)置一個表是否有弱引用,以及弱引用的方式。
a = {}b = { __mode = "k"} -- 引號中添加 k 表示 key 弱引用,v 表示 value 弱引用, kv 表示均弱引用。setmetable(a,b) -- b 是 a 的元表,綁定后就不能在更改 __mode 的值。
垃圾回收機(jī)制會把弱引用的部分回收。但是不論是哪種弱引用,回收機(jī)制都會把整個鍵值對從弱表中移除。
這部分描述 Lua 的 C API,即用來與 Lua 進(jìn)行通信的 C 函數(shù),所有的函數(shù)和常量都定義在 lua.h
頭文件里面。
有一部分 C 函數(shù)是用宏來實(shí)現(xiàn)的。~~為什么?:由于所有的宏只會使用他們的參數(shù)一次(除了第一個參數(shù),即Lua 狀態(tài)機(jī)),所以不必?fù)?dān)心宏展開帶來的副作用。~~
默認(rèn)情況下 Lua 在進(jìn)行函數(shù)調(diào)用時不會檢查函數(shù)的有效性和堅固性,如果想要進(jìn)行檢查,則使用 luaconf.h
中的 luai_apicheck()
函數(shù)開啟。
Lua 調(diào)用 C API 時使用一個虛擬棧來傳遞參數(shù),棧中的所有元素都是 Lua 的類型(例如boolean
,table
,nil
等)。
Lua 調(diào)用 C 函數(shù)的時候都會新建一個虛擬棧,而不是使用舊?;蛘咂渌臈!M瑫r在 C 函數(shù)中,對 Lua API 調(diào)用時,只能使用當(dāng)前調(diào)用所對應(yīng)棧中的元素,其他棧的元素是無法訪問的。
虛擬棧中包含 C 函數(shù)所需的所有參數(shù),函數(shù)的返回值也都放在該棧中。
這里所謂的棧概念并不是嚴(yán)格意義上的棧,可以通過下標(biāo)對棧中的元素進(jìn)行訪問。1表示棧底,-1表示棧頂,又例如 3 表示從棧底開始的第三個元素。
由于 Lua 的 C API 默認(rèn)不做有效性和堅固性(魯棒性)檢測,因此開發(fā)人員有責(zé)任保證堅固性。特別要注意的是,不能讓堆棧溢出。Lua 只保證棧大小會大于 LUA_MINSTACK
(一般是 20)。開發(fā)人員可以使用lua_checkstack
函數(shù)來手動設(shè)置棧的大小。
除了用索引訪問函數(shù)堆棧的 Lua 元素,C 代碼還可以使用偽索引來訪問堆棧以外的 Lua 元素,例如線程的環(huán)境、注冊表、函數(shù)的環(huán)境 以及 C函數(shù)的 upvalue
(上值)??梢酝ㄟ^特別聲明來禁用偽索引。
線程的環(huán)境放在偽索引 LUA_GLOBALSINDEX
處,函數(shù)的環(huán)境放在偽索引 LUA_ENVIRONINDEX
處。
訪問環(huán)境的方式跟訪問表的方式是一致的,例如要訪問全局變量的值,可以使用:
lua_getfield(L,LUA_GLOBALSINDEX,varname)
當(dāng)我們把創(chuàng)建出來的函數(shù)和一些值關(guān)聯(lián)在一起,就得到了一個閉包。那些關(guān)聯(lián)起來的值稱為 upvalue
(上值)。
函數(shù)的上值都放在特定的偽索引處,可以通過 lua_upvalueindex
獲取上值的偽索引。例如lua_upvalueindex(3)
表示獲取第三個關(guān)聯(lián)值(按照關(guān)聯(lián)順序排列)對應(yīng)的偽索引。
Lua 提供了一個注冊表,C 代碼可以用來存放想要存放的 Lua 值。注冊表用偽索引 LUA_REGISTRYINDEX
定位。
為了避免命名沖突,一般采用包含庫名的字符串作為鍵名。~~什么東西?:或者可以取你自己 C 代碼 中的一個地址,以 light userdata 的形式做鍵。~~
注冊表中的整數(shù)鍵有特定用途(用于實(shí)現(xiàn)補(bǔ)充庫的引用系統(tǒng)),不建議用于其他用途。
[待補(bǔ)充]
本節(jié)介紹 C API 中的函數(shù)和類型。
余下部分見 Lua 學(xué)習(xí)筆記(下)
BNF范式簡介 (簡要介紹 BNF)
Lua入門系列-果凍想(對Lua進(jìn)行了較為全面的介紹)
Lua快速入門(介紹 Lua 中最為重要的幾個概念,為 C/C++ 程序員準(zhǔn)備)
Lua 5.1 中文手冊(全面的 Lua5.1 中文手冊)
Lua 5.3 中文手冊(云風(fēng)花了6天寫的,天哪,我看都要看6天的節(jié)奏呀)
Lua迭代器和泛型for(介紹 Lua 迭代器的詳細(xì)原理以及使用)
How do JavaScript closures work?——StackOverflow(詳細(xì)介紹了 Javascript 中閉包的概念)
Lua模式匹配(參考了此文中對 %b
的使用)
LuaSocket(LuaSocket 官方手冊)
Lua loadfile的用法, 與其他函數(shù)的比較(loadfile的介紹部分引用了此文)
Lua 的元表(對元表的描述比較有條理,通俗易懂,本文元表部分參考了此文)
設(shè)置函數(shù)環(huán)境——setfenv(解釋了如何方便地設(shè)置函數(shù)的環(huán)境,以及為什么要那樣設(shè)置)
lua5.1中的setfenv使用(介紹了該環(huán)境的設(shè)置在實(shí)際中的一個應(yīng)用)
聯(lián)系客服