初始化数据结构或对象时,如果使用后有需要显式发布过程的子对象,我应该如何处理初始化过程中的错误?
让我举一个例子,初始化一个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#)异常和资源管理语句相对应的功能,如try
与catch
和/或finally
。
Common Lisp中的try
-catch
是用handler-case
实现的,正如您在代码中所做的那样。可以简单地将相同的错误重新提交,但您不会在实际发生错误的调试器上捕捉到错误。Java在创建异常时包含异常的堆栈跟踪。C#在抛出异常时包含异常的堆栈跟踪。在任何情况下,我认为两者都有方法抛出一个带有内部异常的新异常,这样您就可以到达原始堆栈。
Common Lisp中的try
到finally
是用unwind-protect
实现的。第一个表单正常执行,其余表单无条件执行,无论第一个表单是否正常返回。
Common Lisp有一个功能,它允许在发出错误信号时运行代码,即handler-bind
。handler-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
表单包装在宏中,并检查受保护子句中的初始化是否成功。