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

Art*_*ode 117 c# comparison ienumerable null 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这是标记问题的测试:

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

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

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

我已经确认这不是测试的错误:在我的主项目中通过调用Console.WriteLinenull比较if块运行该方法时,控制台上没有显示任何内容,并且catch我添加的任何块都没有捕获到任何异常.而且,使用string.IsNullOrEmpty而不是== null具有相同的问题.

为什么这个简单的比较失败了呢?

Luc*_*ski 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

请注意,您还应该检查str参数null,因为可以null值上调用扩展方法,因为它们只是语法糖.


如果您对编译器对代码的作用感到好奇,那么这是您的方法,使用Show Compiler生成的代码选项使用dotPeek进行反编译.

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()
  {
  }
}
Run Code Online (Sandbox Code Playgroud)

这是无效的C#代码,因为允许编译器执行语言不允许的操作,但这些操作在IL中是合法的 - 例如,以一种您无法避免名称冲突的方式命名变量.

但正如您所看到的,AllIndexesOf唯一的构造并返回一个对象,其构造函数只初始化某个状态.GetEnumerator只复制对象.当你开始枚举(通过调用MoveNext方法)时,真正的工作就完成了.

  • 顺便说一句,我在答案中添加了以下重要内容:*请注意,您还应该检查`null`的`str`参数,因为扩展方法可以在`null`值上调用,因为它们只是语法糖.* (9认同)
  • `yield return`原则上是一个好主意,但它有很多奇怪的陷阱.感谢您带来这一点! (2认同)

Ser*_*rvy 34

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

当您实际尝试迭代序列时,您将获得异常.

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

所以这是一般模式:

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;
}
Run Code Online (Sandbox Code Playgroud)