1-8、Lua編譯-運行-錯誤信息

1-8、Lua編譯-運行-錯誤信息



雖然我們把Lua當作解釋型語言,但是Lua會首先把代碼預編譯成中間碼然後再執行(很多解釋型語言都是這麼做的)。在解釋型語言中存在編譯階段聽起來不合適,然而,解釋型語言的特徵不在於他們是否被編譯,而是編譯器是語言運行時的一部分,所以,執行編譯產生的中間碼速度會更快。我們可以說函數dofile的存在就是說明可以將Lua作爲一種解釋型語言被調用。
前面我們介紹過dofile,把它當作Lua運行代碼的chunk的一種原始的操作。dofile實際上是一個輔助的函數。真正完成功能的函數是loadfile;與dofile不同的是loadfile編譯代碼成中間碼並且返回編譯後的chunk作爲一個函數,而不執行代碼;另外loadfile不會拋出錯誤信息而是返回錯誤碼。我們可以這樣定義dofile:

function dofile (filename)
	local f = assert(loadfile(filename))
	return f()
end

如果loadfile失敗assert會拋出錯誤。
完成簡單的功能dofile比較方便,他讀入文件編譯並且執行。然而loadfile更加靈活。在發生錯誤的情況下,loadfile返回nil和錯誤信息,這樣我們就可以自定義錯誤處理。另外,如果我們運行一個文件多次的話,loadfile只需要編譯一次,但可多次運行。dofile卻每次都要編譯。
loadstring與loadfile相似,只不過它不是從文件裏讀入chunk,而是從一個串中讀入。例如:

f = loadstring("i = i + 1")
f將是一個函數,調用時執行i=i+1。
i = 0
f(); print(i)		--> 1
f(); print(i)		--> 2

loadstring函數功能強大,但使用時需多加小心。確認沒有其它簡單的解決問題的方法再使用。
Lua把每一個chunk都作爲一個匿名函數處理。例如:chunk “a = 1”,loadstring返回與其等價的function () a = 1 end
與其他函數一樣,chunks可以定義局部變量也可以返回值:

f = loadstring("local a = 10; return a + 20")
print(f())			--> 30

loadfile和loadstring都不會拋出錯誤,如果發生錯誤他們將返回nil加上錯誤信息:

print(loadstring("i i"))
		--> nil	[string "i i"]:1: '=' expected near 'i'

另外,loadfile和loadstring都不會有邊界效應產生,他們僅僅編譯chunk成爲自己內部實現的一個匿名函數。通常對他們的誤解是他們定義了函數。Lua中的函數定義是發生在運行時的賦值而不是發生在編譯時。假如我們有一個文件foo.lua:

-- file `foo.lua'
function foo (x)
	print(x)
end

當我們執行命令f = loadfile(“foo.lua”)後,foo被編譯了但還沒有被定義,如果要定義他必須運行chunk:

f()				-- defines `foo'
foo("ok")		--> ok

如果你想快捷的調用dostring(比如加載並運行),可以這樣

loadstring(s)()

調用loadstring返回的結果,然而如果加載的內容存在語法錯誤的話,loadstring返回nil和錯誤信息(attempt to call a nil value);爲了返回更清楚的錯誤信息可以使用assert:

assert(loadstring(s))()

通常使用loadstring加載一個字串沒什麼意義,例如:

f = loadstring("i = i + 1")

大概與f = function () i = i + 1 end等價,但是第二段代碼速度更快因爲它只需要編譯一次,第一段代碼每次調用loadstring都會重新編譯,還有一個重要區別:loadstring編譯的時候不關心詞法範圍:

local i = 0
f = loadstring("i = i + 1")
g = function () i = i + 1 end

這個例子中,和想象的一樣g使用局部變量i,然而f使用全局變量i;loadstring總是在全局環境中編譯他的串。
loadstring通常用於運行程序外部的代碼,比如運行用戶自定義的代碼。注意:loadstring期望一個chunk,即語句。如果想要加載表達式,需要在表達式前加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

1、require函數

Lua提供高級的require函數來加載運行庫。粗略的說require和dofile完成同樣的功能但有兩點不同:

  • 1.require會搜索目錄加載文件
  • 2.require會判斷是否文件已經加載避免重複加載同一文件。由於上述特徵,require在Lua中是加載庫的更好的函數。
    require使用的路徑和普通我們看到的路徑還有些區別,我們一般見到的路徑都是一個目錄列表。require的路徑是一個模式列表,每一個模式指明一種由虛文件名(require的參數)轉成實文件名的方法。更明確地說,每一個模式是一個包含可選的問號的文件名。匹配的時候Lua會首先將問號用虛文件名替換,然後看是否有這樣的文件存在。如果不存在繼續用同樣的方法用第二個模式匹配。例如,路徑如下:
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua

調用require "lili"時會試着打開這些文件:

lili
lili.lua
c:\windows\lili
/usr/local/lua/lili/lili.lua

