1. ホーム
  2. c#

[解決済み] この文字列拡張メソッドはなぜ例外を投げないのですか?

2022-09-16 13:04:55

質問

C# の文字列拡張メソッドで IEnumerable<int> を返す C# 文字列拡張メソッドがあります。それは意図された目的のために完全に動作し、期待される結果が返されます (以下のテストではなく、私のテストの 1 つによって証明されます) が、別のユニット テストはそれに関する問題を発見しました。

私がテストしている拡張メソッドは以下のとおりです。

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 も同じ問題があります。

この単純なはずの比較はなぜ失敗するのでしょうか?

どうすれば解決するのか?

あなたが使用している 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 のパラメータも確認する必要があります。 null というのは、エクステンションメソッド を呼び出すことができます。 null の値に対して呼び出すことができます。これは単なる構文上の糖分です。


コンパイラがあなたのコードに何をするのか興味があるのなら、あなたのメソッドを コンパイラで生成されたコードを表示する オプションを使用してドットピークでデコンパイルしたものです。

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 メソッドを呼び出すことで)列挙を開始するときに行われます。