彼らが言うように、悪魔は細部に...
コレクションを列挙する2つの方法の最大の違いは、foreach
状態を保持するのに対し、状態を保持ForEach(x => { })
しないことです。
しかし、もう少し深く掘り下げてみましょう。これは、決定に影響を与える可能性があることを知っておくべきことがいくつかあり、どちらの場合もコーディングするときに注意すべきいくつかの注意事項があるためです。
使用することができますList<T>
動作を観察するために私たちの小さな実験で。この実験では、.NET 4.7.2を使用しています。
var names = new List<string>
{
"Henry",
"Shirley",
"Ann",
"Peter",
"Nancy"
};
foreach
最初にこれを繰り返しましょう:
foreach (var name in names)
{
Console.WriteLine(name);
}
これを次のように拡張できます。
using (var enumerator = names.GetEnumerator())
{
}
列挙子を手に入れ、カバーの下を見ると次のようになります。
public List<T>.Enumerator GetEnumerator()
{
return new List<T>.Enumerator(this);
}
internal Enumerator(List<T> list)
{
this.list = list;
this.index = 0;
this.version = list._version;
this.current = default (T);
}
public bool MoveNext()
{
List<T> list = this.list;
if (this.version != list._version || (uint) this.index >= (uint) list._size)
return this.MoveNextRare();
this.current = list._items[this.index];
++this.index;
return true;
}
object IEnumerator.Current
{
{
if (this.index == 0 || this.index == this.list._size + 1)
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
return (object) this.Current;
}
}
2つのことがすぐに明らかになります。
- 元になるコレクションの詳細な知識を持つステートフルオブジェクトが返されます。
- コレクションのコピーは浅いコピーです。
もちろん、これは決してスレッドセーフではありません。上記で指摘したように、反復しながらコレクションを変更することは、単に悪いモジョです。
しかし、反復の間にコレクションをいじくる外部の手段によって、反復の間にコレクションが無効になるという問題はどうですか?ベストプラクティスでは、操作中および反復中にコレクションのバージョン管理を行い、バージョンをチェックして、基になるコレクションが変更されたことを検出することをお勧めします。
ここで、事態は非常に曖昧になります。Microsoftのドキュメントによると:
要素の追加、変更、削除など、コレクションに変更が加えられた場合、列挙子の動作は未定義です。
さて、それはどういう意味ですか?例として、List<T>
例外処理を実装しているからといって、実装しているすべてのコレクションがIList<T>
同じように動作するわけではありません。これは、リスコフの代替原則に明らかに違反しているようです。
スーパークラスのオブジェクトは、アプリケーションを壊すことなく、そのサブクラスのオブジェクトと置き換えることができます。
もう1つの問題は、列挙子を実装する必要があることです。つまり、IDisposable
呼び出し元が間違っている場合だけでなく、作成者がDispose
パターンを正しく実装していない場合でも、メモリリークが発生する可能性があります。
最後に、寿命の問題があります...イテレータが有効であるが、基になるコレクションがなくなった場合はどうなりますか?現在のスナップショットだったものが、あなたが収集し、そのイテレータの寿命を分離する場合...、あなたはトラブルを求めています。
調べてみましょうForEach(x => { })
:
names.ForEach(name =>
{
});
これは次のように拡張されます。
public void ForEach(Action<T> action)
{
if (action == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
int version = this._version;
for (int index = 0; index < this._size && (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5); ++index)
action(this._items[index]);
if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
return;
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}
重要な注意点は次のとおりです。
for (int index = 0; index < this._size && ... ; ++index)
action(this._items[index]);
このコードは列挙子を割り当てず(には何もないDispose
)、反復中に一時停止しません。
これは、基になるコレクションの浅いコピーも実行しますが、コレクションは今のところスナップショットです。作成者がコレクションの変更をチェックしたり、「古くなったり」するチェックを正しく実装していなくても、スナップショットは引き続き有効です。
これは決して生涯問題の問題からあなたを保護しません...根底にあるコレクションが消えた場合、今は何があったかを指し示す浅いコピーを持っています...しかし、少なくともあなたはDispose
問題を抱えていません孤立したイテレータを扱います...
はい、私はイテレータを言いました...時々状態を持つことは有利です。データベースカーソルのようなものを維持したいとします。たぶん、複数のforeach
スタイルを使用Iterator<T>
する方法です。寿命の問題が多すぎるため、このスタイルのデザインは個人的に嫌いです。あなたは、自分が信頼しているコレクションの作者の優れた恵みに頼っています(自分ですべてを最初から自分で書く場合を除きます)。
常に3番目のオプションがあります...
for (var i = 0; i < names.Count; i++)
{
Console.WriteLine(names[i]);
}
セクシーではありませんが、歯が生えています(トムクルーズと映画「ザファーム」に謝罪)
それはあなたの選択ですが、今あなたは知っており、それは知識のあるものになることができます。