require關注的問題只有分號(模式之間的分隔符)和問號,其他的信息(目錄分隔符,文件擴展名)在路徑中定義。
爲了確定路徑,Lua首先檢查全局變量LUA_PATH是否爲一個字符串,如果是則認爲這個串就是路徑;否則require檢查環境變量LUA_PATH的值,如果兩個都失敗require使用固定的路徑(典型的"?;?.lua")require的另一個功能是避免重複加載同一個文件兩次。Lua保留一張所有已經加載的文件的列表(使用table保存)。如果一個加載的文件在表中存在require簡單的返回;表中保留加載的文件的虛名,而不是實文件名。所以如果你使用不同的虛文件名require同一個文件兩次,將會加載兩次該文件。比如require “foo"和require “foo.lua”,路徑爲”?;?.lua"將會加載foo.lua兩次。我們也可以通過全局變量_LOADED訪問文件名列表,這樣我們就可以判斷文件是否被加載過;同樣我們也可以使用一點小技巧讓require加載一個文件兩次。比如,require "foo"之後_LOADED[“foo”]將不爲nil,我們可以將其賦值爲nil,require "foo.lua"將會再次加載該文件。

一個路徑中的模式也可以不包含問號而只是一個固定的路徑,比如:

?;?.lua;/usr/local/default.lua

這種情況下,require沒有匹配的時候就會使用這個固定的文件(當然這個固定的路徑必須放在模式列表的最後纔有意義)。在require運行一個chunk以前,它定義了一個全局變量_REQUIREDNAME用來保存被required的虛文件的文件名。我們可以通過使用這個技巧擴展require的功能。舉個極端的例子,我們可以把路徑設爲"/usr/local/lua/newrequire.lua",這樣以後每次調用require都會運行newrequire.lua,這種情況下可以通過使用_REQUIREDNAME的值去實際加載required的文件。

2、C Packages

Lua和C是很容易結合的,使用C爲Lua寫包。與Lua中寫包不同,C包在使用以前必須首先加載並連接,在大多數系統中最容易的實現方式是通過動態連接庫機制,然而動態連接庫不是ANSI C的一部分,也就是說在標準C中實現動態連接是很困難的。

通常Lua不包含任何不能用標準C實現的機制,動態連接庫是一個特例。我們可以將動態連接庫機制視爲其他機制之母:一旦我們擁有了動態連接機制,我們就可以動態的加載Lua中不存在的機制。所以,在這種特殊情況下,Lua打破了他平臺兼容的原則而通過條件編譯的方式爲一些平臺實現了動態連接機制。標準的Lua爲windows、Linux、FreeBSD、Solaris和其他一些Unix平臺實現了這種機制,擴展其它平臺支持這種機制也是不難的。在Lua提示符下運行print(loadlib())看返回的結果,如果顯示bad arguments則說明你的發佈版支持動態連接機制,否則說明動態連接機制不支持或者沒有安裝。

Lua在一個叫loadlib的函數內提供了所有的動態連接的功能。這個函數有兩個參數:庫的絕對路徑和初始化函數。所以典型的調用的例子如下:

local path = "/usr/local/lua/lib/libluasocket.so"
local f = loadlib(path, "luaopen_socket")

loadlib函數加載指定的庫並且連接到Lua,然而它並不打開庫(也就是說沒有調用初始化函數),反之他返回初始化函數作爲Lua的一個函數,這樣我們就可以直接在Lua中調用他。如果加載動態庫或者查找初始化函數時出錯,loadlib將返回nil和錯誤信息。我們可以修改前面一段代碼,使其檢測錯誤然後調用初始化函數:

local path = "/usr/local/lua/lib/libluasocket.so"
-- or path = "C:\\windows\\luasocket.dll"
local f = assert(loadlib(path, "luaopen_socket"))
f()  -- actually open the library

一般情況下我們期望二進制的發佈庫包含一個與前面代碼段相似的stub文件,安裝二進制庫的時候可以隨便放在某個目錄,只需要修改stub文件對應二進制庫的實際路徑即可。將stub文件所在的目錄加入到LUA_PATH,這樣設定後就可以使用require函數加載C庫了。

3、錯誤

Errare humanum est(拉丁諺語:犯錯是人的本性)。所以我們要儘可能的防止錯誤的發生,Lua經常作爲擴展語言嵌入在別的應用中,所以不能當錯誤發生時簡單的崩潰或者退出。相反,當錯誤發生時Lua結束當前的chunk並返回到應用中。
當Lua遇到不期望的情況時就會拋出錯誤,比如:兩個非數字進行相加;調用一個非函數的變量;訪問表中不存在的值等(可以通過metatables修改這種行爲,後面介紹)。你也可以通過調用error函數顯式地拋出錯誤,error的參數是要拋出的錯誤信息。

print "enter a number:"
n = io.read("*number")
if not n then error("invalid input") end

Lua提供了專門的內置函數assert來完成上面類似的功能:

print "enter a number:"
n = assert(io.read("*number"), "invalid input")

assert首先檢查第一個參數,若沒問題,assert不做任何事情;否則,assert以第二個參數作爲錯誤信息拋出。第二個參數是可選的。注意,assert會首先處理兩個參數,然後才調用函數,所以下面代碼,無論n是否爲數字,字符串連接操作總會執行:

n = io.read()
assert(tonumber(n), "invalid input: " .. n .. " is not a number")

