【问题标题】:C#: Should I bother checking for null in this situation?C#:在这种情况下我应该费心检查 null 吗?
【发布时间】:2009-03-20 09:23:16
【问题描述】:

假设我有这种扩展方法:

public static bool HasFive<T>(this IEnumerable<T> subjects)
{
    if(subjects == null)
        throw new ArgumentNullException("subjects");

    return subjects.Count() == 5;
}

你认为这种空检查和异常抛出真的有必要吗?我的意思是,当我使用Count 方法时,无论如何都会抛出ArgumentNullException,对吧?

我可能会想到一个我应该这样做的原因,但只是想听听其他人对此的看法。是的,我问这个问题的部分原因是懒惰(想尽可能少写),但也因为我有点认为一堆空检查和异常抛出会使方法变得混乱,这些方法通常最终是两倍长他们真的需要。有人应该比将 null 发送到方法中更好:p

不管怎样,你们怎么看?


注意:Count() 是一个扩展方法,抛出一个ArgumentNullException,而不是NullReferenceException。见Enumerable.Count&lt;TSource&gt; Method (IEnumerable&lt;TSource&gt;)。不信你自己试试=)


注意 2: 在这里给出的答案之后,我被说服开始检查更多的空值。不过我还是很懒,所以我开始使用Lokad Shared Libraries中的Enforce类。可以推荐看看。而不是我的例子,我可以这样做:

public static bool HasFive<T>(this IEnumerable<T> subjects)
{
    Enforce.Argument(() => subjects);
    return subjects.Count() == 5;
}

