盡管Lua是一門解析型的語言,但是在運行前也會被編譯成某個中間狀態。一門解析型的語言需要編譯,這聽起來有點不合常理。但是,實際上,解析型語言的與眾不同,不是說它不需要編譯,而是說它把編譯作為其運行時的一部分,因此,它就可以執行各種來自外部的代碼(例如網上的)。也許因為Lua中存在的如*dofile* 這樣的函數,才使Lua可以被稱為一門解析型語言。
1. 編譯
之前我們介紹了*dofile* 來執行代碼塊,但是*dofile* 只是一個輔助函數。這里介紹一下*loadfile* 函數,它會從一個file中加載語句塊,但是不運行;而是僅僅編譯并作為一個函數返回。*loadfile* 不會像*dofile* 那樣在運行時直接報錯退出,而是返回錯誤碼,這樣我們就可以根據錯誤碼做相應的處理。我們可以像下面這樣定義*dofile* ,這也可以看出*dofile* 和*loadfile* 的區別
~~~
function dofile (filename)
local f = assert(loadfile(filename))
return f()
end
~~~
注意,*assert* 可以使*loadfile* 發生錯誤時,報錯退出。
*dofile* 在處理有些簡單的任務時,使用起來比較方便,只需要一次調用,它會完成所有的操作(編譯,運行啥的)。但是,*loadfile*更加靈活。發生錯誤的時候,*loadfile* 會返回**nil**+ 'err_msg',我們可以根據實際情況對錯誤做出相應的處理。除此之外,如果想要運行一個file多次,可以先調用一次*loadfile* ,然后調用*loadfile* 返回的結果多次,就可以了。這比調用幾次*dofile* 開銷要小很多,因為*loadfile* 只會執行一次編譯,而*dofile* 每次都用都要編譯。
*loadstring* 跟*loadfile* 差不多,區別是從一個string中加載代碼塊,而不是從file中。例如:
~~~
f = loadstring("i = i + 1")
~~~
*f* 是*loadstring* 的返回值,應該是function類型, 調用的時候,會執行i = i + 1 :?
~~~
i = 0
f(); print(i) --> 1
f(); print(i) --> 2
~~~
*loadstring* 函數功能非常強大,但是運行起來,開銷也不小,并且有時會導致產生一些莫名其妙的代碼。因此,在用*loadstring* 之前,先考慮下有沒有更簡單的辦法。
下面這行代碼,不太好看,但是很方便。(不鼓勵這種調用方法)
~~~
loadstring(s)()
~~~
如果有語法錯誤,*loadstring* 會返回**nil?**+ 類似‘attempt to call a nil value’這樣的err_msg。如果想獲取更詳細的err_msg,那就需要用*assert* :
~~~
assert(loadstring(s))()
~~~
下面這樣的使用方式(對一個字面值string使用loadstring),沒什么意思,
~~~
f = loadstring("i = i + 1")
~~~
粗略的等價于:
~~~
f = function () i = i + 1 end
~~~
但是,也不是完全相同,且繼續往下看。第二種方式的代碼運行起來更快,因為它只需要編譯一次,而*loadstring* 每次都需要編譯。下面我們來看看,上面兩段代碼到底有什么不同,如下示例:
~~~
i = 32
local i = 0
f = loadstring("i = i + 1; print(i)")
g = function () i = i + 1; print(i) end
f() --> 33
g() --> 1
~~~
*g* 函數處理的事局部變量i , 而*f* 函數處理的是全局變量i ,*loadstring* 總是在全局環境中進行編譯。
*loadstring* 最典型的用途是:運行外來代碼,例如網絡上的,別人的。。。注意,*loadsting* 只能load語句,不能load表達式, 如果你想算一個表達式的值,那么前面要加上一個*return* 來返回給定表達式的值。下面是一個示例幫助理解:
~~~
print "enter your expression:"
local l = io.read()
local func = assert(loadstring("return " .. l))
print("the value of your expression is " .. func())
~~~
看一下運行情況:(注意第一個為什么報錯了,想想什么才叫表達式)

*loadstring* 返回的就是一個普通的函數,可以多次調用:
~~~
print "enter function to be plotted (with variable 'x'):"
local l = io.read()
local f = assert(loadstring("return " .. l))
for i=1,20 do
x = i -- global 'x' (to be visible from the chunk)
print(string.rep("*", f()))
end
~~~
(*string.rep* 函數復制一個string給定的次數),下面是運行結果:

如果我們在深究一下,其實不管*loadstring* 也好,*loadfile* 也好,Lua中最基礎的函數是*load* 。*loadfile* 從一個file中加載代碼塊,*loadstring* 從一個string中加載代碼塊,而*load* 調用一個*reader* 函數來獲取代碼塊,這個*reader* 函數分塊返回代碼塊,*load* 調用它,直到它返回**nil**。我們很少使用*load* 函數;通常只有在代碼塊不是位于一個file中,但是又太大了,不適合放到內存中(如果適合放到內存中,那就可以用*loadstring* 了)的時候,才會用load 。
Lua將每一個獨立的代碼塊看作是一個含有不定數量參數的匿名函數。例如,*loadstring*("a = 1")跟下面的表達式基本等價:
~~~
function (...) a = 1 end
~~~
跟其他函數一樣,代碼塊也可以聲明局部變量:
~~~
f = loadstring("local a = 10; print(a + 20)")
f() --> 30
~~~
利用這個特性,我們可以重寫上面的一個例子:
~~~
print "enter function to be plotted (with variable 'x'):"
local l = io.read()
local f = assert(loadstring("local x = ...; return " .. l))
for i=1,20 do
print(string.rep("*", f(i)))
end
~~~
在代碼的開始處,加了“local x = ...”,將x聲明為局部變量。調用*f*? 函數的時候,實參i 就變成了變參表達式"..."的值。運行結果,跟上面一樣,就不截圖了。
*load?*函數不會發生運行時錯誤崩潰。如果出錯了,總是返回**nil**+err_msg:

一個很常見的誤解:將load代碼塊與定義函數劃等號。在Lua中,函數定義實際上是賦值行為,而且是在運行時才發生,而不是在編譯時。例如,我們有個文件foo.lua:
~~~
function foo (x)
print(x)
end
~~~
接著運行下面這條cmd:
~~~
f = loadfile("foo.lua")
~~~
這個時候,foo被編譯了,但是還沒有被定義。要定義它,必須運行這個代碼塊:
~~~
print(foo) --> nil
f() -- defines 'foo'
foo("ok") --> ok
~~~
在生產環境中的程序,在運行外部代碼的時候,要盡可能的捕獲所有的錯誤并作出處理。甚至,如果這些代碼不被信任,那就應該在一個安全的環境中運行,避免在運行這些外來代碼的時候,產生一些不愉快的事情。
## 2. C代碼
不像Lua代碼,C代碼必須先跟程序鏈接才能被使用。在大多數系統中,做這個鏈接動作的做簡單的方法是:使用動態鏈接功能。那么怎么檢測你的環境是否已經支持這個功能呢?
運行 *print(package.loadlib("a", "b"))*。如果出現類似說文件不存在這樣的錯誤,那么就說明你的環境支持這個動態鏈接功能啦。否則,出現的錯誤信息會告訴你這個功能不支持,或者沒有被安裝。下面是我的環境的表現:

*package.loadlib* 有兩個string類型的參數,第一個是庫的完整路徑,第二個是函數的名字。因此,一個典型的調用應該跟下面很相似:
~~~
local path = "/usr/local/lib/lua/5.1/socket.so"
local f = package.loadlib(path, "luaopen_socket")
~~~
*loadlib* 函數,加載一個給定的庫,并鏈接,但是并沒有去調用那個函數。而是將這個C 函數當作Lua函數返回。如果這個過程出現什么錯誤,那么*loadlib* 會返回**nil**+ err_msg。
要使用*loadlib* 函數,我們必須要給出庫的完整路徑和準確的函數名字,好像有點麻煩。這里有個替代方案,*require* 函數。我們后面在討論這個函數,這里只需知道有這么個東西就可以了。
## 3. 錯誤
是個人就難免會犯錯。因此我們要盡可能的處理可捕獲的錯誤。因為Lua是一個擴展語言,通常是被嵌入到別的程序(姑且叫做宿主程序吧)中,在出錯的時候,不能簡單的讓它崩潰掉或者退出。而是結束執行當前的代碼塊,并返回到宿主程序中。
Lua遇到任何非期望的條件,都會產生一個錯誤。例如,對非數值進行加運算,調用一個非函數的值,索引一個非table的值,等等。可以顯式地調用error 函數來產生一個錯誤。例如:
~~~
print "enter a number:"
n = io.read("*number")
if not n then error("invalid input") end
~~~
*if not condition then error end*,在Lua中被封裝成了assert 函數:
~~~
print "enter a number:"
n = assert(io.read("*number"), "invalid input")
~~~
*assert*?檢查它的第一個參數,如果為**nil**或者**false**,就產生一個error。第二個參數是可選的。
在函數發現錯誤時,有兩種處理方式,一個是返回error code,另一個是產生錯誤(聯想下C語言中的*assert*)。如何選擇呢?建議如下:可以輕松避免的錯誤,可以通過編碼來修改并規避的,產生錯誤;否則返回errcode。
舉個例子,sin 函數,如果參數用了一個table,假設它返回了一個error code,如果我們需要去檢查一下這個錯誤,那么代碼應該像下面這樣寫:
~~~
local res = math.sin(x)
if not res then -- error?
<error-handling code>
~~~
但是,實際上,我們可以在調用sin 函數之前就檢查一下x 是否合法:
~~~
if not tonumber(x) then -- x is not a number?
<error-handling code>
~~~
如果參數x不是一個數值,那么意味著你的程序中某個地方出錯了。這種情況下,停止運行并產生一個錯誤信息,是最簡單有效的方式來處理這個錯誤。
我們再來看下*io.open* 函數,當我去open一個并不存在的file時,會怎么樣呢?這個情況,并不能提前檢查這個file是否存在,因為在很多系統中,要想知道某個file是否存在,只有去嘗試打開它。因此,如果函數*io.open* 因為一些外部原因(例如file does not exist, permisson denied)而不能打開一個file,它會返回**nil**+ err_msg。這樣的話,我們就可以進行一些處理,例如要求用戶重新輸入一個文件名:
~~~
local file, msg
repeat
print "enter a file name:"
local name = io.read()
if not name then return end -- no input
file, msg = io.open(name, "r")
if not file then print(msg) end
until file
~~~
如果僅僅希望保證*io.open* 能夠正常工作,可以簡單的使用:
~~~
file = assert(io.open(name, "r"))
~~~
這是Lua中的一個習慣用法,如果*io.open*失敗了,就會產生一個錯誤。
## 4. 錯誤處理和異常
對大多數程序來說,不需要在Lua代碼中進行錯誤處理,宿主程序本身會對錯誤進行相應處理。所有的Lua動作基本都是由宿主程序調用起來的,如果發生錯誤,Lua代碼塊只需要返回相應的err_code,宿主程序本身針對err_code做出相應的處理。在獨立的Lua解析器中,出錯的時候,也只是打印出相應的錯誤信息,然后繼續提示用戶繼續進行運行其他的命令。
如果想要在Lua中對錯誤進行處理,那么必須用*pcall* (protected call)函數來封裝代碼。
假設我們運行一段lua代碼,并捕獲到運行過程中出現的錯誤。我們首先要做的就是封裝這段代碼,假設封裝成函數*foo* :
~~~
function foo ()
<some code>
if unexpected_condition then error() end
<some code>
print(a[i]) -- potential error: 'a' may not be a table
<some code>
end
~~~
然后用*pcall*?去調用這個函數:
~~~
if pcall(foo) then
-- no errors while running 'foo'
<regular code>
else
-- 'foo' raised an error: take appropriate actions
<error-handling code>
end
~~~
上面的*foo* 函數也可以替換成匿名函數的。
*pcall* 會在protected mode下調用它的第一個參數,以便能夠在函數運行過程中捕獲到出現的錯誤。如果函數運行正常,沒有錯誤產生,*pcall* 返回**true**+ 函數的返回值;如果出現錯誤,*pcall* 返回**false**+ err_msg。
err_msg不一定非得是string,任何傳遞給error 的值都會被pcall 返回,例如下面的示例:
~~~
local status, err = pcall(function () error({code=121}) end)
print(status, err.code, type(err))
~~~
運行結果如下:

## 5. 錯誤信息和堆棧
像上面說的,任何類型的值都可以作為err_msg,但是,通常err_msg還是string類型的,說明發生了什么錯誤。當遇到了內部錯誤(例如試圖索引一個非table值),Lua負責產生err_msg;否則,err_msg就是傳遞給error 函數的值。另外,Lua總是在錯誤發生的地方添加一些位置信息, 如下示例:
~~~
local status, err = pcall(function () a = "a"+1 end)
print(err)
~~~

~~~
local status, err = pcall(function () error("my error") end)
print(err)
~~~

位置信息指明了filename和line number。
*error* 函數有一個額外的參數level, 說明如何獲取錯誤發生的位置。level默認為1,返回error 函數被調用的位置; level 為2, 返回調用*error* 函數的函數被調用的位置; level 為0,不獲取位置信息。對比下下面三段代碼的的區別和執行結果就明白了。
~~~
function foo(str)
if type(str) ~= "string" then
error("string expected", 0) --level 0
end
print("foo success")
end
local status, err = pcall(foo(3))
print(err)
~~~
運行結果:

~~~
function foo(str)
if type(str) ~= "string" then
error("string expected", 1) --level 1
end
print("foo success")
end
local status, err = pcall(foo(3))
print(err)
~~~
運行結果:

~~~
function foo(str)
if type(str) ~= "string" then
error("string expected", 2) --level 2
end
print("foo success")
end
local status, err = pcall(foo(3))
print(err)
~~~
運行結果:

err_test03.lua, err_test04.lua是我code文件的名字而已,可能每個人起名字都不同的。注意一下level 1 和level 2的錯誤行號是不同的。通過這3個例子,很明白了吧。
通常情況下,在程序出錯的時候,可能僅僅知道錯誤發生的位置是不夠的。至少,還需要函數的調用堆棧吧。但是當*pcall* 函數返回的時候,堆棧信息已被部分破壞了。因此,為了獲取堆棧信息,必須在*pcall* 返回之前就建立它。Lua為我們提供了*xpcall* 函數, 它比*pcall* 函數多一個參數*error handler function*。 一旦發生錯誤,Lua在堆棧被損壞之前調用這個*handler* 函數,在*handler* 中,我們可以用*debug* 庫來收集盡可能的有關錯誤的信息。兩個常用的*handler* 函數是*debug.debug* 和*debug.traceback* ;具體用法,后續會詳細討論。
終于寫完這篇了,關電腦睡覺去。
水平有限,如果有朋友發現錯誤,歡迎留言交流