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

时间:2012-01-30 16:00:23

标签: c# java exception null

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

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

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

现在想象一下,在应用程序的某个点,我们有一个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();
    }

我知道异常不应该用于普通的控制流,但是我看到一些程序员没有使用前面的代码:

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

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

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

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

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

感谢。

9 个答案:

答案 0 :(得分:16)

如果parentObject需要知道A包含一个包含一个包含C的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,节省开发时间绝不是容忍设计问题的理由。这些问题迟早会比你在第一时间解决问题所花费的时间更多

答案 1 :(得分:4)

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

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

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

您可以提出的问题:

  • 为什么调用者需要使用Z对象执行Something()?为什么不把doSomething()放在A类上呢?如果需要,如果相关字段不为null,这可以在链中传播doSomething()....
  • 如果此链中存在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(),你就可以获得额外的奖励,你可以为每个级别的空值指定不同的默认操作。

答案 2 :(得分:3)

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

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

E.g。在C#中:

class A {
    Option<B> B { get { return this.optB; } }
}

class B {
    Option<C> 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 => a.B) // gives the value or empty Option<B>
       .Convert(b => b.C) // gives value or empty Option<C>
       .Convert(c => c.D) // gives value or empty Option<D>
       .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上找到实现。如果您喜欢我的,请告诉我,我会发给您。

答案 3 :(得分:3)

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

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

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

答案 4 :(得分:2)

&#34;遇到异常的问题&#34;方法是看起来有点笨拙。异常堆栈跟踪应该显示它失败的位置,因为您的方法名称非常清楚您在层次结构中的位置,但它不是一个很好的方法。另外,您如何从异常中恢复并继续保持良好的代码状态?

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

使&#34;空&#34;状态实例静态,系统中只有一种类型。但你需要考虑一下&#34;空的&#34;状态看起来类似于层次结构中的每种类型,并确保它不会无意中影响应用程序的任何其他部分。

答案 5 :(得分:2)

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

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

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

答案 6 :(得分:2)

使用异常似乎不适合这里。如果其中一个getter包含非平凡的逻辑,并抛出NullPointerException怎么办?您的代码将吞下该异常,而无意。在相关说明中,如果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();
}

答案 7 :(得分:0)

我认为你可以在每个类上提供静态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
}

然而,虽然我不打算开展业务,但我必须说这种链接方法并没有说明设计的任何好处;也许这是重构需要的标志。

答案 8 :(得分:0)

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

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

    public IEnumerable<type> GetSubProperties(ClassA A)
    {
        yield return A;
        yield return A.B;
        yield return A.B.C;
        ...
        yield return A.B.C...Z;
    }

然后使用它:

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

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