我正在尝试编写一个宏来捕获 Clojure 中的编译时错误。具体来说,我想捕获当调用尚未为该数据类型实现的协议(protocol)方法并抛出 clojure.lang.Compiler$CompilerException
时抛出的异常。
到目前为止我有:
(defmacro catch-compiler-error
[ body ]
(尝试
(评估机构)
(捕获异常 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
的序列中。 - 创建一个包含这些元素的列表:
- 符号
尝试
- 所有作为参数传入的表达式
- 包含这些元素的另一个列表:
- 符号
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]
; ,,,]}
非常好。让我们尝试一些协议(protocol):
(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
由于试图传递不正确数量的参数而导致的:
(catch-error (bar (->Baz)))
;=> #error {,,,}
(catch-error (foo (->Baz) nil))
;=> #error {,,,}
但是(我想说得很清楚)不要这样做。如果您发现自己将编译时间推回到运行时只是为了 try catch 这些类型的错误,那么您你几乎肯定做错了什么。重构您的项目会更好,这样您就不必这样做了。
我猜你已经看过了 this question ,这很好地解释了 eval
的一些陷阱。特别是在 Clojure 中,除非您完全理解它提出的有关范围和上下文的问题,以及该问题中讨论的其他问题,否则您绝对不应该使用它。
关于clojure - 我可以不使用 eval 来编写这个宏吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35977973/