ダックタイピングまたは「古代はとても単純なforeachですか?」

多くの開発者は、C#のforeachループが一見すると単純ではないことを知っていると思います。 まず、「 foreachコンストラクトを正常にコンパイルするには何が必要ですか?」という質問に答えましょう。 この質問に対する直感的な答えは、「クラスはIEnumerableまたはIEnumerable < T >を実装しています。」インターフェイスのようなもののようです。 しかし、これはそうではない、まあ、そうではない。



この質問に対する完全な答えは次のとおりです。「 foreachコンストラクトを正常にコンパイルするには、オブジェクトにGetEnumerator ()メソッドが必要です。このメソッドは、 MoveNext ()メソッドとCurrentプロパティでオブジェクトを返します。およびIEnumerable < T > "。



この「アヒル」動作には2つの理由があります。





言語がシンプルで簡単で、ジェネリック、LINQ、またはその他のクロージャーが含まれていなかったC#1.0の昔を思い出しましょう。 しかし、ジェネリックがないため、「ジェネラライゼーション」と再利用はポリモーフィズムとオブジェクトタイプに基づいており、実際にはコレクションとその反復子のクラスで行われました。



これらの同じイテレータはIEnumerableIEnumeratorのペアであり、後者はCurrentプロパティでオブジェクトを返しました。 その場合、 IEnumeratorインターフェイスを使用して、重要な型の強く型付けされたコレクションの要素を反復処理すると、各反復でこの値をパックおよびアンパックすることになります。これは、要素の列挙などの一般的な操作に関しては非常にコストがかかる可能性があります。



この問題を解決するために、アヒルのタイピングを使用したハックを使用し、パフォーマンスのためにOOPの原理を少し採点することにしました。 この場合、クラスはIEnumerableインターフェイスを明示的に実装し、追加のGetEnumerator ()メソッドを提供して、 Currentプロパティが特定の型(パッケージ化なしのDateTimeなどを返す厳密に型指定された列挙子を返します。



わかった 恐竜を見つけましたが、現実の世界はどうですか? 確かに、まだ石器時代ではない中庭で、COMはすでにオークを与えており、ドンボックスはもはや本を書いていません。 現在、この動作の利点はありますか?



IEnumerable < T >およびIEnumerator < T >インターフェースの一般化バージョンが登場した後、アヒルのタイピングトリックは不要になったと思うかもしれませんが、これは完全に真実ではありません。 List < T >などのコレクションクラスをよく見ると、このクラスが(BCLの他のすべてのコレクションと同様に) IEnumerable < T >インターフェイスを明示的に(明示的に)実装し、追加のGetEnumerator ()メソッドを提供していることがわかります



// ! public class List<T> : IEnumerable<T> { //   List<T> //  ,  !!! public struct Enumerator : IEnumerator<T>, IDisposable { } public List<T>.Enumerator GetEnumerator() { return new Enumerator(this); } //    IEnumerator<T> IEnumerator<T>.GetEnumerator() { return GetEnumerator(); } }
      
      







はい、そうです。 GetEnumerator ()メソッドは、可変構造であるイテレータインスタンスを返します(結局、イテレータには現在のリストアイテムへの「ポインタ」が含まれます)。 また、多くの人によると、可変の重要なタイプは、.NETプラットフォームで最も鋭いものであり、非常に経験豊富な開発者でさえ足を引っ張ることができます。





はい、はい。 私はすでに一般的な変数列挙子特に変数有効型で耳をすませていることを知っていますが、ここでは、この動作の理由などの説明を見つけようとします。 それでもう少し耐えてください:)



問題は、構造を反復子として使用し、 foreachループの 「アヒル」の性質と一緒に使用すると、この構造を使用するときにヒープ上のメモリ割り当てが妨げられることです。



 var list = new List<int> {1, 2, 3}; //  List<T>.Enumerator GetEnumerator foreach(var i in list) {} //  IEnumerable<T> GetEnumerator foreach(var i in (IEnumerable<int>)list) {}
      
      







