用显式资源管理初始化数据结构时的适当错误处理



初始化数据结构或对象时,如果使用后有需要显式发布过程的子对象,我应该如何处理初始化过程中的错误?

让我举一个例子,初始化一个OBJECT对象,其中SUBOBJ1和SUBOBJ2插槽将被设置为指向int值的外部指针:

(defun init-object ()
(let ((obj (make-object)))
(setf (subobj1 obj) (cffi:foreign-alloc :int)
(subobj2 obj) (cffi:foreign-alloc :int))
obj))

如果我们在SUBOBJ2插槽的FOREIGN ALLOCing中出现错误,我们应该为SUBOBJ1插槽执行FOREIGN FREEing,以避免内存泄漏。

作为一个想法,我可以写如下:

(defun init-object ()
(let ((obj (make-object)))
(handler-case
(setf (subobj1 obj) (cffi:foreign-alloc :int)
(subobj2 obj) (cffi:foreign-alloc :int))
(condition (c)   ; forcedly handling all conditions
(when (subobj1 obj) (cffi:foreign-free (subobj1 obj)))
(error c)))    ; invoke the condition c again explicitly
obj))

你有什么更好的想法,或者说是惯用的模式吗?

感谢


在回答之后,我使用UNWIND-PROTECT添加了一个代码。它不会起作用,因为即使所有分配都成功完成,释放表单也会运行。

(defun init-object ()
(let ((obj (make-object)))
(unwind-protect
(progn
(setf (subobj1 obj) (cffi:foreign-alloc :int)
(subobj2 obj) (cffi:foreign-alloc :int))
obj)
; foreign pointers freed even when successfully initialized
(when (subobj2 obj) (cffi:foreign-free (subobj2 obj)))
(when (subobj1 obj) (cffi:foreign-free (subobj1 obj))))))

使用UNWIND-PROTECT。当错误导致退出范围时,unwind-protect允许您强制执行清理表单。

类似这样的东西:

(defun init-object ()
(let ((obj (make-object)))
(unwind-protect
(setf (subobj1 obj) (cffi:foreign-alloc :int)
(subobj2 obj) (cffi:foreign-alloc :int))
(unless (and (subobj2 obj) (subobj1 obj))
(when (subobj1 obj) (cffi:foreign-free (subobj1 obj)))
(when (subobj2 obj) (cffi:foreign-free (subobj2 obj)))))
obj))

使用任何可用的工具来检测插槽是否已绑定。上面假设未初始化的时隙具有值NIL

Common Lisp具有与当今语言(如Java、C#)异常和资源管理语句相对应的功能,如trycatch和/或finally

Common Lisp中的try-catch是用handler-case实现的,正如您在代码中所做的那样。可以简单地将相同的错误重新提交,但您不会在实际发生错误的调试器上捕捉到错误。Java在创建异常时包含异常的堆栈跟踪。C#在抛出异常时包含异常的堆栈跟踪。在任何情况下,我认为两者都有方法抛出一个带有内部异常的新异常,这样您就可以到达原始堆栈。

Common Lisp中的tryfinally是用unwind-protect实现的。第一个表单正常执行,其余表单无条件执行,无论第一个表单是否正常返回。

Common Lisp有一个功能,它允许在发出错误信号时运行代码,即handler-bindhandler-case的主要区别在于,如果没有处理程序非本地退出,它不会倒带堆栈,也不会阻止错误弹出到其他处理程序或调试器。

因此,你可以使用这样的东西:

(defun init-object ()
(let ((obj (make-object)))
(handler-bind
(;; forcedly handling all conditions
(condition #'(lambda (c)
(declare (ignore c))
(when (subobj1 obj) (cffi:foreign-free (subobj1 obj)))
;; return normally, allowing the condition to go up the handler chain
;; and possibly to the debugger, if none exits non-locally
)))
(setf (subobj1 obj) (cffi:foreign-alloc :int)
(subobj2 obj) (cffi:foreign-alloc :int)))
obj))

我建议您不要使用condition进行匹配,因为所有条件都继承自它,例如storage-condition。在无法或不可能恢复的情况下,你可能不想做任何事情。


仅供参考,Common Lisp中完整的try-catch-finally子句是用unwind-protect围绕handler-case:实现的

(unwind-protect
(handler-case
(do-something)
(error-type-1 ()
(foo))
(error-type-2 (e)
(bar e)))
(cleanup-form-1)
(cleanup-form-2))

有人建议使用UNWIND-PROTECT。这是处理资源分配的惯用方法。然而,如果您的目标是在出现错误时释放资源,但如果一切成功,则返回这些资源,那么您可以使用以下方法:

(defun init-object ()
(let ((obj (create-object)))
(handler-case
(progn
(setf (subobj1 obj) (cffi:foreign-alloc :int))
(setf (subobj2 obj) (cffi:foreign-alloc :int))
obj)
(error (condition)
(free-object obj)
;; Re-throw the error up in the call chain
(error condition)))))
(defun free-object (obj)
(when (subobj2 obj) (cffi:foreign-free (subobj2 obj)))
(when (subobj1 obj) (cffi:foreign-free (subobj1 obj))))

实现相同功能的另一种方法是进行检查,验证是否已到达函数的末尾,如果没有,则释放对象。然而,我真的不喜欢这种风格,因为它并不能很好地显示正在发生的事情

但是,请注意,当使用函数INIT-OBJECT时,需要将其包含在UNWIND-PROTECT中。否则,一旦函数返回的对象为GC’ed,就会泄漏资源。

这样做的方法是在使用该功能时始终执行以下操作:

(let ((obj (init-object)))
(unwind-protect
... use object here ...
(free-object obj)))

另一种解决方案是在对象被GC’ed时释放它。没有标准的方法,但必要的功能是在TRIVIAL-GARBAGE:FINALIZE函数中抽象出来的。

我赞同Rainer的建议:我将unwind-protect表单包装在宏中,并检查受保护子句中的初始化是否成功。

最新更新