資源取得實作初始化 |
|
非常適合於 RAII 的典型問題如下
function dostuff() local f = assert(io.open("out", "w")) domorestuff() -- this may raise an error f:close() -- this is not called if that error was raised end dostuff()
如果會引發錯誤,檔案就不會立刻關閉 (RAII 本來會這麼做)。是的,垃圾收集器最後會關閉檔案,但我們不知道什麼時候。程式的成功或正確性可能取決於是否立即釋放檔案鎖定。不過,這時在 pcall
外呼叫 collectgarbage('collect')
可能會有幫助,在這種情況下,Lua 會呼叫 __gc
(終結器) 元方法來關閉檔案,不過你可能必須呼叫 collectgarbage
超過一次 [*1]。此外,Lua 不允許以純 Lua 實作的物件 (沒有 C 使用者資料的協助) 定義它們自己的 __gc
元方法。
以下是在純 Lua 中的一種做法,它會維持需要回收的所有物件的堆疊。在範圍結束或處理例外狀況時,會從堆疊中移除要回收的物件並使其完成 (也就是呼叫 close
,如果存在;否則將其作為函式呼叫),以釋放它們的資源。
-- raii.lua local M = {} local frame_marker = {} -- unique value delimiting stack frames local running = coroutine.running -- Close current stack frame for RAII, releasing all objects. local function close_frame(stack, e) assert(#stack ~= 0, 'RAII stack empty') for i=#stack,1,-1 do -- release in reverse order of acquire local v; v, stack[i] = stack[i], nil if v == frame_marker then break else -- note: assume finalizer never raises error if type(v) == "table" and v.close then v:close() else v(e) end end end end local function helper1(stack, ...) close_frame(stack); return ... end -- Allow self to be used as a function modifier -- to add RAII support to function. function M.__call(self, f) return function(...) local stack, co = self, running() if co then -- each coroutine gets its own stack stack = self[co] if not stack then stack = {} self[co] = stack end end stack[#stack+1] = frame_marker -- new frame return helper1(stack, f(...)) end end -- Show variables in all stack frames. function M.__tostring(self) local stack, co = self, running() if co then stack = stack[co] end local ss = {} local level = 0 for i,val in ipairs(stack) do if val == frame_marker then level = level + 1 else ss[#ss+1] = string.format('[%s][%d] %s', tostring(co), level, tostring(val)) end end return table.concat(ss, '\n') end local function helper2(stack, level, ok, ...) local e; if not ok then e = select(1, ...) end while #stack > level do close_frame(stack, e) end return ... end -- Construct new RAII stack set. function M.new() local self = setmetatable({}, M) -- Register new resource(s), preserving order of registration. function self.scoped(...) local stack, co = self, running() if co then stack = stack[co] end for n=1,select('#', ...) do stack[#stack+1] = select(n, ...) end return ... end -- a variant of pcall -- that ensures the RAII stack is unwound. function self.pcall(f, ...) local stack, co = self, running() if co then stack = stack[co] end local level = #stack return helper2(stack, level, pcall(f, ...)) end -- Note: it's somewhat convenient having scoped and pcall be -- closures.... local scoped = raii.scoped return self end -- singleton. local raii = M.new() return raii
範例用法
local raii = require "raii" local scoped, pcall = raii.scoped, raii.pcall -- Define some resource type for testing. -- In practice, this is a resource we acquire and -- release (e.g. a file, database handle, Win32 handle, etc.). local Resource = {}; do Resource.__index = Resource function Resource:__tostring() return self.name end function Resource.open(name) local self = setmetatable({name=name}, Resource) print("open", name) return self end function Resource:close() print("close", self.name) end function Resource:foo() print("hello", self.name) end end local test3 = raii(function() local f = scoped(Resource.open('D')) f:foo() print(raii) error("opps") end) local test2 = raii(function() scoped(function(e) print("leaving", e) end) local f = scoped(Resource.open('C')) test3(st) end) local test1 = raii(function() local g1 = scoped(Resource.open('A')) local g2 = scoped(Resource.open('B')) print(pcall(test2)) end) test1() --[[ OUTPUT: open A open B open C open D hello D [nil][1] A [nil][1] B [nil][2] function: 0x68a818 [nil][2] C [nil][3] D close D close C leaving complex2.lua:23: opps complex2.lua:23: opps close B close A ]]
使用 coroutine 的範例
local raii = require "raii" local scoped, pcall = raii.scoped, raii.pcall -- Define some resource type for testing. -- In practice, this is a resource we acquire and -- release (e.g. a file, database handle, Win32 handle, etc.). local Resource = {}; do Resource.__index = Resource local running = coroutine.running function Resource:__tostring() return self.name end function Resource.open(name) local self = setmetatable({name=name}, Resource) print(running(), "open", self.name) return self end function Resource:close() print(running(), "close", self.name) end function Resource:foo() print(running(), "hello", self.name) end end local test3 = raii(function(n) local f = scoped(Resource.open('D' .. n)) f:foo() print(raii) error("opps") end) local test2 = raii(function(n) scoped(function(e) print(coroutine.running(), "leaving", e) end) local f = scoped(Resource.open('C' .. n)) test3(n) end) local test1 = raii(function(n) local g1 = scoped(Resource.open('A' .. n)) coroutine.yield() local g2 = scoped(Resource.open('B' .. n)) coroutine.yield() print(coroutine.running(), pcall(test2, n)) coroutine.yield() end) local cos = {coroutine.create(test1), coroutine.create(test1)} while true do local is_done = true for n=1,#cos do if coroutine.status(cos[n]) ~= "dead" then coroutine.resume(cos[n], n) is_done = false end end if is_done then break end end -- Note: all coroutines must terminate for RAII to work. --[[ OUTPUT: thread: 0x68a7f0 open A1 thread: 0x68ac10 open A2 thread: 0x68a7f0 open B1 thread: 0x68ac10 open B2 thread: 0x68a7f0 open C1 thread: 0x68a7f0 open D1 thread: 0x68a7f0 hello D1 [thread: 0x68a7f0][1] A1 [thread: 0x68a7f0][1] B1 [thread: 0x68a7f0][2] function: 0x68ada0 [thread: 0x68a7f0][2] C1 [thread: 0x68a7f0][3] D1 thread: 0x68a7f0 close D1 thread: 0x68a7f0 close C1 thread: 0x68a7f0 leaving complex3.lua:24: opps thread: 0x68a7f0 complex3.lua:24: opps thread: 0x68ac10 open C2 thread: 0x68ac10 open D2 thread: 0x68ac10 hello D2 [thread: 0x68ac10][1] A2 [thread: 0x68ac10][1] B2 [thread: 0x68ac10][2] function: 0x684258 [thread: 0x68ac10][2] C2 [thread: 0x68ac10][3] D2 thread: 0x68ac10 close D2 thread: 0x68ac10 close C2 thread: 0x68ac10 leaving complex3.lua:24: opps thread: 0x68ac10 complex3.lua:24: opps thread: 0x68a7f0 close B1 thread: 0x68a7f0 close A1 thread: 0x68ac10 close B2 thread: 0x68ac10 close A2 ]]
JohnBelmonte 在 LuaList:2007-05/msg00354.html [*2] 中建議實作類似於 D 語言 scope
保護陳述 [3][4] 結構的某種東西。想法是變數類別 (例如 local
) 名為 scoped
,它在提供函式 (或可呼叫表格) 時,它會在範圍結束時呼叫它
function test() local fh = io:open() scoped function() fh:close() end foo() end
這可以在純 Lua 中實作。這在 Lua 編程珍寶中 Gem #13 「Lua 中的例外情況」 [5] 中有說明,以允許類似以下內容
function dostuff() scope(function() local fh1 = assert(io.open('file1')) on_exit(function() fh1:close() end) ... local fh2 = assert(io.open('file2')) on_exit(function() fh2:close() end) ... end) end
這需要建立一個匿名函式,不過這麼做的話從效率角度來看會有好處。
以下是另一種非常基本的構想 («finally ... end» 結構)
function load(filename) local h = io.open (filename) finally if h then h:close() end end ... end
請注意 scope
建構函式在 D 中的實作在句法上類似於 if
陳述式,在作用域結束時執行。也就是說,假設我們將 exit
、success
和 failure
視為真實的條件式;事實上,不妨做這樣的概化。我為 Lua 提出下列語法擴充
stat :: scopeif exp then block {elseif exp then block} [else block] end
其中 err
是隱式變數(例如 self
),可在 exp 或 block 中使用,代表引發的錯誤,如果未引發錯誤,則為 nil
。(註解:在幾個月後重訪該語法時,我發現語意不太直覺,特別是 err
的特殊用法。)
「例外安全程式設計」中的範例 [3] 可轉譯為 Lua 語法如下
function abc() local f = dofoo(); scopeif err then dofoo_undo(f) end local b = dobar(); scopeif err then dobar_undo(b) end local d = dodef(); return Transaction(f, b, d) end ----- function bar() local verbose_save = verbose verbose = false scopeif true then verbose = verbose_save end ...lots of code... end ----- function send(msg) do local origTitle = msg.Title() scopeif true then msg.SetTitle(origTitle) end msg.SetTitle("[Sending] " .. origTitle) Copy(msg, "Sent") end scopeif err then Remove(msg.ID(), "Sent") else SetTitle(msg.ID(), "Sent", msg.Title) end SmtpSend(msg) -- do the least reliable part last end
scopeif true then ... end
有點冗長,但與 while true do ... end
類似。使用 scopeif
而不是 scope if
遵循 elseif
的模式。
JohnBelmonte的資料庫範例縮短為
function Database:commit() for attempt = 1, MAX_TRIES do scopeif instance_of(err, DatabaseConflictError) then if attempt < MAX_TRIES then log('Database conflict (attempt '..attempt..')') else error('Commit failed after '..attempt..' tries.') end end -- note: else no-op self.commit() return end end
以下是如何模擬一般的 RAII(D 文章並未說明 RAII 永遠沒有用)
function test() local resource = Resource(); scope if true then resource:close() end foo() end
但是,這個比建議的更冗長
function test() scoped resource = Resource() foo() end
也許可以在 Metalua [6] 中建立此原型。
已發布幾個 Lua 修補程式來處理這種類型的問題
(2008-01-31) 修補程式:由 Hu Qiwei 發布,提供try/catch/finally 支援 [10][11][12]。try
區塊禁止使用 return
和 break
。
finalize
和 guard
區塊。
MetaLua 0.4 提供名為「withdo」的 RAII 擴充。適用於透過呼叫方法 :close() 釋放的所有資源。防護受保護區塊的正常終止、區塊內部的傳回,以及錯誤。以下會在關閉檔案 filename1 和 filename2 的處理常式之後,回傳這兩個檔案大小的總和
with h1, h2 = io.open 'filename1', io.open 'filename2' do local total = #h1:read'*a' + #h2:read'*a' return total end
請注意,Metalua 的設計受到資源物件具備特定方法(在這裡是「close()」)的要求所限制。在 Python 中它被拒絕而採用「with ... as ...」語法,允許資源管理物件與資源本身分開 [7]。此外,Python 陳述允許省略指派,因為在許多情況下不需要資源變數--例如如果你只是想在區塊中鎖定一個物件。
[*1]視用戶資料如何相互交雜,它可能為兩次以上,但如果用戶資料是某個其他物件的最後參照,且該物件本身是/參照一個用戶資料,則確定需要兩次收集才能移除具有 __gc
元資料的用戶資料,然後循環會繼續。(由 RiciLake 指出)
[*2] RAII 模式會遇到需要建立臨時類別來管理資源,以及取得順序資源時所需的笨拙巢狀結構的問題。請參閱 http://www.digitalmars.com/d/exception-safe.html。更好的模式在 Lua 範例 #13「Lua 中的例外狀況」中 [5]。 --JohnBelmonte
以下是我提議的範例程式碼,說明 Google Go defer
和 D scope(exit)
的語法。--DavidManura
-- Lua 5.1, example without exceptions local function readfile(filename) local fh, err = io.open(filename) if not fh then return false, err end local data, err = fh:read'*a' -- note: in this case, the two fh:close()'s may be moved here, but in general that is not possible if not data then fh:close(); return false, err end fh:close() return data end -- Lua 5.1, example with exceptions, under suitable definitions of given functions. local function readfile(filename) return scoped(function(onexit) -- based on pcall local fh = assert(io.open(filename)); onexit(function() fh:close() end) return assert(fh:read'*a') end) end -- proposal, example without exceptions local function readfile(filename) local fh, err = io.open(filename); if not fh then return false, err end defer fh:close() local data, err = fh:read'*a'; if not data then return false, err end return data end -- note: "local val, err = io.open(filename); if not val then return false, err end" is a common -- pattern and perhaps warrants a syntax like "local val = returnunless io.open(filename)". -- proposal, example with exceptions local function readfile(filename) local fh = assert(io.open(filename)); defer fh:close() return assert(fh:read'*a') end -- proposal, example catching exceptions do defer if class(err) == 'FileError' then print(err) err:suppress() end print(readfile("test.txt")) end -- alternate proposal - cleanup code by metamechanism local function readfile(filename) scoped fh = assert(io.open(filename)) -- note: fh:close() or getmetatable(fh).__close(fh) called on scope exit return assert(fh:read'*a') end