为什么此字符串扩展方法不引发异常?


119

我有一个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在null比较if块中调用时,控制台上未显示任何内容,并且catch添加的任何块均未捕获任何异常。此外,使用string.IsNullOrEmpty代替== null具有相同的问题。

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


5
您是否尝试单步执行代码?那可能会很快解决它。
马修·豪根

1
什么没有发生?(它抛出一个异常;如果是这样,哪一个和什么线?)
user2864740

@ user2864740我已经描述了所有发生的事情。没有例外,只有失败的测试和运行方法。
ArtOfCode 2015年

7
迭代器要经过迭代才可以执行
BlueRaja-Danny Pflughoeft 2015年

2
别客气。这也是乔恩的“最糟糕的陷阱”列表:stackoverflow.com/a/241180/88656。这是一个非常普遍的问题。
埃里克·利珀特

Answers:


158

您正在使用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参数null,因为扩展方法可以null值上调用,因为它们只是语法糖。


如果您对编译器对代码的处理方式感到好奇,请使用dotPeek使用“ 显示编译器生成的代码”选项对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()
  {
  }
}

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

但是如您所见,AllIndexesOf唯一的构造和返回对象,其构造函数仅初始化某些状态。GetEnumerator仅复制对象。当您开始枚举时(通过调用MoveNext方法),实际的工作就完成了。


9
顺便说一句,我在答案中添加了以下要点:请注意,您还应该检查的str参数null,因为扩展方法可以在null值上调用,因为它们只是语法糖。
卢卡斯Trzesniewski

2
yield return从原理上讲,这是一个不错的主意,但它有很多怪异的陷阱。感谢您揭露这一点!
nateirvin

那么,如果运行枚举器,基本上会抛出错误,就像在foreach中一样?
MVCDS

1
@MVCDS确实如此。MoveNextforeach构造在幕后调用。如果您希望看到确切的模式foreach我就回答我的答案做了什么解释,解释集合语义
Lucas Trzesniewski

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

0

正如其他人所说,枚举器直到开始枚举(即IEnumerable.GetNext调用该方法)时才进行评估。因此这

List<int> indexes = "a.b.c.d.e".AllIndexesOf(null).ToList<int>();

在开始枚举之前不会得到评估,即

foreach(int index in indexes)
{
    // ArgumentNullException
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.