最初の例では、「アヒル」の性質により、 ListクラスのGetEnumerator ()メソッドが呼び出され、マネージヒープに追加のメモリ割り当てがなくても、スタック上で静かに生きる重要なタイプのオブジェクトを返します。 2番目のケースでは、変数リストをインターフェイスにキャストします。これにより、インターフェイスメソッドが呼び出され、それに応じてイテレータがパッケージ化されます。 はい。C#開発者は、効率を向上させるために、ポリモーフィズムと他の多くのOOP原則を導入しています。



var list = new List {1、2、3};



 var x1 = new { Items = ((IEnumerable<int>)list).GetEnumerator() }; while (x1.Items.MoveNext()) { Console.WriteLine(x1.Items.Current); } Console.ReadLine(); var x2 = new { Items = list.GetEnumerator() }; while (x2.Items.MoveNext()) { Console.WriteLine(x2.Items.Current); }
      
      







このため、最初のwhileループは期待される1、2、3、2番目のwhileループを出力します ...さて、自分でチェックしてください。



このようなソリューション(可変構造を使用)は、マイクロ最適化を超えているように見えますが、 foreachループをネストでき、ギガバイトのメモリを備えたマルチコアプロセッサで全員が動作するわけではないことを忘れないでください。 この決定を下す前に、BCLチームは、構造を使用することが本当に価値があることを示すいくつかの真剣な調査を行いました。





独自のイテレータまたは他の補助クラスを実装するときに、この例をすぐに使用しないでください。 構造を使用すること自体が最適化されますが、可変構造を使用することは重大な決定であるため、セキュリティを犠牲にすることで得られる利点について明確にする必要があります。



小さな追加: Dispose 呼び出しの目的は何 ですか?



foreachループ実装のもう1つの機能は、イテレータのDisposeメソッドを呼び出すことです。 以下は、 foreachループリスト変数を反復処理するときにコンパイラーによって生成されるコードの簡易バージョンです。



 { var enumerator = list.GetEnumerator(); try { while(enumerator.MoveNext()) { int current = enumerator.Current; Console.WriteLine(current); } } finally { enumerator.Dispose(); } }
      
      







反復子で管理対象リソースがどこから来るのかについて、合理的な疑問が生じる場合がありますか? ええ、はい、メモリ内のコレクションを並べ替える場合、実際にはどこから来るのかはわかりませんが、C#の列挙子はメモリ内のコレクションのイテレータとしてだけでなく使用できることを忘れないでください。 誰もファイルの内容を1行ずつ返すイテレータを気にしません。



 public static class FileEx { public static IEnumerable<string> ReadByLine(string path) { if (path == null) throw new ArgumentNullException("path"); return ReadByLineImpl(path); } private static IEnumerable<string> ReadByLineImpl(string path) { using (var sr = new StreamReader(path)) { string s; while ((s = sr.ReadLine()) != null) yield return s; } } } foreach(var line in FileEx.ReadByLine("D:\\1.txt")) { Console.WriteLine(line); }
      
      







そのため、ファイルを開くReadByLineメソッドがありますが、これは確かにリソースであり、いつ閉じるのですか? 明らかに、 ReadByLineImplメソッドがcontrolを離れるたびに毎回ではありません。それは、このファイルにある行と同じ回数だけ閉じるためです。

実際、ファイルは一度閉じられます。これは、イテレーターのDisposeメソッドを呼び出すときと同じです。これはforeachループの finallyブロックで発生します 。 これは、 finallyブロックが自動的に呼び出されず、ハンドルによってのみ呼び出される場合の.NETプラットフォームでのまれなケースの1つです。 そのため、何らかのシーケンスを手動で突然反復する場合、イテレーターにリソースを含めることができることを忘れてはなりません。イテレーターのDisposeメソッドを明示的に呼び出してそれらをクリアすることは非常に良いことです。





C#のイテレータについては、注... C#のイテレータを参照してください



Z.Y. そして、誰がすぐにこの質問に答えることができます:なぜ2つのメソッドReadByLineReadByLineImplが必要なのですか、なぜ1つのメソッドのみを使用しないのですか?



Z.Y.Y. ちなみに、 foreachブロックはC#でのダックタイピングの唯一の例からはほど遠いですが、さらに多くの例を思い出せますか?



All Articles