當函數遇到異常有兩個基本的動作:返回錯誤代碼或者拋出錯誤。選擇哪一種方式,沒有固定的規則,不過基本的原則是:對於程序邏輯上能夠避免的異常,以拋出錯誤的方式處理之,否則返回錯誤代碼。
例如sin函數,假定我們讓sin碰到錯誤時返回錯誤代碼,則使用sin的代碼可能變爲:

local res = math.sin(x)
if not res then		-- error
	...
當然,我們也可以在調用sin前檢查x是否爲數字:
if not tonumber(x) then		-- error: x is not a number
...

而事實上,我們既不是檢查參數也不是檢查返回結果,因爲參數錯誤可能意味着我們的程序某個地方存在問題,這種情況下,處理異常最簡單最實際的方式是拋出錯誤並且終止代碼的運行。
再來看一個例子。io.open函數用於打開文件,如果文件不存在,結果會如何?很多系統中,我們通過“試着去打開文件”來判斷文件是否存在。所以如果io.open不能打開文件(由於文件不存在或者沒有權限),函數返回nil和錯誤信息。依據這種方式,我們可以通過與用戶交互(比如:是否要打開另一個文件)合理地處理問題:

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

如果你想偷懶不想處理這些情況,又想代碼安全的運行,可以使用assert:

file = assert(io.open(name, "r"))

Lua中有一個習慣:如果io.open失敗,assert將拋出錯誤。

file = assert(io.open("no-file", "r"))
		--> stdin:1: no-file: No such file or directory

注意:io.open返回的第二個結果(錯誤信息)會作爲assert的第二個參數。

4、異常和錯誤處理

很多應用中,不需要在Lua進行錯誤處理,一般有應用來完成。通常應用要求Lua運行一段chunk,如果發生異常,應用根據Lua返回的錯誤代碼進行處理。在控制檯模式下的Lua解釋器如果遇到異常,打印出錯誤然後繼續顯示提示符等待下一個命令。
如果在Lua中需要處理錯誤,需要使用pcall函數封裝你的代碼。
假定你想運行一段Lua代碼,這段代碼運行過程中可以捕捉所有的異常和錯誤。

  • 第一步:將這段代碼封裝在一個函數內
function foo ()
	...
	if unexpected_condition then error() end
	...
	print(a[i])	-- potential error: `a' may not be a table
	...
end
  • 第二步:使用pcall調用這個函數
if pcall(foo) then
	-- no errors while running `foo'
	...
else
	-- `foo' raised an error: take appropriate actions
	...
end

當然也可以用匿名函數的方式調用pcall:

if pcall(function () ... end) then ...
else ...

pcall在保護模式(protected mode)下執行函數內容,同時捕獲所有的異常和錯誤。若一切正常,pcall返回true以及“被執行函數”的返回值;否則返回nil和錯誤信息。
錯誤信息不一定僅爲字符串(下面的例子是一個table),傳遞給error的任何信息都會被pcall返回:

local status, err = pcall(function () error({code=121}) end)
print(err.code)  -->  121

這種機制提供了強大的能力,足以應付Lua中的各種異常和錯誤情況。我們通過error拋出異常,然後通過pcall捕獲之。

5、錯誤信息和回跟蹤(Tracebacks)

雖然你可以使用任何類型的值作爲錯誤信息,通常情況下,我們使用字符串來描述遇到的錯誤。如果遇到內部錯誤(比如對一個非table的值使用索引下標訪問)Lua將自己產生錯誤信息,否則Lua使用傳遞給error函數的參數作爲錯誤信息。不管在什麼情況下,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)與行號。
函數error還可以有第二個參數,表示錯誤發生的層級。比如,你寫了一個函數用來檢查“error是否被正確調用”:

function foo (str)
	if type(str) ~= "string" then
		error("string expected")
	end
	...
end

可有人這樣調用此函數:

foo({x=1})

Lua會指出發生錯誤的是foo而不是error,實際上,錯誤是調用error時產生的。爲了糾正這個問題,修改前面的代碼讓error報告錯誤發生在第二級(你自己的函數是第一級)如下:

function foo (str)
	if type(str) ~= "string" then
		error("string expected", 2)
	end
	...
end

當錯誤發生的時候,我們常常希望瞭解詳細的信息,而不僅是錯誤發生的位置。若能瞭解到“錯誤發生時的棧信息”就好了,但pcall返回錯誤信息時,已經釋放了保存錯誤發生情況的棧信息。因此,若想得到tracebacks,我們必須在pcall返回以前獲取。Lua提供了xpcall來實現這個功能,xpcall接受兩個參數:調用函數、錯誤處理函數。當錯誤發生時,Lua會在棧釋放以前調用錯誤處理函數,因此可以使用debug庫收集錯誤相關信息。有兩個常用的debug處理函數:debug.debug和debug.traceback,前者給出Lua的提示符,你可以自己動手察看錯誤發生時的情況;後者通過traceback創建更多的錯誤信息,也是控制檯解釋器用來構建錯誤信息的函數。你可以在任何時候調用debug.traceback獲取當前運行的traceback信息:

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