我正在阅读优秀的《清洁规范》
一个讨论是关于将空值传递给方法。
1 2 3 4 5 6 7
| public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
return (p2.x - p1.x) * 1.5;
}
}
...
calculator.xProjection(null, new Point(12,13)); |
它代表了不同的处理方式:
1 2 3 4 5 6 7 8 9 10 11 12
| public double xProjection(Point p1, Point p2) {
if (p1 == null || p2 == null) {
throw new IllegalArgumentException("Invalid argument for xProjection");
}
return (p2.x - p1.x) * 1.5;
}
public double xProjection(Point p1, Point p2) {
assert p1 != null :"p1 should not be null";
assert p2 != null :"p2 should not be null";
return (p2.x - p1.x) * 1.5;
} |
我更喜欢断言方法,但是我不喜欢断言默认情况下处于关闭状态的事实。
该书最后指出:
In most programming languages there is no good way to deal with a null that is passed by a caller accidentally. Because this is the case, the rational approach is to forbid passing null by default.
它实际上并没有涉及如何实施此限制?
无论哪种方式,您中的任何人都有强烈的意见。
一般规则是,如果您的方法不希望使用null参数,则应抛出System.ArgumentNullException。抛出正确的Exception不仅可以保护您免受资源破坏和其他不良影响,而且还可以为代码用户提供指南,从而节省了调试代码的时间。
另请阅读有关防御性编程的文章
也不是立即使用,而是与Spec#的提及有关。有一个建议在Java的未来版本中添加"空安全类型":"增强的空处理-空安全类型"。
根据提案,您的方法将变为
1 2 3 4 5
| public class MetricsCalculator {
public double xProjection(#Point p1, #Point p2) {
return (p2.x - p1.x) * 1.5;
}
} |
其中#Point是对Point类型的对象的非null引用的类型。
断言的使用和异常的抛出都是有效的方法。任何一种机制都可以用来指示编程错误,而不是运行时错误,如此处的情况。
-
断言具有性能优势,因为它们通常在生产系统上被禁用。
-
异常具有安全性的优点,因为始终执行检查。
选择实际上取决于项目的开发实践。整个项目需要确定一个断言策略:如果选择是在所有开发过程中启用断言,那么我会说要使用断言来检查这种无效参数-在生产系统中,由于以下原因而抛出NullPointerException:无论如何,编程错误都不太可能以有意义的方式捕获和处理,因此就像断言一样。
但是实际上,我知道许多开发人员不相信断言会在适当的时候启用,因此选择了抛出NullPointerException的安全性。
当然,如果您不能为代码实施策略(例如,如果您正在创建库,并且这取决于其他开发人员如何运行您的代码),则应该选择对这些代码抛出NullPointerException的安全方法库API的一部分的方法。
It doesn't really go into how you would enforce this restriction?
如果它们传入null,则通过抛出ArgumentExcexception来强制实施。
1 2 3
| if (p1 == null || p2 == null) {
throw new IllegalArgumentException("Invalid argument for xProjection");
} |
我更喜欢使用断言。
我有一条规则,我只能在公共方法和受保护方法中使用断言。这是因为我相信调用方法应确保将有效参数传递给私有方法。
Spec#看起来非常有趣!
当类似的东西不可用时,我通常会通过运行时空检查和内部方法的断言来测试非私有方法。我没有在每个方法中显式地编写null检查代码,而是将其委托给具有check null方法的实用程序类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| /**
* Checks to see if an object is null, and if so
* generates an IllegalArgumentException with a fitting message.
*
* @param o The object to check against null.
* @param name The name of the object, used to format the exception message
*
* @throws IllegalArgumentException if o is null.
*/
public static void checkNull(Object o, String name)
throws IllegalArgumentException {
if (null == o)
throw new IllegalArgumentException(name +" must not be null");
}
public static void checkNull(Object o) throws IllegalArgumentException {
checkNull(o,"object");
}
// untested:
public static void checkNull(Object... os) throws IllegalArgumentException {
for(Object o in os) checkNull(o);
} |
然后检查变成:
1 2 3 4 5 6 7 8 9 10
| public void someFun(String val1, String val2) throws IllegalArgumentException {
ExceptionUtilities.checkNull(val1,"val1");
ExceptionUtilities.checkNull(val2,"val2");
/** alternatively:
ExceptionUtilities.checkNull(val1, val2);
**/
/** ... **/
} |
可以与编辑器宏或代码处理脚本一起添加。
编辑:详细检查也可以通过这种方式添加,但是我认为自动添加一行非常容易。
In most programming languages there is no good way to deal with a null that is passed by a caller accidentally. Because this is the case, the rational approach is to forbid passing null by default.
我发现,到目前为止,JetBrains的@Nullable和@NotNull注释方法可用于处理此问题。不幸的是,它是特定于IDE的,但确实干净,功能强大,IMO。
http://www.jetbrains.com/idea/documentation/howto.html
将此(或类似的东西)作为java标准会很好。
由于离题似乎已成为话题,因此Scala为此采取了一种有趣的方法。假定所有类型都不为空,除非您将其明确地包装在Option中以指示其可能为空。所以:
1 2 3 4 5 6 7 8 9 10 11 12
| // allocate null
var name : Option[String]
name = None
// allocate a value
name = Any["Hello"]
// print the value if we can
name match {
Any[x] => print x
_ => print"Nothing at all"
} |
我通常不喜欢任何一种,因为这只是在减慢速度。无论如何,稍后都会引发NullPointerException,这将迅速导致用户发现他们正在将null传递给该方法。我曾经检查过,但是我40%的代码最终还是要检查代码,这时我认为这不值钱。
@克里斯·卡歇尔,我会说完全正确。我要说的唯一一件事是分别检查参数,并让exeption报告参数为空的参数,因为这使跟踪零点的来源变得更加容易。
@wvdschel哇!如果编写代码对您来说太费力,则应考虑使用PostSharp(或Java等效语言,如果可用的话)之类的东西,可以对程序集进行后处理并为您插入参数检查。
稍微偏离主题,但我认为findbug的一个非常有用的功能是能够注释方法的参数,以描述哪些参数不应传递空值。
通过对代码进行静态分析,findbug可以指出使用潜在的空值调用该方法的位置。
这有两个优点:
注释描述了您应如何调用该方法的意图,并帮助了文档
FindBugs可以指向该方法的潜在问题调用者,从而使您可以跟踪潜在的错误。
仅在可以访问调用方法的代码时才有用,但通常是这种情况。
我同意还是不同意wvdschel的帖子,这取决于他的具体说法。
在这种情况下,请确保此方法将在null上崩溃,因此此处可能不需要显式检查。
但是,如果该方法仅存储传递的数据,并且稍后有其他方法可以处理该数据,则尽早发现错误的输入是更快修复错误的关键。在此之后的某个时刻,可能会有无数种方式将错误的数据恰巧提供给您的班级。这是在试图弄清事实之后老鼠如何进入您的房子,并试图在某个地方找到洞。
尽管它不是严格相关的,但您可能要看一下Spec#。
我认为它仍在开发中(由Microsoft开发),但是可以使用一些CTP,并且看起来很有希望。基本上,它允许您执行以下操作:
1 2 3 4
| public static int Divide(int x, int y)
requires y != 0 otherwise ArgumentException;
{
} |
要么
1 2 3 4 5 6
| public static int Subtract(int x, int y)
requires x > y;
ensures result > y;
{
return x - y;
} |
它还提供了诸如Notnull类型的其他功能。它建立在.NET Framework 2.0之上,并且完全兼容。您可能会看到,语法为C#。
我认为,在方法开始时立即抛出C#ArgumentException或Java IllegalArgumentException是最清晰的解决方案。
对于运行时异常,应该始终保持谨慎-方法签名上未声明的异常。由于编译器不会强制您捕获这些错误,因此很容易忘记它们。确保您具有某种"全部捕获"异常处理,以防止软件突然停止。那是用户体验中最重要的部分。
处理此问题的最佳方法实际上是使用异常。最终,断言最终将给最终用户提供类似的体验,但是在向最终用户显示异常之前,开发人员无法调用您的代码来处理这种情况。 Ultimatley,您想确保尽早测试无效输入(尤其是在面向公众的代码中),并提供调用代码可以捕获的适当异常。
以Java的方式,假设null来自编程错误(即永远不应该超出测试阶段),然后让系统将其抛出,或者如果有副作用到达该点,请在开始时检查null并抛出IllegalArgumentException或NullPointerException。
如果null可能来自实际的特殊情况,但是您不想为此使用检查的异常,那么您肯定想在方法开始时使用IllegalArgumentException路由。