この文字列拡張メソッドが例外をスローしないのはなぜですか?


119

IEnumerable<int>文字列内の部分文字列のすべてのインデックスを返すC#文字列拡張メソッドがあります。それは意図した目的に完全に機能し、期待される結果が返されます(以下のテストではなく、私のテストの1つで証明されています)が、別の単体テストで問題が発見されました:null引数を処理できません。

これが私がテストしている拡張メソッドです:

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.WriteLinenull比較ifブロックの呼び出しを使用してメソッドを実行すると、コンソールに何も表示されず、catch追加したブロックで例外がキャッチされません。さらに、string.IsNullOrEmpty代わりにを使用し== nullても同じ問題があります。

なぜこの単純な比較が失敗するのですか?


5
コードをステップ実行してみましたか?それはおそらくそれをかなり迅速に解決するでしょう。
マシューハウゲン

1
起こりますか?(例外をスローしますか?スローする場合、どの行とどの行ですか?)
user2864740

@ user2864740私は起こるすべてを説明しました。例外はなく、失敗したテストと実行メソッドのみです。
ArtOfCode

7
イテレータは反復されるまで実行されません
BlueRaja-Danny Pflughoeft

2
どういたしまして。これはJonの「最悪のこと」リストにもなりました:stackoverflow.com/a/241180/88656。これは非常に一般的な問題です。
Eric Lippert、2015年

回答:


158

を使用していyield returnます。その場合、コンパイラーはメソッドを、ステートマシンを実装する生成されたクラスを返す関数に書き換えます。

大まかに言えば、ローカルをそのクラスのフィールドに書き換え、yield return命令間のアルゴリズムの各部分が状態になります。コンパイル後にこのメソッドがどうなるかを逆コンパイラで確認できます(生成されるスマート逆コンパイルを必ずオフにしてくださいyield return)。

しかし、肝心なのは、反復を開始するまで、メソッドのコードは実行されないということです。

前提条件を確認する通常の方法は、メソッドを2つに分割することです。

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

これが機能するのは、最初のメソッドが期待どおりに動作し(即時実行)、2番目のメソッドによって実装された状態マシンが返されるためです。

拡張メソッド構文上の砂糖であるため、値に対して呼び出すことできるため、のstrパラメーターも確認する必要があります。nullnull


コンパイラがコードに対して行うことについて知りたい場合は、ここにメソッドを示します。コンパイラで生成されたコードの表示オプションを使用して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パラメーターも確認する必要があることに注意してください。nullnull
Lucas Trzesniewski、2015年

2
yield return原理的にはいいアイデアですが、非常に多くの奇妙な問題があります。これを公開してくれてありがとう!
nateirvin

つまり、foreachのように、列挙子が実行された場合、基本的にエラーがスローされますか?
MVCDS

1
@MVCDSその通りです。構造体MoveNextによって内部で呼び出されforeachます。正確なパターンを確認したい場合foreach、コレクションのセマンティクスを説明する私の回答で何が行われるかについての説明を書きました。
Lucas Trzesniewski、2015年

34

イテレータブロックがあります。そのメソッドのコードはMoveNext、返されたイテレータの呼び出し以外で実行されることはありません。メソッドを呼び出すと、ステートマシンは作成されますが、作成は失敗しません(メモリ不足エラー、スタックオーバーフロー、スレッドアボート例外などの極端な状況以外では)。

実際にシーケンスを反復しようとすると、例外が発生します。

これが、LINQメソッドが実際に必要なエラー処理セマンティクスを持つために2つのメソッドを必要とする理由です。それらには、イテレーターブロックであるプライベートメソッドがあり、その後、他のすべての機能を据え置きながら、引数の検証のみを行う非イテレーターブロックメソッドがあります。

したがって、これは一般的なパターンです。

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
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.