我正在尝试编写一个宏,它将捕获Clojure中的编译时错误。具体而言,我想捕获并调用尚未针对该数据类型实现的协议方法时clojure.lang.Compiler$CompilerException
引发的异常。
到目前为止,我有:
(defmacro catch-compiler-error [body] (try (eval body) (catch Exception e e)))
但是,当然,有人告诉我这eval
是邪恶的,您通常不需要使用它。有没有一种无需使用即可实现的方法eval
?
我倾向于认为这eval
是适当的,因为我特别希望代码在运行时而不是在编译时进行评估。
宏在编译时扩展。他们不需要eval
编码。相反,他们汇编代码,稍后将在运行时对其进行评估。换句话说,如果你想确保传递给宏的代码在运行时,而不是在编译时计算,这告诉你,你绝对应该不是 eval
它的宏定义。
考虑到这个名称,这个名称catch-compiler-error
有点用词不当;如果调用您的宏的代码出现编译器错误(也许缺少括号),则您的宏实际上并没有采取任何措施来捕获它。您可以编写这样的catch-runtime-error
宏:
(defmacro catch-runtime-error
[& body]
`(try
~@body
(catch Exception e#
e#)))
此宏的工作方式如下:
body
。try
catch
java.lang.Exception
(的限定版本Exception
)e#
可以一次吞掉所有内容。让我们看一下它对一些实际代码的作用:
(macroexpand
'(catch-runtime-error
(/ 4 2)
(/ 1 0)))
如您所见,我并不是简单地将宏作为第一个元素来评估表单。这将扩展宏并评估结果。我只想执行扩展步骤,所以我正在使用macroexpand
,这给了我这个:
(try
(/ 4 2)
(/ 1 0)
(catch java.lang.Exception e__19785__auto__
e__19785__auto__))
这确实是我们所期望的:包含符号列表try
,我们的身体表达,并用符号另一个列表catch
和java.lang.Exception
后跟的独特象征的两个副本。
您可以通过直接评估该宏来检查该宏是否可以实现您想要的功能:
(catch-runtime-error (/ 4 2) (/ 1 0))
;=> #error {
; :cause "Divide by zero"
; :via
; [{:type java.lang.ArithmeticException
; :message "Divide by zero"
; :at [clojure.lang.Numbers divide "Numbers.java" 158]}]
; :trace
; [[clojure.lang.Numbers divide "Numbers.java" 158]
; [clojure.lang.Numbers divide "Numbers.java" 3808]
; ,,,]}
优秀。让我们尝试一些协议:
(defprotocol Foo
(foo [this]))
(defprotocol Bar
(bar [this]))
(defrecord Baz []
Foo
(foo [_] :qux))
(catch-runtime-error (foo (->Baz)))
;=> :qux
(catch-runtime-error (bar (->Baz)))
;=> #error {,,,}
但是,如上所述,使用这样的宏根本无法捕获编译器错误。您可以编写一个宏,返回一个代码块,该代码块将调用eval
传入的其余代码,从而将编译时间推回到运行时:
(defmacro catch-error
[& body]
`(try
(eval '(do ~@body))
(catch Exception e#
e#)))
让我们测试宏扩展以确保其正常工作:
(macroexpand
'(catch-error
(foo (->Baz))
(foo (->Baz) nil)))
扩展为:
(try
(clojure.core/eval
'(do
(foo (->Baz))
(foo (->Baz) nil)))
(catch java.lang.Exception e__20408__auto__
e__20408__auto__))
现在我们可以捕获更多错误,例如IllegalArgumentException
s ,这些错误是由于尝试传递错误数量的参数而引起的:
(catch-error (bar (->Baz)))
;=> #error {,,,}
(catch-error (foo (->Baz) nil))
;=> #error {,,,}
但是(我想说得很清楚),不要这样做。如果您发现自己将编译时间拖回运行时只是为了捕捉这些错误,那几乎肯定是在做错什么。您最好重组项目,这样就不必这样做了。
我猜您已经看到了这个问题,它eval
很好地解释了一些陷阱。特别是在Clojure中,除非您完全理解它引起的关于范围和上下文的问题以及该问题中讨论的其他问题,否则您绝对不应使用它。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句