文字列内の部分文字列のすべてのインデックスの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
を返しています。したがって、例外はスローされず、コードは続行されます。
これはテストのバグではないことを確認しました。null比較if
ブロックでConsole.WriteLine
を呼び出してメインプロジェクトでメソッドを実行すると、コンソールには何も表示されず、例外は、追加した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番目のメソッドによって実装されたステートマシンを返すためです。
拡張メソッドcanはstr
値で呼び出されるため、null
パラメーターのnull
もチェックする必要があることに注意してください。
コンパイラがコードに対して行う処理に興味がある場合は、Show Compiler-generated Codeオプションを使用して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
メソッドを呼び出すことにより)行われます。
イテレータブロックがあります。そのメソッドのコードは、返されたイテレーターで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;
}
列挙子は、他の人が言ったように、列挙され始めるまで評価されません(つまり、IEnumerable.GetNext
メソッドが呼び出されます)。したがって、これ
List<int> indexes = "a.b.c.d.e".AllIndexesOf(null).ToList<int>();
列挙を開始するまで評価されません。
foreach(int index in indexes)
{
// ArgumentNullException
}