是否可以使用exception而不是详细的空检查?

我在项目中遇到了这个问题:有一系列嵌套对象,例如:类A包含一个B类的实例变量,它实际上有一个类C的实例变量,……,直到我们有一个节点Z级树。

----- ----- ----- ----- ----- | A | ---> | B | ---> | C | ---> | D | ---> ... ---> | Z | ----- ----- ----- ----- ----- 

每个类为其成员提供getter和setter。 父A实例由XML解析器创建,链中的任何对象都是null是合法的。

现在想象一下,在应用程序的某个点,我们有一个A实例的引用,只有当它包含一个Z对象时,我们必须在它上面调用一个方法。 使用定期检查,我们得到以下代码:

  A parentObject; if(parentObject.getB() != null && parentObject.getB().getC() != null && parentObject.getB().getC().getD() != null && parentObject.getB().getC().getD().getE() != null && ... parentObject.getB().getC().getD().getE().get...getZ() != null){ parentObject.getB().getC().getD().getE().get...getZ().doSomething(); } 

我知道exception不应该用于普通的控制流,但是我看到一些程序员这样做,而不是以前的代码:

  try { parentObject.getB().getC().getD().getE().get...getZ().doSomething(); } catch (NullPointerException e){} 

这段代码的问题在于它在维护时可能会混淆,因为它没有清楚地显示允许哪些对象为null。 但另一方面更简洁,更不“伸缩”。

这样做可以节省开发时间吗? 如何重新设计API以避免此问题?

我唯一能想到避免长null检查的是提供嵌套对象的void实例并为它们中的每一个提供isValid方法,但是这不会在内存中创建很多不必要的对象吗?