【问题讨论】:

    标签: c#


    【解决方案1】:

    是的,它会抛出一个ArgumentNullException。我可以想到两个额外检查的原因:

    • 如果您稍后返回并更改方法以在调用 subjects.Count() 之前执行某些操作并且忘记在该点进行检查,您可能会在抛出异常之前产生副作用,这不是很好.
    • 目前,堆栈跟踪将在顶部显示subjects.Count(),并且可能带有带有source 参数名称的消息。这可能会让HasFive 的调用者感到困惑,因为他们可以看到subjects 参数名称。

    编辑:只是为了让我不必在其他地方再次写它:

    subjects.Count() 的调用将抛出一个ArgumentNullException不是一个NullReferenceExceptionCount() 是这里的另一种扩展方法,假设正在使用 System.Linq.Enumerable 中的实现,则记录(正确)抛出 ArgumentNullException。不信就试试看。

    编辑:让这更容易......

    如果您进行大量这样的检查,您可能希望简化操作。我喜欢下面的扩展方法:

    internal static void ThrowIfNull<T>(this T argument, string name)
        where T : class
    {
        if (argument == null)
        {
            throw new ArgumentNullException(name);
        }
    }
    

    那么问题中的示例方法就可以变成:

    public static bool HasFive<T>(this IEnumerable<T> subjects)
    {
        subjects.ThrowIfNull("subjects");    
        return subjects.Count() == 5;
    }
    

    另一种选择是编写一个检查值的版本并返回它,如下所示:

    internal static T NullGuard<T>(this T argument, string name)
        where T : class
    {
        if (argument == null)
        {
            throw new ArgumentNullException(name);
        }
        return argument;
    }
    

    然后你就可以流利地调用它了:

    public static bool HasFive<T>(this IEnumerable<T> subjects)
    {
        return subjects.NullGuard("subjects").Count() == 5;
    }
    

    这也有助于在构造函数中复制参数等:

    public Person(string name, int age)
    {
        this.name = name.NullGuard("name");
        this.age = age;
    }
    

    (对于不重要的地方,您可能需要不带参数名称的重载。)

    【讨论】:

    • 好点。在第 2 点,如果我将 subject 参数重命名为 source 会怎样?
    • 它实际上会抛出一个 System.NullReferenceException。建议您编辑帖子。
    • @codeape:不会,因为 Count() 也是一种扩展方法。建议你试试看:)
    • 自己试过了,Jon 是对的。但它依赖于 Count() 方法进行检查,因此异常不是那么准确。如果我使用的库在我没有调用的 Count() 方法中显示失败,我会感到困惑......
    • 在 System.Linq.Enumerable.Count[TSource](IEnumerable`1 源)
    【解决方案2】:

    我认为@Jon Skeet 绝对是正确的,但我想补充以下几点:-

    • 提供有意义的错误消息对于调试、日志记录和异常报告很有用。 BCL 抛出的异常不太可能描述异常 WRT 您的代码库的具体情况。也许这不是空检查的问题,它(大多数时候)不一定能给你很多特定于域的信息——“我意外地传递了一个空值,不知道为什么”几乎是你能做的最好的事情但有时您可以提供更多信息,显然这在处理其他异常类型时更有可能相关。
    • null 检查清楚地向其他开发人员和您展示了一种文档形式,如果/当您在一年后返回代码时,可能有人可能会传递 null,如果他们这样做了
    • 扩展 Jon 的优点 - 您可能会在获取 null 之前做一些事情 - 我认为参与 defensive programming 非常重要。在运行其他代码之前检查异常是一种防御性编程形式,因为您考虑到的事情可能不会按您预期的方式工作(或者将来可能会做出您没想到的更改)并确保无论如何发生(假设您的空检查未删除)此类问题不会出现。
    • 这是运行时assert 的一种形式,您的参数不为空。您可以假设事实并非如此。
    • 上述假设可能会导致代码更精简,您可以在知道参数不为空的情况下编写其余代码,从而减少无关的后续空检查。

    【讨论】:

      【解决方案3】:

      在我看来,您应该检查空值。想到了两件事。

      它明确了运行时可能发生的错误。

      它还让您有机会抛出更好的异常,而不是一般的 ArgumentNullException。因此,使异常的原因更加明确。

      【讨论】:

      • 更好的例外?比如指定哪个参数为空?还是不同的例外?在明确说明时,我是否可以将异常添加到 xml 文档中?
      • 是的。我的意思是明确说明异常的原因。
      【解决方案4】:

      您将被抛出的异常将是未设置为对象实例的对象引用。

      在追踪问题时不是最有用的异常。

      通过明确说明您的主题引用为空,您拥有它的方式将为您提供更多有用的信息。

      【讨论】:

      • 否,将抛出 ArgumentNullException,因为 Count() 也是 一种扩展方法,据记载,如果源为 null,则抛出 ArgumentNullException。
      • 是的,Count() 只是用作示例,因为它也会引发 ArgumentNullException。
      【解决方案5】:

      我认为在函数顶部进行前置条件检查是一个好习惯。也许只是我的代码充满了错误,但这种做法为我捕获了很多错误。

      此外,如果您得到一个带有参数名称的 ArgumentNullException,它是从最相关的堆栈帧中抛出的,那么找出问题的根源要容易得多。此外,您的函数主体中的代码可能会随着时间而改变,因此我不会依赖它来捕捉未来的前置条件问题。

      【讨论】:

        【解决方案6】:

        它总是取决于上下文(在我看来)。

        例如,在编写库(供他人使用)时,完全检查每个参数并抛出适当的异常当然是有意义的。

        在编写项目内部使用的方法时,我通常会跳过这些检查,试图减少代码库的大小。但即使在这种情况下,也可能存在一个级别(在应用程序层之间),您仍然可以在其中进行此类检查。这取决于环境、项目的规模、工作团队的规模......

        对于一个人构建的小项目,这样做当然没有意义:)

        【讨论】:

        • 不,调用 Count() 会引发 ArgumentNullException,因为 Count() 是扩展方法。换句话说,已经在执行相同的检查,但是方法不同。结果仍将是 ArgumentNullException,但具有更大的堆栈跟踪和不同的消息。
        • 是的,刚刚检查过,你是对的。我会删除那部分,但答案的其余部分仍然有效......
        【解决方案7】:

        这取决于具体的方法。在这种情况下 - 我认为,如果扩展方法可以处理 null,则不需要异常,并且更好的用法将是。

        public static bool HasFive<T>(this IEnumerable<T> subjects) {
            if ( object.ReferenceEquals( subjects, null ) ) { return false; }
        
            return subjects.Count() == 5;
        }
        

        如果你调用“items.HasFive()”并且“items”为空,那么items没有五个项目是真的。

        但是如果你有扩展方法:

        public static T GetFift<T>(this IEnumerable<T> subjects) {
            ...
        }
        

        应该调用“subjects == null”的异常,因为没有有效的方法,如何处理。

        【讨论】:

        • 猜猜你的 GetFifth 方法应该返回 T 吗? =) 无论如何,也很好。
        • @Svich: Ctrl+C, Ctrl+V 错误:)。感谢提醒。
        【解决方案8】:

        如果您查看 Enumerable 类 (System.Core.dll) 的源代码,其中为 IEnumerables 类定义了许多默认扩展方法,您会发现它们都使用参数检查空引用。

        public static IEnumerable<TSource> Skip<TSource>(this IEnumerable<TSource> source, int count)
        {
            if (source == null)
            {
                throw Error.ArgumentNull("source");
            }
            return SkipIterator<TSource>(source, count);
        }
        

        这有点明显,但我倾向于遵循我在基本框架库源代码中找到的内容,因为您知道这很可能是最佳实践。

        【讨论】:

        • 这里有一个有趣的区别 - 检查确保在延迟执行开始之前立即抛出异常。在这种情况下,使用 Count(),异常将仍然 i> 立即被抛出。唯一的区别在于堆栈跟踪和错误消息。
        • 他们给出了相同的堆栈跟踪,不是吗?由于 Error.ArgumentNull() 返回一个新的 ArgumentNullException
        【解决方案9】:

        是的,有两个原因:

        首先,IEnumerable 上的其他扩展方法和代码的使用者可以期望您的代码也这样做,但其次,更重要的是,如果您的查询中有很长的运算符链,那么知道 which 抛出异常是有用的信息。

        【讨论】:

          【解决方案10】:

          在我看来,应该检查以后会引发错误的已知条件(至少对于公共方法)。这样更容易发现问题的根源。

          我会提出一个更具信息性的异常,例如:

          if (subjects == null)
          {
               throw new ArgumentNullException("subjects ", "subjects is null.");
          }
          

          【讨论】:

          • 这并没有提供比所示示例更多的信息。唯一的信息是参数的名称,它是空的,并且已经被提供了。为什么要把事情复杂化?
          猜你喜欢
          • 1970-01-01
          • 2011-01-17
          • 2022-07-22
          • 1970-01-01
          • 2013-05-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多