我觉得这是列出一些错误检查技术的好机会。我将讨论我知道的那些,请随时编辑这篇文章并添加更多内容。我认为这里要回答的主要问题是我们希望函数在出错的情况下返回什么,以及如何在技术上做到这一点。
出错时返回什么
我可以在这里看到 3 种不同的选择
该函数发出错误消息并返回未评估的结果。
这个更适合符号环境,并且对应于大多数 Mathematica 函数所具有的语义 w.r.t.错误。这个想法是符号环境比更传统的环境更宽容,并且通过返回相同的表达式,我们表明 Mathematica 根本不知道如何处理它。这为稍后执行函数调用留下了机会,例如当一些符号参数获取数字或其他值时。
从技术上讲,这可以通过利用条件模式的语义来实现。这是一个简单的例子:
ClearAll[f];
f::badargs = "A single argument of type integer was expected";
f[x_Integer] := x^2;
f[args___] := "nothing" /; Message[f::badargs]
f[2]
4
f[1, 2]
f::badargs: A single argument of type integer was expected
f[1, 2]
这个想法是,最后,模式被认为不匹配(因为条件中的测试不会评估为显式 True),但Message 在此过程中被调用。这个技巧也可以用于多个定义——因为最后的模式被认为是不匹配的,模式匹配器继续测试DownValues 列表中的其他规则。这可能是可取的,也可能不是,取决于具体情况。
函数的性质使得返回$Failed(显式失败)更合适。
这可能适用的典型示例是无法写入文件或无法在磁盘上找到文件。一般来说,我认为这种行为最适合用于软件工程的函数(换句话说,那些不将其结果直接传递到另一个函数但调用其他应该返回的函数并形成执行堆栈的函数)。如果确定给定函数失败后继续执行没有意义,则应返回$Failed(可能还会发出错误消息)。
返回$Failed 也可以作为一种预防措施,以防止由于实现更改而导致的偶尔出现的回归错误,例如,当某些函数已被重构为接受或返回不同数量和/或类型的参数时,但要调用的函数它没有及时更新。在像 Java 这样的强类型语言中,编译器会捕获这类错误。在 Mathematica 中,这是程序员的任务。对于包内的某些内部函数,在这种情况下返回$Failed 似乎比发出错误消息并返回未评估更合适。此外,在实践中,它更容易 - 很少有人会向其所有内部函数提供错误消息(无论如何这也可能是一个坏主意,因为用户不应该关心代码的一些内部问题),同时返回$Failed 快速而直接。当许多辅助函数返回$Failed而不是保持沉默时,调试就容易多了。
从技术上讲,最简单的方法是使用 Return 从函数体内显式返回 $Failed,例如在此示例中的自定义文件导入函数:
ClearAll[importFile];
Options[importFile] = {ImportDirectory :> "C:\\Temp"};
importFile::nofile = "File `1` was not found during import";
importFile[filename_String, opts : OptionsPattern[]] :=
Module[{fullName =
getFullFileName[OptionValue[ImportDirectory], filename], result},
result = Quiet@Import[fullName, "Text"];
If[result === $Failed,
Message[importFile::nofile, Style[fullName, Red]];
Return[$Failed],
(* else *)
result
]];
但是,通常以@Verbeia 的答案中概述的方式使用模式匹配器更方便。对于无效输入参数的情况,这是最简单的。例如,我们可以很容易地向上面的函数添加一条包罗万象的规则,如下所示:
importFile[___] := (Message[importFile::badargs]; $Failed)
有更多有趣的方式使用模式匹配器,见下文。
这里的最后一条评论是,每个可能返回$Failed 的链接函数的一个问题是需要大量If[f[arg]===$Failed, Return[$Failed],do-somthing] 类型的样板代码。我最终使用这个更高级别的函数来解决这个问题:
chainIfNotFailed[funs_List, expr_] :=
Module[{failException},
Catch[
Fold[
If[#1 === $Failed,
Throw[$Failed, failException],
#2[#1]] &,
expr,
funs], failException]];
它通过异常停止执行,并在任何中间函数调用导致$Failed 时立即返回$Failed。例如:
chainIfNotFailed[{Cos, #^2 &, Sin}, x]
Sin[Cos[x]^2]
chainIfNotFailed[{Cos, $Failed &, Sin}, x]
$Failed
可以使用Throw 抛出异常,而不是返回$Failed。
这种方法在 IMO 几乎从不适合向用户公开的*功能。 Mathematica 异常没有被检查(在 Java 中被检查的异常的意义上),并且 mma 不是强类型的,所以没有好的语言支持的方式告诉用户在某些事件中可能会抛出异常。但是,它对于包中的内部函数可能非常有用。这是一个玩具示例:
ClearAll[ff, gg, hh, failTag];
hh::fail = "The function failed. The failure occured in function `1` ";
ff[x_Integer] := x^2 + 1;
ff[args___] := Throw[$Failed, failTag[ff]];
gg[x_?EvenQ] := x/2;
gg[args___] := Throw[$Failed, failTag[gg]];
hh[args__] :=
Module[{result},
Catch[result =
gg[ff[args]], _failTag, (Message[hh::fail, Style[First@#2, Red]];
#1) &]];
以及一些使用示例:
hh[1]
1
hh[2]
hh::fail: The function failed.
The failure occured in function gg
$Failed
hh[1,3]
hh::fail: The function failed.
The failure occured in function ff
$Failed
我发现这种技术非常有用,因为如果始终使用它,它可以非常快速地定位错误源。这在几个月后使用代码时特别有用,因为您不再记得所有细节。
什么不能返回
不要返回Null。这是模棱两可的,因为Null 对于某些函数可能是有意义的输出,不一定是错误。
不返回使用Print 打印的错误消息(从而返回Null)。
不要返回Message[f::name](再次返回Null)。
1234563在 mma 中(可能只是我。但同时,我在 C 和 Java 中使用了很多)。我的猜测是,这在更强大(也可能是静态)类型的语言中变得更有益。
使用模式匹配器简化错误处理代码
@Verbeia 的回答中已经描述了其中一个主要机制 - 使用模式的相对普遍性。关于这一点,我可以指出例如this 包,我经常使用这种技术,作为这种技术的工作示例的额外来源。
多条消息问题
该技术本身可用于上面讨论的所有 3 种返回情况。但是,对于返回未计算函数的第一种情况,有一些微妙之处。一个是,如果您有多个“重叠”模式的错误消息,您可能希望“短路”匹配失败。我将说明这个问题,借用here 的讨论。考虑一个函数:
ClearAll[foo]
foo::toolong = "List is too long";
foo::nolist = "First argument is not a list";
foo::nargs = "foo called with `1` argument(s); 2 expected";
foo[x_List /; Length[x] < 3, y_] := {#, y} & /@ x
foo[x_List, y_] /; Message[foo::toolong] = Null
foo[x_, y_] /; Message[foo::nolist] = Null
foo[x___] /; Message[foo::nargs, Length[{x}]] = Null
我们叫错了:
foo[{1,2,3},3]
foo::toolong: List is too long
foo::nolist: First argument is not a list
foo::nargs: foo called with 2 argument(s); 2 expected
foo[{1,2,3},3]
显然,生成的消息是相互矛盾的,而不是我们想要的。原因是,由于在这种方法中错误检查规则被认为是不匹配的,如果模式构造得不够仔细,模式匹配器会继续并尝试多个错误检查规则。避免这种情况的一种方法是仔细构建模式,使它们不会重叠(互斥)。在提到的线程中讨论了其他一些方法。我只是想提请注意这种情况。请注意,当显式返回 $Failed 或抛出异常时,这不是问题。
将Module、Block 和With 与共享局部变量一起使用
此技术基于具有条件模式的定义的语义,涉及范围构造Module、Block 或With。提到了here。这种构造类型的一大优点是它允许执行一些计算,然后才在函数评估的中间某处确定错误的事实。然而,模式匹配器会将其解释为模式不匹配,并继续执行其他规则,就好像从未发生过针对该规则的主体评估(即,如果您没有引入副作用)。下面是一个查找文件“短名称”的函数示例,但会检查文件是否属于给定目录(否定目录被视为失败):
isHead[h_List, x_List] := SameQ[h, Take[x, Length[h]]];
shortName::incns = "The file `2` is not in the directory `1`";
shortName[root_String, file_String] :=
With[{fsplit = FileNameSplit[file], rsplit = FileNameSplit[root]},
FileNameJoin[Drop[fsplit, Length[rsplit]]] /;isHead[rsplit, fsplit]];
shortName[root_String, file_String]:= ""/;Message[shortName::incns,root,file];
shortName[___] := Throw[$Failed,shortName];
(在我使用它的上下文中,它适合Throw 异常)。我觉得这是一个非常强大的技术,并且经常使用它。在this 线程中,我给出了一些我知道的使用示例的指针。
带有选项的函数
函数接收选项的情况在 IMO 中并不是很特殊,因为到目前为止我所说的任何内容也适用于它们。一件很难的事情是错误检查传递的选项。我尝试使用包CheckOptions 和PackageOptionChecks(可以找到here)来自动化这个过程。我时不时会使用这些,但不能说它们对其他人是否有用。
元编程和自动化
您可能已经注意到许多错误检查代码是重复的(样板代码)。尝试自动执行错误检查定义的过程似乎是一件很自然的事情。我将举一个例子来说明 mma 元编程的强大功能,它通过对上面讨论的带有内部异常的玩具示例进行自动错误检查。
以下是自动化流程的功能:
ClearAll[setConsistencyChecks];
Attributes[setConsistencyChecks] = {Listable};
setConsistencyChecks[function_Symbol, failTag_] :=
function[___] := Throw[$Failed, failTag[function]];
ClearAll[catchInternalError];
Attributes[catchInternalError] = {HoldAll};
catchInternalError[code_, f_, failTag_] :=
Catch[code, _failTag,
Function[{value, tag},
f::interr = "The function failed due to an internal error. The failure \
occured in function `1` ";
Message[f::interr, Style[First@tag, Red]];
f::interr =.;
value]];
这就是我们之前示例的重写方式:
ClearAll[ff, gg, hh];
Module[{failTag},
ff[x_Integer] := x^2 + 1;
gg[x_?EvenQ] := x/2;
hh[args__] := catchInternalError[gg[ff[args]], hh, failTag];
setConsistencyChecks[{ff, gg}, failTag]
];
您可以看到它现在更加紧凑,我们可以专注于逻辑,而不是被错误检查或其他簿记细节分散注意力。额外的好处是我们可以使用Module- 生成的符号作为标签,从而封装它(不暴露于顶层)。以下是测试用例:
hh[1]
1
hh[2]
hh::interr: The function failed due to an internal error.
The failure occured in function gg
$Failed
hh[1,3]
hh::interr: The function failed due to an internal error.
The failure occured in function ff
$Failed
许多错误检查和错误报告任务可以以类似的方式自动化。在他的第二篇文章here 中,@WReach 讨论了类似的工具。