第八課 編譯、執行與錯誤

儘管Lua稱爲是一種解釋型的語言,但Lua確實允許在運行源代碼前,先將源代碼預編譯爲一種中間形式。其實,區別解釋型語言的主要特徵並不在於是否能編譯他們,而是在於編譯器是否是語言運行時庫的一部分,即是否有能力執行動態生成的代碼。可以說正是因爲存在了諸如dofile這樣的函數,纔可以將Lua稱爲一種解釋型語言。

編譯
dofile函數,是一種內置的操作,用於運行Lua代碼塊。但實際上dofile是 一個輔助函數,loadfile才做了真正核心的工作。類似於dofile,loadfile會從一個文件加載Lua代碼塊,但它不會運行代碼塊,只是編譯代碼,然後將編譯結果作爲一個函數返回。此外,與dofile不同的還有loadfile不會引發錯誤,它只是返回錯誤值並不會處理它。一般 dofile可以這樣來定義:
function dofile (filename)
local f = assert(loadfile(filename))
return f()
end
如果需要多次運行一個文件,那麼只需要在調用一次loadfile之後,多次調用返回結果就可以了。相對於多次調用dofile來說,由於只編譯一次文件,開銷就小的多了。
函數loadstring與loadfile類似,不同之處在於它是從一個字符串中讀取代碼。
f = loadstring("i = i + 1")

i = 0
f(); print(i) -->1
f(); print(i) -->2

一般將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
loadstring總是在全局環境中編譯它的字符串。
loadstring最典型的 用處就是執行外部代碼。例如,若讓用戶來參與一個函數的定義,那麼這時就需要讓用戶輸入函數代碼,然後調用loadstring來對其求值。如果要對一個表達式求值,則必須在其之前添加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
print(string.rep("*", f())) --打印多少個*星號
end

Lua將所有獨立的程序塊視爲一個匿名函數的函數體,並且該匿名函數還具有可變長實參。例如,loadstring("a = 1")返回的結果等價於以下表達式:
function (...) a = 1 end
與所有其他函數一樣,程序塊中可以聲明局部變量:
f = loadstring("local a = 10; print(a + 20)")
f() -->30
通過這個特性,就可以重寫那個用戶參與的實例,在其中避免使用全局變量x:
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
這些函數不會帶來任何副作用。它們只是將程序塊編譯爲一種中間表示,然後將結果作爲一個匿名函數來返回。常見的誤解是認爲加載了一個程序塊,也就是定義了其中的函數。其實在Lua中,函數定義是一種賦值操作。也就是說,它們是在運行時才完成的操作。
foo.lua文件:
function foo (x)
print(x)
end

f = loadfile("foo.lua")
print(foo) -->nil
f() --定義foo
foo("ok") -->ok

C代碼
Lua所提供的所有關於動態鏈接的功能都聚集在一個函數中,即package.loadlib。該函數有兩個字符串參數:動態庫的完整路徑和一個函數名稱。如:
local path = "/usr/local/lib/lua/5.1/socket.so"
local f = package.loadlib(path, "luaopen_socket")
loadlib加載指定的庫,並將其鏈接入Lua。不過,它並沒有調用庫中的任何函數。相反,它將一個C函數作爲Lua函數返回。如果在加載庫或者查找 初始化函數時發生任何錯誤,loadlib返回nil及一條錯誤消息。

錯誤
由於Lua是一種擴展語言,通常嵌入在應用程序中,因此在發生錯誤時它不能簡單地崩潰或退出。相反,只要發生了一個錯誤,Lua就應該結束當前的程序塊並返回應用程序。
也可以顯示地引發一個錯誤,通過調用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函數檢查其第一個參數是否爲true。若爲true,則簡單地返回該參數;否則(爲false或者nil)就引發一個錯誤。Lua會在調用該函數前對其參數求值。
當一個函數遭遇了一種未預期的狀況(即‘異常’),它可以採取兩種基本的行爲:返回錯誤代碼(通常是nil)或引發一個錯誤(調用error)。

