C#: 在这种情况下,我需要检查 null 吗?

5

假设我有这个扩展方法:

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<TSource> Method (IEnumerable<TSource>)。如果你不相信,请自行尝试 =)

注意2: 在这里给出的答案后,我被说服开始更多地检查空值。虽然我仍然很懒,但我已经开始使用Lokad Shared Libraries中的Enforce类。可以推荐看一下它。与我的示例不同,我可以这样做:

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

19
是的,它会抛出一个ArgumentNullException。我可以想到两个原因来添加额外的检查:
  • 如果以后您回去更改该方法以在调用subjects.Count()之前执行某些操作并忘记在那一点上放置检查,您可能会在异常被抛出之前产生副作用,这不好。
  • 目前,堆栈跟踪将在顶部显示subjects.Count(),并且可能带有具有source参数名称的消息。对于可以看到subjects参数名称的HasFive的调用者来说,这可能会令人困惑。

编辑:只是为了让我不必再写一遍:

调用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;
}

(在一些不重要的地方,你可能需要一个没有参数名称的重载。)

@Jon,将NullGuard作为扩展方法而不是类上的静态方法会增加多少开销? - Chris S
@Chris:性能开销?没有。这只是语法糖——IL 仍将调用静态方法。 - Jon Skeet
@codeape:在扩展方法之前,它不行。使用扩展方法可以非常方便,特别是像这样的整洁操作。考虑一下如果String.IsNullOrEmpty是一个扩展方法:if (x.IsNullOrEmpty()) ... vs if (String.IsNullOrEmpty(x)) - Jon Skeet
希望.NET4有个.IsFull()方法:P 大部分时间我都是用这个:if (!string.IsNullOrEmpty()) - Chris S
@Jhonny:糟糕 - 現在正在修復 :) - Jon Skeet
显示剩余11条评论

2

我认为@Jon Skeet说得非常准确,但我想补充以下想法:

  • 提供有意义的错误消息对于调试、日志记录和异常报告非常有用。BCL引发的异常不太可能描述与您的代码库相关的特定异常情况。也许在空值检查方面这个问题不太严重(大多数情况下),因为空值检查无法提供太多领域特定信息 - “我意外地传递了一个空值,不知道为什么”是大多数情况下你所能做的最好的事情,但有时你可以提供更多信息,显然在处理其他异常类型时这更可能是相关的。
  • 空值检查清楚地向其他开发人员和你自己展示了一种文档形式,如果/当你一年后回到代码时,“有人可能会传递一个空值,并且如果他们这样做,会有问题”。
  • 在Jon的优秀观点基础上进行扩展 - 在捕获空值之前可能会执行某些操作 - 我认为参与防御性编程非常重要。在运行其他代码之前检查异常是一种防御性编程,因为您考虑到事情可能不按您预期的方式工作(或者未来可能会进行更改),并确保无论发生什么问题(假设您的空值检查没有被删除),这样的问题都不会出现。
  • 这是一种运行时断言,表明您的参数不为空。您可以在假定它不为空的情况下继续执行。
  • 上述假设可能导致更简洁的代码,您编写其余代码时知道参数不为空,从而减少了后续多余的空值检查。

1

这取决于具体的方法。在这种情况下 - 我认为,异常并不是必要的,更好的用法是,如果扩展方法可以处理 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 吗?不管怎样,这也是一个好观点。 - Svish
@Svich:Ctrl+C,Ctrl+V的bug : )。感谢您的提醒。 - TcKs

1
在我看来,你应该检查空值。有两件事需要考虑。
首先,它明确了运行时可能发生的错误。
其次,它还让你有机会抛出更好的异常,而不是通用的ArgumentNullException。因此,使异常的原因更加明确。

更好的异常?比如指定哪个参数为空?还是不同的异常?如果明确表达,我能否将异常添加到XML文档中? - Svish
是的,我的意思是要明确异常的原因。 - Marcel

1

你将会遇到的异常是一个对象引用未设置为对象实例。

当追踪问题时,这并不是最有用的异常。

通过明确指出它是你的主题引用为空,你将得到更有用的信息。


不,将引发ArgumentNullException异常,因为Count()也是一个扩展方法,其文档记录了如果源为空则会引发ArgumentNullException异常。 - Jon Skeet
是的,Count()只是作为一个例子被使用,因为它也会抛出ArgumentNullException异常。 - Svish

1

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

此外,如果您从最相关的堆栈帧中抛出带有参数名称的ArgumentNullException,那么更容易找出问题的源头。另外,函数体中的代码随时间而变化,因此我不会依赖它来捕获未来的前置条件问题。


1

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

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

当编写在项目内部使用的方法时,我通常会跳过这些检查,试图减少代码库的大小。但即使在这种情况下,可能仍然存在一些层次(在应用程序层之间),您仍然可以放置此类检查。这取决于上下文,项目的规模,团队的规模...

对于由一个人构建的小型项目,这肯定没有意义 :)


不,调用Count()会抛出ArgumentNullException异常,因为Count()是一个扩展方法。换句话说,相同的检查已经在另一个方法中执行了。结果仍然是一个ArgumentNullException异常,但堆栈跟踪更大且消息不同。 - Jon Skeet
是的,我刚刚检查过了,你是正确的。我会删除那部分,但答案的其余部分仍然有效... - Dan C.

0
如果您查看 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(),异常仍然会立即抛出。唯一的区别将在堆栈跟踪和错误消息中。 - Jon Skeet
它们会给出相同的堆栈跟踪,因为Error.ArgumentNull()返回一个新的ArgumentNullException。 - Chris S

0
在我看来,人们应该检查那些可能会在以后引发错误的已知条件(至少对于公共方法)。这样可以更容易地检测问题的根源。
我会提出一个更具信息性的异常,比如:
if (subjects == null)
{
     throw new ArgumentNullException("subjects ", "subjects is null.");
}

那没有提供比所示例更多的信息。唯一的信息就是参数的名称为空,而这已经被提供了。为什么要把事情复杂化呢? - Jon Skeet

0

有两个原因:

首先,IEnumerable上的其他扩展方法都是这样做的,你的消费者也可以期望您的代码能够如此执行;其次,更重要的是,如果您在查询中有一个长链操作符,那么知道是哪一个抛出了异常就是有用的信息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接