为什么这个字符串扩展方法不会抛出异常?

时间:2015-05-11 19:31:51

标签: c# null comparison ienumerable argumentnullexception

我有一个C#字符串扩展方法,它应该返回字符串中子字符串的所有索引的IEnumerable<int>。它完美地用于其预期目的,并返回预期的结果(通过我的一个测试证明,虽然不是下面的一个),但另一个单元测试发现它有一个问题:它无法处理空参数。

这是我正在测试的扩展方法:

public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
    if (searchText == null)
    {
        throw new ArgumentNullException("searchText");
    }
    for (int index = 0; ; index += searchText.Length)
    {
        index = str.IndexOf(searchText, index);
        if (index == -1)
            break;
        yield return index;
    }
}

以下是标记问题的测试:

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Extensions_AllIndexesOf_HandlesNullArguments()
{
    string test = "a.b.c.d.e";
    test.AllIndexesOf(null);
}

当测试针对我的扩展方法运行时,它会失败,并带有标准错误消息,表明方法“没有抛出异常”。

这令人困惑:我已明确将null传递给该函数,但由于某种原因,比较null == null正在返回false。因此,不会抛出异常并且代码会继续。

我已经确认这不是测试的错误:在我的主项目中运行方法时,在空比较Console.WriteLine块中调用if,控制台上没有显示任何内容并且我添加的任何catch块都没有捕获异常。此外,使用string.IsNullOrEmpty代替== null会遇到同样的问题。

为什么这种简单的比较会失败?

3 个答案:

答案 0 :(得分:154)

您正在使用yield return。这样做时,编译器会将您的方法重写为一个返回生成状态机的生成类的函数。

从广义上讲,它将本地重写为该类的字段,并且yield return指令之间的算法的每个部分都成为一个状态。您可以使用反编译器检查编译后此方法的内容(确保关闭可生成yield return的智能反编译)。

但最重要的是:您的方法代码在您开始迭代之前不会被执行。

检查前提条件的常用方法是将方法拆分为两个:

public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
    if (str == null)
        throw new ArgumentNullException("str");
    if (searchText == null)
        throw new ArgumentNullException("searchText");

    return AllIndexesOfCore(str, searchText);
}

private static IEnumerable<int> AllIndexesOfCore(string str, string searchText)
{
    for (int index = 0; ; index += searchText.Length)
    {
        index = str.IndexOf(searchText, index);
        if (index == -1)
            break;
        yield return index;
    }
}

这是有效的,因为第一个方法的行为与您期望的一样(立即执行),并将返回第二个方法实现的状态机。

请注意,您还应该检查str的{​​{1}}参数,因为可以在null值上调用扩展方法 ,因为它们是&#39}只是语法糖。

如果您对编译器对代码的作用感到好奇,请使用 Show Compiler生成的代码选项,使用dotPeek进行反编译。

null

这是无效的C#代码,因为允许编译器执行语言不允许但在IL中合法的事情 - 例如以一种你无法避免名称的方式命名变量碰撞。

但正如您所看到的,public static IEnumerable<int> AllIndexesOf(this string str, string searchText) { Test.<AllIndexesOf>d__0 allIndexesOfD0 = new Test.<AllIndexesOf>d__0(-2); allIndexesOfD0.<>3__str = str; allIndexesOfD0.<>3__searchText = searchText; return (IEnumerable<int>) allIndexesOfD0; } [CompilerGenerated] private sealed class <AllIndexesOf>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable { private int <>2__current; private int <>1__state; private int <>l__initialThreadId; public string str; public string <>3__str; public string searchText; public string <>3__searchText; public int <index>5__1; int IEnumerator<int>.Current { [DebuggerHidden] get { return this.<>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return (object) this.<>2__current; } } [DebuggerHidden] public <AllIndexesOf>d__0(int <>1__state) { base..ctor(); this.<>1__state = param0; this.<>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] IEnumerator<int> IEnumerable<int>.GetEnumerator() { Test.<AllIndexesOf>d__0 allIndexesOfD0; if (Environment.CurrentManagedThreadId == this.<>l__initialThreadId && this.<>1__state == -2) { this.<>1__state = 0; allIndexesOfD0 = this; } else allIndexesOfD0 = new Test.<AllIndexesOf>d__0(0); allIndexesOfD0.str = this.<>3__str; allIndexesOfD0.searchText = this.<>3__searchText; return (IEnumerator<int>) allIndexesOfD0; } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return (IEnumerator) this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator(); } bool IEnumerator.MoveNext() { switch (this.<>1__state) { case 0: this.<>1__state = -1; if (this.searchText == null) throw new ArgumentNullException("searchText"); this.<index>5__1 = 0; break; case 1: this.<>1__state = -1; this.<index>5__1 += this.searchText.Length; break; default: return false; } this.<index>5__1 = this.str.IndexOf(this.searchText, this.<index>5__1); if (this.<index>5__1 != -1) { this.<>2__current = this.<index>5__1; this.<>1__state = 1; return true; } goto default; } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } void IDisposable.Dispose() { } } 仅构造并返回一个对象,其构造函数仅初始化某个状态。 AllIndexesOf仅复制对象。当你开始枚举(通过调用GetEnumerator方法)时,真正的工作就完成了。

答案 1 :(得分:34)

你有一个迭代器块。该方法中的所有代码都不会在返回的迭代器上对MoveNext的调用之外运行。调用该方法注意到但是创建状态机,并且不会失败(超出内存错误,堆栈溢出或线程中止异常等极端事件之外)。

当您实际尝试迭代序列时,您将获得例外。

这就是为什么LINQ方法实际上需要两个方法来获得他们想要的错误处理语义。它们有一个私有方法,它是一个迭代器块,然后是一个非迭代器块方法,除了执行参数验证之外什么都不做(因此它可以急切地完成,而不是延迟),同时仍然推迟所有其他功能。 / p>

所以这是一般模式:

public static IEnumerable<T> Foo<T>(
    this IEnumerable<T> souce, Func<T, bool> anotherArgument)
{
    //note, not an iterator block
    if(anotherArgument == null)
    {
        //TODO make a fuss
    }
    return FooImpl(source, anotherArgument);
}

private static IEnumerable<T> FooImpl<T>(
    IEnumerable<T> souce, Func<T, bool> anotherArgument)
{
    //TODO actual implementation as an iterator block
    yield break;
}

答案 2 :(得分:0)

正如其他人所说的那样,在他们开始枚举之前(即调用IEnumerable.GetNext方法),枚举器不会被评估。这样呢

List<int> indexes = "a.b.c.d.e".AllIndexesOf(null).ToList<int>();
在你开始枚举之前,

不会被评估,即

foreach(int index in indexes)
{
    // ArgumentNullException
}