扩展方法引发的ArgumentNullException或NullReferenceException?

45

当在空实例上调用扩展方法(不允许该扩展方法)时,你认为最好抛出什么类型的异常?由于扩展方法只是静态方法,因此可以认为应该抛出ArgumentNullException,但另一方面,它们像实例方法一样使用,因此可能更自然地使用NullReferenceException。让我们看以下示例:

public static string ToInvariantString(this IFormattable value, string format)
{
    return value.ToString(format, CultureInfo.InvariantCulture);
}

如果value参数为null,将抛出NullReferenceException异常。

另一个例子是:

public static string ToInvariantString(this IFormattable value, string format)
{
    if (value == null) throw new ArgumentNullException("value");
    return value.ToString(format, CultureInfo.InvariantCulture);
}

编辑: 在一些回答中,您指出扩展方法可以像静态方法一样调用,在这种情况下,空引用异常是错误的,这是一个很好的观点,实际上也是我担心的问题之一,不确定为什么我忘记在问题中首先提到它。

有人还指出抛出NullReferenceException是错误的,是的,确实如此。那就是为什么我不抛出它,我只是让它发生(让CLR抛出它)而不保护方法。

我认为我倾向于ArgumentNullException(这是我迄今为止使用的方法),但我仍然认为至少有理由争论NullReferenceException,因为它在大多数将使用该方法的地方似乎更自然。

6个回答

43

总的来说,除了一些例外情况,您应该将扩展方法视为普通静态方法。在这种情况下,您应该抛出ArgumentNullException。

在此处抛出NullReferenceException是一个不好的想法,原因如下:

  • 实际上并没有发生空引用,所以看到一个空引用是违反直觉的
  • 抛出NullReferenceException并导致NullReferenceException发生会产生可辨别的不同异常(可以通过错误代码来看到区别)。对于CLR抛出的许多异常都是如此。

请参见何时可以捕获StackOverflowException (我关于此主题的帖子)。

  • 调用扩展方法与调用常规方法完全合法。在这种情况下,我当然不会期望NullReferenceException,而是期望ArgumentNullException.

很棒的评论,正如我在帖子中所编辑的那样,这就是我不会显式抛出NullReferenceException的原因,我仍然让CLR抛出它。 - Patrik Hägne
1
“空引用实际上并没有发生”。是的,它发生了;您传递了一个空引用,而该方法尝试对其进行取消引用。 - piedar
@piedar 这个方法试图取消引用它,而不是运行时! - Ohad Schneider
@OhadSchneider 实际上,它也没有。 - Marc.2377
1
NullReferenceException实际上表示空的引用。调整您的术语并指出一点。不过,最终问题在于您是否希望将扩展方法视为实例或静态。如果最佳实践是将它们视为实例(我认为不是),则抛出NullReferenceException将是最直观的,“技术上”正确或不正确。 - snarf
@JaredPar:通过反射给只读变量赋值或创建一个实例而不调用任何构造函数也是完全合法的,但这并不是API创建者预期的行为,因此你必须自担风险。 - Christoph

29

除了其他答案(都很好),我认为值得看一下微软的做法,以保持一致性...就我所见,Enumerable中的扩展方法全部抛出ArgumentNullException异常。


好主意,我自己也有过这个想法,但没去实现,不确定为什么!;-) - Patrik Hägne
1
这是一种我自己经常使用的有用技术。然而,人们应该始终记住,仅仅因为FCL使用某种方法来实现某事,并不意味着该方法是最优或正确的。我建议将FCL方法视为实现某事的建议,但应将给定方法的优点与替代方案进行比较,然后判断它是否是最佳选择。 - Adam Ralph
微软两者都做,这并没有帮助。 - Christoph
@Christoph:你能给一些例子吗?我的经验是微软在这方面做得相当好(使用ArgumentNullException)。 - Jon Skeet

8

既然扩展方法可以在C# 2.0中使用,并且它们可以像静态方法一样被调用(你不必将它们用作扩展方法),因此你应该使用ArgumentNullException。

仅仅因为它们看起来像类型的方法,这并不意味着它们就是方法,或者总是像方法一样被调用。


3

从用户的角度来看,这种方法看起来和行为像是一个实例方法,所以如果我是他们,我会期望看到一个NullReferenceException。

话虽如此,我建议在代码中明确地抛出其中之一,而不仅仅是像你第一个示例中那样“偶然”抛出一个异常。


是的,我考虑过显式抛出NullReferenceException,但我的感觉是它应该真正保留给编译器,但你很可能是正确的。 - Patrik Hägne
2
从用户的角度来看,扩展方法可以作为扩展方法或静态方法进行调用。 - JaredPar
我同意mqp的观点,不同意JaredPar的观点:从用户的角度来看,扩展方法永远不应该通过静态接口调用,并且它应该始终像实例方法一样行为。因此,我建议自己抛出一个没有参数名称的NullReferenceException。不要在扩展方法中允许其他错误处理(第一个参数)-尽管这很酷,而且经常看到例如在未初始化的字符串上调用.Left(..)扩展方法时返回一个空字符串,但这完全是意料之外和不透明的。 - Christoph

2

参数空异常。没有必要将扩展方法调用视为实例方法。您可以像调用普通方法一样调用它们。在这种情况下,NullReferenceException是完全不正确的。


0
为了增加混淆,微软同时抛出ArgumentNullExceptions和NullReferenceExceptions。这个抛出隐式NullReferenceException的例子来自Roslyn(src\Workspaces\CSharp\Portable\Extensions\StringExtensions.cs):
internal static class StringExtensions
{
    public static string EscapeIdentifier(
        this string identifier,
        bool isQueryContext = false)
    {
        var nullIndex = identifier.IndexOf('\0');
        if (nullIndex >= 0)
        {
            identifier = identifier.Substring(0, nullIndex);
        }

        var needsEscaping = SyntaxFacts.GetKeywordKind(identifier) != SyntaxKind.None;

        // Check if we need to escape this contextual keyword
        needsEscaping = needsEscaping || (isQueryContext && SyntaxFacts.IsQueryContextualKeyword(SyntaxFacts.GetContextualKeywordKind(identifier)));

        return needsEscaping ? "@" + identifier : identifier;
    }

这是从.NET Framework (System.Core/System/Linq/Enumerable.cs)中抛出ArgumentNullException的扩展方法:

public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) {
            if (source == null) throw Error.ArgumentNull("source");
            if (selector == null) throw Error.ArgumentNull("selector");
            if (source is Iterator<TSource>) return ((Iterator<TSource>)source).Select(selector);
            if (source is TSource[]) return new WhereSelectArrayIterator<TSource, TResult>((TSource[])source, null, selector);
            if (source is List<TSource>) return new WhereSelectListIterator<TSource, TResult>((List<TSource>)source, null, selector);
            return new WhereSelectEnumerableIterator<TSource, TResult>(source, null, selector);


   }

如上评论所述,我建议将扩展方法实现为实例方法,并在this参数为null时抛出NullReferenceException。如果有人不当地调用我的扩展方法,他们可以这样做,但也必须预期不当行为(而不是ArgumentNullExceptionNullReferenceException)。但是,如果他们按照预期调用该方法,他们也应该获得预期的行为:一致的体验。
//Instance method
string foo = null;
foo.Trim();

//Extension method
string foo = null;
foo.Right(10); 

它们看起来很相似,应该表现得相似,程序员甚至不需要知道它是实例方法还是扩展方法。


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