资源获取即初始化 |
|
一个非常典型的适合 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 ]]
使用协程的示例
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] 的构造在 Lua 中。这个想法是为变量类(如 local
)命名为 scoped
,当提供一个函数(或可调用表)时,它会在范围退出时调用它
function test() local fh = io:open() scoped function() fh:close() end foo() end
可以在纯 Lua 中实现这一点。这在 Lua Programming Gems 中的 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
请注意,在 D 中实现的 scope
结构在语法上类似于在作用域结束时执行的 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
块以实现 RAII。
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 gem #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