(我使用过Java代码,但同样的问题可以应用于C#属性)

谢谢。

我个人喜欢通过使用选项类型完全避免这个问题。 通过将从这些方法/属性返回的值调整为Option而不是T ,调用者可以选择他们希望如何处理无值的情况。

选项类型可以包含或不包含(但选项本身永远不能为空),但调用者不能简单地传递它而不解包值,因此它会强制调用者处理可能没有值的事实。

例如在C#中:

 class A { Option B { get { return this.optB; } } } class B { Option C { get { return this.optC; } } } // and so on 

如果调用者想要抛出,他们只是检索值而不显式检查是否有一个:

 A a = GetOne(); D d = a.Value.B.Value.C.Value.D.Value; // Value() will throw if there is no value 

如果调用者想要默认任何步骤没有值,他们可以执行映射/绑定/投影:

 A a = GetOne(); D d = a.Convert(a => aB) // gives the value or empty Option .Convert(b => bC) // gives value or empty Option .Convert(c => cD) // gives value or empty Option .ValueOrDefault(new D("No value")); // get a default if anything was empty 

如果呼叫者想要在每个阶段默认,他们可以:

 A a = GetOne(); D d = a.ValueOrDefault(defaultA) .B.ValueOrDefault(defaultB) .C.ValueOrDefault(defaultC) .D.ValueOrDefault(defaultD); 

Option目前不是C#的一部分,但我想有一天会是。 您可以通过引用F#库来获得实现,也可以在Web上找到实现。 如果你喜欢我的,请告诉我,我会把它寄给你。

如果parentObject需要知道A包含一个包含C的B,那么这是一个糟糕的设计….这样,一切都与所有东西相连。 你应该看一下demeter法则: http : //en.wikipedia.org/wiki/Law_Of_Demeter

parentObject应该只调用其实例变量B上的方法。因此,B应该提供一个允许做出决定的方法,例如

 public class A { private B myB; //... public boolean isItValidToDoSomething(){ if(myB!=null){ return myB.isItValidToDoSomething(); }else{ return false; } } } 

最终,在Z级别,该方法必须返回true。

Imho,节省开发时间绝不是容忍设计问题的理由。 这些问题迟早会比你先解决问题所花费的时间更多

在这里使用Exceptions是不好的做法。

名称中有一个提示:exception是针对特殊情况 (即意外情况 )。 如果null是期望值,那么遇到它们并不例外。

相反,我会看一下类层次结构,并尝试理解为什么需要进行这种深度访问链接。 这似乎是一个很大的设计问题,您通常不应期望调用者使用对A类中隐藏的对象结构的深入了解来构造调用。

你可以问的问题:

  • 为什么调用者需要使用Z对象执行Something()? 为什么不把doSomething()放在A类上呢? 如果需要,这可以在链中传播doSomething(),如果相关字段不为null ….
  • 如果它存在于这个链中,null 意味着什么? null的含义将表明应该使用什么业务逻辑来处理它….在每个级别可能会有所不同。

总的来说,我怀疑正确的答案是将doSomething()放在层次结构的每个层面上并实现类似的实现:

 class A { ... public void doSomething() { B b=getB(); if (b!=null) { b.doSomething(); } else { // do default action in case of null B value } } } 

如果你这样做,那么API用户只需要调用a.doSomething(),你就可以获得额外的好处,你可以为每个级别的空值指定不同的默认操作。

那么,这取决于你在捕获中究竟做了什么。 在上面的例子中,似乎你想要调用doSomething(),如果它可用,但如果它不是你不在乎。 在这种情况下,我会说,捕获您所追求的特定exception与详细检查一样可接受,以确保您不会抛出一个开头。 有许多“null-safe”方法和扩展,它们以与你提议的方式非常类似的方式使用try-catch; “ValueOrDefault”类型的方法是非常强大的包装器,用于完成try-catch所做的事情,正是因为使用了try-catch。

根据定义,Try / catch是程序流控制语句。 因此,它有望用于“控制普通程序流程”; 我认为你试图做的区别是它不应该被用来控制正常无差错逻辑流程的“快乐路径”。 即便如此,我也可能不同意; .NET Framework和第三方库中有方法可以返回所需的结果或抛出exception。 “例外”不是“错误”,直到你因为它而无法继续; 如果你可以尝试其他东西或某些默认情况,情况可以归结为,接收exception可以被视为“正常”。 因此,catch-handle-continue是try-catch的完全有效使用,并且框架中的exception抛出的许多用法期望您可以强有力地处理它们。

你想要避免的是使用try / catch作为“goto”,抛出exception并非真正的exception,以便在满足某些条件后“跳转”到catch语句。 这绝对是一个黑客,因此编程很糟糕。

“捕捉exception”方法的问题在于它似乎有点笨拙。 exception堆栈跟踪应该显示它失败的位置,因为您的方法名称非常清楚您在层次结构中的位置,但它不是一个很好的方法。 另外,您如何从exception中恢复并继续保持良好的代码状态?

如果必须保持这个非常深的层次结构,那么您可以使用定义“空”状态的每个对象的静态实例。 我能想到的最好的例子是C# string类,它有一个静态的string.Empty字段。 然后每次调用getB()getC()getZ()都会返回实数值或“空”状态,允许您链接方法调用。

通过使“空”状态实例静态,系统中只有一种类型。 但是,您需要考虑层次结构中每种类型的“空”状态,并确保它不会无意中影响应用程序的任何其他部分。

在Python中,他们鼓励“更容易请求宽恕而非许可”的风格,这可以在这里应用,以便更好地乐观地尝试在没有安全检查的情况下到达Z,并让exception处理程序修复错过。 这样更容易编码,如果Z不在调用链中的调用比它的情况更不可能,那么它的性能会更高。

除了违反一堆OOP良好的设计原则并暴露出深层嵌套的私有成员之外,这段代码在性质上看起来也是模糊的。 也就是说,您希望调用方法X,但仅当对象上存在X时,您希望该逻辑应用于未知长度的层次结构中的所有对象。 而且您无法更改设计,因为这是您的XML翻译为您提供的。

你能改变语言吗? 静态类型的C#可能不是您在这里所做的最佳选择。 也许使用Iron Python或其他一些在打字方面稍微宽松的语言可以让你更轻松地操纵你的DOM。 一旦您将数据保持在稳定状态,您可以将其传递给C#以进行其他操作。

使用exception似乎不适合这里。 如果其中一个getter包含非平凡的逻辑,并抛出NullPointerException怎么办? 您的代码将吞下该exception而不打算这样做。 在相关的说明中,如果parentObjectnull ,则代码示例表现出不同的行为。

而且,真的没有必要“望远镜”:

 public Z findZ(A a) { if (a == null) return null; B b = a.getB(); if (b == null) return null; C c = b.getC(); if (c == null) return null; D d = c.getD(); if (d == null) return null; return d.getZ(); } 

我认为你可以在每个类上提供静态的isValid方法,例如对于类A ,它将是:

 public class A { ... public static boolean isValid (A obj) { return obj != null && B.isValid(obj.getB()); } ... } 

等等。 然后你会有:

 A parentObject; if (A.isValid(parentObject)) { // whatever } 

然而,虽然我不会涉及你的业务,但我必须说这种方法链接并没有说出任何关于设计的好处; 也许这是重构需要的标志。

我同意其他答案,这不应该做,但如果你必须在这里是一个选项:

您可以创建一个枚举器方法,例如:

  public IEnumerable GetSubProperties(ClassA A) { yield return A; yield return AB; yield return ABC; ... yield return ABC..Z; } 

然后使用它像:

  var subProperties = GetSubProperties(parentObject); if(SubProperties.All(p => p != null)) { SubProperties.Last().DoSomething(); } 

调查员将被懒惰地评估,导致没有例外。