【问题标题】:Handling null objects when calling methods调用方法时处理空对象
【发布时间】:2017-06-01 20:35:29
【问题描述】:

我一直在寻找在调用方法(或方法链)时处理空对象的最佳选择。

我们通常的做法是检查 if 条件:

if ( customObject != null ) {
    customObject.callMe();
}

通过使用扩展方法可以进一步改进:

Program customObject = null;
if (customObject.NotNull()) {
    customObject.CallMe();
}

public static bool NotNull(this object o) {
    return o == null;
}

请注意:我通常忽略!来自我的编程实践。因此,明智的做法是说扩展方法对我来说很好。

但是,当涉及到方法链时,处理变得非常复杂。

customObject.CallMe().CallMe2() ex... 

您认为它可以在 C# 中如何处理,因此仅当 customObject 不为 null 时才调用 CallMe,并且仅当 CallMe 返回非 null 对象时才调用 CallMe2

当然我可以使用 If 条件或三元运算符。但是,我想知道 vNext,C#5.0 是否有一些东西可以提供。

【问题讨论】:

  • 从哪方面来说,使用 NotNull() 是一种改进?
  • 通过使用扩展方法来适应实际情况,我可以控制我定义为 Null 的内容,因此有改进。 (我通常忽略!从我的编程实践中)。
  • 您可能想查看Null Object Pattern
  • 至少 if(myObject != null) 是一个清晰的代码。我不确定使用模式来避免这种代码是否会帮助下一个接受你的代码的程序员。

标签: c# c#-5.0 null-conditional-operator


【解决方案1】:

在即将到来的 C# 6 (vNext) 中,?. 运算符(Null Conditional Operator)允许您轻松地为每个嵌套属性链接空引用检查。

这方面的一个例子是:

int? first = customers?.[0].Orders?.Count();

这是 Visual Studio UserVoice 站点中请求的功能

Add ?. Operator to C#

您可以在 Roslyn 的 Codeplex 网站上查看 C# 6.0 的所有新语言功能的状态:

C# 6 Language Features Status

【讨论】:

  • 对即将推出的新功能感到非常兴奋
  • 好吧,这很有意义,因为目前没有办法,所以我接受这个作为我的答案,因为这正是我想要的。我确实听说过?但并没有给予太多关注。谢谢斯科特
【解决方案2】:

您目前可以编写这些类型的扩展方法:

public static class Extensions
{
    public static R IfNotNull<T, R>(this T @this, Func<T, R> @select) where T : class
    {
        return @this.IfNotNull(@select, () => default(R));
    }

    public static R IfNotNull<T, R>(this T @this, Func<T, R> @select, Func<R> @default) where T : class
    {
        return @this != null ? @select(@this) : @default();
    }

    public static void IfNotNull<T>(this T @this, Action<T> @action) where T : class
    {
        if (@this != null)
        {
            @action(@this);
        }
    }
}

然后这样称呼他们:

customObject 
    .IfNotNull(y => y.CallMe())
    .IfNotNull(y => y.CallMe2());