錯誤處理與異常
對於大多數應用程序而言,無須在Lua代碼中作任何錯誤處理,應用程序本身會負責這類問題。
如果需要在Lua中處理錯誤,則必須使用函數pcall來包裝需要執行的代碼。
假設在執行一段Lua代碼時,捕獲所有執行中引發的錯誤,那麼第一步就是將這段代碼封裝到一個函數中,將其稱爲foo:
function foo ()
<一些代碼>
if 未預期的條件 then error() end
<一些代碼>
print(a[i]) --潛在的錯誤:a可能不是一個table
<一些代碼>
end
然後使用pcall來調用foo:
if pcall(foo) then
--在執行foo時沒有發生錯誤
<常規代碼>
else
--foo引發了一個錯誤,採取適當的行爲
<錯誤處理代碼>
end
當然,在調用pcall時可以傳入一個匿名函數:
if pcall (function ()
<受保護的代碼>
end) then
<常規代碼>
else
<異常代碼>
end
pcall函數會以一種“保護模式”來調用它的第一個參數,因此pcall可以捕獲函數執行中的任何錯誤。如果沒有發生錯誤,pcall會返回true以及函數調用的返回值;否則,返回false及錯誤消息。
任何類型的Lua值都可以作爲錯誤消息傳遞給error函數,並且這些值也會成爲pcall的返回值:
local status, err = pcall(function () error({code = 121}) end)
print(err.code) -->121

錯誤消息與追溯
雖然可以使用任何類型的值作爲錯誤消息,但是錯誤消息通常是一個描述了出錯內容的字符串。只要錯誤消息是一個字符串,Lua就會附加一些關於錯誤發生位置的信息。
local status, err = pcall(function () a = "a" + 1 end)
print(err)
-->stdin:1:attempt to perform arithmetic on a string value

local status, err = pcall(function () error("my error") end)
print(err)
-->stdin:1: my error
位置信息中包含了文件名(上例中的stdin)以及行號(上例中的1)
error函數還有第二個附加參數level(層),用於指出應由調用層級中的哪個層函數來報告當前的錯誤,也就是說明了誰應該爲錯誤負責。
如:
function foo (str)
if type(str) ~= "string" then
error("string expected")
end
<常規代碼>
end
然後,其他人在調用函數時傳入了錯誤的參數:
foo ({x = 1})
由於foo是調用了error,所以Lua會認爲是函數發生了錯誤。但實際上卻是foo的調用者造成的錯誤。爲了糾正這個問題,就要告知error函數錯誤是發生在調用層級的第二層中(第一層是讀函數):
function foo (str)
if type(str) ~= "string" then
error("string expected", 2)
end
<常規代碼>
end
通常在錯誤發生時,希望得到更多的調試信息,而不是隻有發生錯誤的位置。至少,能夠追溯到發生錯誤時的函數調用情況,顯示一個完整的函數調用棧。當pcall返回其錯誤消息時,它已經銷燬了調用棧的部分內容。因此,如果希望的到一個有意義的調用棧,那麼就必須在pcall返回前獲取該信息。爲了達成這一要求,Lua提供了xpcall函數。該函數除了接受一個需要被調用的函數外,還接受第二個參數----一個錯誤處理函數。當發生錯誤時,Lua會在調用棧展開前調用錯誤處理函數。於是就可以在這個函數中使用debug庫來獲取關於錯誤的額外的信息了。debug庫提供了兩個通用的錯誤處理函數,一個是debug.debug,它會提供一個Lua提示符,讓用戶來檢查錯誤原因;另一個是debug.traceback,它會根據調用棧來構建一個擴展的錯誤消息。另外,也可以在任何時候調用這個函數來獲取當前執行 的調用棧。
print(debug.traceback())





























發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章