C# 使用异常而不是冗长的空检查是否可以接受?

C# 使用异常而不是冗长的空检查是否可以接受?,c#,java,exception,null,C#,Java,Exception,Null,我最近在一个项目中遇到了这个问题:有一个嵌套对象链,例如:类a包含类B的实例变量,而类B又有一个类C的实例变量,…,直到我们在类Z的树中有一个节点 ----- ----- ----- ----- ----- | A | ---> | B | ---> | C | ---> | D | ---> ... ---> | Z | ----- ----- -----

我最近在一个项目中遇到了这个问题:有一个嵌套对象链,例如:类a包含类B的实例变量,而类B又有一个类C的实例变量,…,直到我们在类Z的树中有一个节点

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

现在想象一下,在应用程序中的某个点上,我们有一个对实例的引用,并且只有当它包含一个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){}
这段代码的问题是,在维护它时可能会混淆,因为它没有清楚地显示哪些对象允许为null。但另一方面,它要简洁得多,不太“伸缩”

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

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

(我使用过Java代码,但同样的问题也适用于C#properties)


谢谢。

如果parentObject需要知道A包含一个B,而B包含一个C,而C包含。。。。这样,一切都与一切相耦合。你应该看看德米特定律:

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

public class A {
  private B myB;
  //...
  public boolean isItValidToDoSomething(){
    if(myB!=null){
      return myB.isItValidToDoSomething();
    }else{
      return false;
    }
  }
}
最终,在Z级别,该方法必须返回true


Imho,节省开发时间从来不是容忍设计中出现问题的理由。这些问题迟早会占用你比一开始解决问题更多的时间

我个人喜欢使用。通过将这些方法/属性返回的值调整为
Option
而不是
T
,调用者可以选择如何处理无值的情况

选项类型可以有包含的值,也可以没有包含的值(但选项本身永远不能为null),但是调用方不能在不展开值的情况下简单地传递它,因此它会强制调用方处理可能没有值的事实

例如,在C#中:

如果调用方希望在任何步骤没有值时只使用默认值,则可以执行映射/绑定/投影:

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 

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

在这里使用异常是不好的做法

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

相反,我会查看类层次结构,并试图理解为什么需要进行如此深入的访问链接。这似乎是一个很大的设计问题,您通常不应该期望调用方使用隐藏在类a中的对象结构的深层知识来构造调用

你可以问的问题:

  • 为什么调用方仍然需要对Z对象执行doSomething()?为什么不把doSomething()放在A类上呢?如果需要并且相关字段不为空,这可能会将doSomething()沿链传播
  • 如果空值存在于该链中,它意味着什么?空值的含义将表明应该使用什么业务逻辑来处理它。。。。在每一个层次上都可能有所不同
总的来说,我认为正确的答案是将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()可用,您似乎想调用它,但如果它不可用,您就不在乎了。在这种情况下,我想说捕获您所关注的特定异常与详细检查一样可以接受,以确保您一开始不会抛出异常。有许多“空安全”方法和扩展,它们使用try-catch的方式与您提出的方式非常相似;“ValueOrDefault”类型的方法是非常强大的包装器,用于确切地说明使用try-catch的原因

根据定义,Try/catch是一个程序流控制语句。因此,预计将用于“控制普通程序流”;我认为您试图做出的区别是,它不应该用于控制正常无错误逻辑流的“快乐路径”。即使这样,我也可能不同意;在.NET Framework和第三方库中,存在返回所需结果或引发异常的方法。“异常”不是“错误”,除非你因为它而无法继续;如果您可以尝试其他方法,或者可以将情况归结为某种默认情况,那么接收异常将被视为“正常”。因此,catch-handle-continue是try-catch的一个非常有效的用法,框架中异常抛出的许多用法都希望您能够可靠地处理它们

您想要避免的是使用try/catch作为“goto”,方法是抛出并非真正异常的异常,以便在满足某个条件后“跳转”到catch语句。这绝对是一个黑客,因此是一个糟糕的程序
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);
class A {
  ...
  public void doSomething() {
    B b=getB();
    if (b!=null) {
      b.doSomething();
    } else {
      // do default action in case of null B value
    }
  }
}
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<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();
    }
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();
}