【讨论】:

    【解决方案3】:

    C# 5 无法简化您的代码。你被困在其中之一:

    • 嵌套 if 条件
    • 复杂的三元表达式
    • Englobing try/catch 捕捉 NullReferenceException(如果你的链中的 null 不是真正的异常,你真的不应该这样做,即它不应该发生,但你想要一个 keepsafe)。

    但是,作为空传播运算符 ?,未来可能会更加光明。是 C# 6 的一个提议的特性,并且有 already be implemented is Roslyn,所以如果它在 C# 6 中没有有效,那将是一个很大的惊喜。有了这个运算符,您将能够编写

    customObject?.CallMe()?.CallMe2()?.[...] 
    

    并且编译器会为你创建嵌套的 if(甚至确保每个方法只被调用一次)。所以,多一点耐心……

    【讨论】:

    • 这到底怎么比例外更好?它基本上是一种自动吃异常的方法。您试图忽略返回的 null 并剪辑执行链,而不解决逻辑错误。
    • @JonJayObermark 这比吃异常要好得多,因为新的运算符永远不会引发异常。这不是尝试/捕获。它评估表达式,检查结果是否为 null,如果不是则调用方法/属性/字段,否则返回 null。那里没有逻辑错误,除了首先允许在语言中进行空引用的错误。这可能是一个设计错误(我相信它是),但至少现在我们有了一个新工具可以更轻松地处理这个错误。
    • 好的,但是您是否希望不必检查列表是否为空,或者是否发生了其他各种意外情况。任何您没有按设计选择使其有意义的 null 确实是一个例外,无论您是否这样对待它。
    • @JonJayObermark ?。运算符绝不会为您提供它可能具有的 null 的语义。 null 的含义仍然取决于您的业务逻辑。我预计最常见的用例是var myValue = someVariable?.SomeProperty ?? defaultValue;
    • @JonJayObermark 不管发生什么,它都不是一个例外。一个例外是相当严重的事情。它会中断执行流程。它与堆栈混淆。就性能而言,这是非常昂贵的。它将使调试器在正确的配置下中断。它将产生日志消息。您可能希望通过简单的健全性检查来避免所有事情。这 ?。操作员会进行这种健全性检查,仅此而已。
    【解决方案4】:

    C# 6 确实有 ? 运算符,否则您可以查看 monad,尤其是 Maybe monad,例如herehereelsewhere。是否值得在设计时没有考虑到它们的非函数式语言中使用 monad 是另一个问题。

    【讨论】:

    • IEnumerable、Nullable、Func、Lazy 和其他一些内置类型让我相信这是值得的 ;) 事实上,C# 的大多数更新和最吸引人的语言特性都与 monad 或共胞(任务...)
    • @Falanwe 同意,但是将 monads 理解为一个概念并不容易,因为我对函数式语言不太熟悉,而且我不会费心去解决一些 if (foo == null)在这种情况下是块。
    • 关于 monad 的一个非常好的事情是你不需要真正理解它们就可以使用它们。当你真正理解它们时,它会打开如此广泛的可能性......
    【解决方案5】:

    由于人们正在撕毁假设您不是白痴的答案,因此这里假设您是白痴。

    您应该检查customObject 的创建时间,因为您会知道为什么会得到一个空值。如果你当时没有检查,现在最好的办法是让你的代码抛出异常。如果在没有明确需要的对象的情况下仍然可以做一些完全有意义的事情,您可以在异常的 catch 中修复它,但可能没有。

    所有忽略意外空值重要性的机制只是传递了之前没有注意的失败,包括习惯性地过度使用所提出的空值传播运算符,以及所有在使用前检查对象的习惯。他们正在寻求对灾难的有效反应,以尽量减少对灾难的关注。

    如果你用一个对象的空值表示重要信息,那可能是一个糟糕的设计选择,你应该在引入空值时将它转换为更有用的信息。

    在对象被使用之前而不是在它产生之后测试它的意外空值并不能真正帮助任何人。这是一种代码气味,表明您错位了责任,可能是以下方式之一:

    1. 你不愿意挑战那些在没有充分解释的情况下做出尴尬的设计决策的人
    2. 您觉得自己无法控制已合并的内容,但又懒得包装它
    3. 您认为 null 不应该存在,并且您试图忽略它们,因此您没有考虑过任何关于它们的政策。

    您指出的策略,即吃掉 null 并继续执行而不分析它,是一个糟糕的策略。如果您打算保留它,那是我的另一个答案。你应该做一些合理的事情。

    在许多工作场所,在任何地方放置一个空的 catch 块是对未来工作的清晰记录。所以它正在做某事。但是离开它永远不会有一个适合好的代码的选项。这样的块将更合理地将异常转换为响应,从而创建未来的工作来修复错误的根源,即使代码块实施了一种以更本地方式解决问题的变通方法。

    【讨论】:

      【解决方案6】:

      这是异常处理的重点。如果您想响应异常情况,例如从函数返回意外的 null,则 catch NullReferenceException,并从那里。异常本身将停止进一步的调用,因此如果您只想完成这一切,请不要实施恢复代码。 我不认为这是一个合理的回应,这样的事情通常至少应该被记录下来。

      try
      {
          customObject.CallMe().CallMe2()
      }
      catch (NullReferenceException ex)
      {
         /* save enough details to determine why you got an unexpected null */
      }
      

      不要使用异常处理来完成正常的编程任务,但不要过度避免它,通过检查每一个可能的意外事件,无论多么罕见。确定您的代码中可能出现的故障(您可以做任何事情),并在每个合理大小的块的末尾包含一个健康的 catch 列表。

      【讨论】:

      • -1:绝对不是。 NullReferenceException 几乎总是代表代码中的错误。它永远不应该被“处理”——它应该是固定的
      • 大多数异常代表应该修复的东西。但是在运行期间立即修复它们是不切实际的。因此存在异常,因此可以记录和解决它们。就像我说的,不要故意使用它们来控制流量,但如果机制没有用,它就不会存在。一直检查 null 并不是一个更好的解决方案,因为 null 返回很可能是一个错误。
      • 你完全错了。不存在让您忽略异常的机制。它的存在是为了让您了解代码中的错误并可以尽快修复它。还是您认为程序绝不能崩溃?
      • 我见过很少有NullReferenceException 的案例实际上是例外。大多数是由于开发人员不知道null 的含义,所以不要检查它,或者不知道null 可能被返回的可能性。在这些情况下,最好的办法通常是让程序立即崩溃,并将这一事实告知其主管。
      • @codebased 这正是本网站提供“downvote”选项的原因——当您觉得答案不正确时。我也不同意这个答案——还有很多其他因素。一个简单的null 检查远比允许抛出异常和遍历堆栈的性能要好得多。 NullReferenceExceptions 总是可以在不捕获它们的情况下修复 - 我会拒绝我查看过的任何代码
      猜你喜欢
      • 1970-01-01
      • 2013-01-23
      • 1970-01-01
      • 1970-01-01
      • 2013-11-26
      • 2013-08-07
      • 2012-01-25
      • 1970-01-01
      相关资源
      最近更新 更多