rider.お品書きサクッと書いて公開して、後でちまちま修正する事がママあります。なんじゃこりゃ?と思ったら、数日してから見て貰えると嬉しいです。「スクラムの実践方法を分かりやすくマンガも交えて解説。いつもはAsyncとLINQの事をしゃべったり書いたりしています。IEnumerableの探索」というお題で書いてみようと思います。.NETでは、LINQにまつわるインターフェイスがかなり多く含まれています。その中でも、IEnumerableインターフェイスの継承グラフに存在する様々なインターフェイスを、どのように使い分けるべきか、と言うのが分かりにくいかも知れません。沢山のインターフェイスが定義されているのは、歴史的な事情もあります。Entitiesなど、一般的に使用されているLINQの背景には、「列挙可能である」という性質があります。この事は、以前にLINQの勉強会でも取り上げましたが、もっと分かりやすく掘り下げたいと思います。1.0LINQにおいて「列挙可能」であるとは、つまり「IEnumerable<T>」インターフェイスを実装している事、という事です。次のような簡単なLINQを考えてみます。0);.NETの配列は、System.Arrayクラスを暗黙に継承しています。このリファレンスを見ると、以下のようなインターフェイスを実装していることが分かります。IEnumerableこれを図示すると、こういう事です。IEnumerableインターフェイスは、「列挙可能」である事を示します。「示す」と言っても、ただ表明しているわけではありません。列挙可能にするための「GetEnumerator」メソッドを実装しています。ここではその詳細に踏み込みませんが、C#の「foreach」を使用する時、C#のコンパイラは暗黙に「GetEnumerator」メソッドを呼び出して、要素の列挙を実行します。そのため、「IEnumerable」を実装している=列挙可能、と見なせるわけです。配列に対して、直接foreachが使えます。(厳密には異なるのですが、このように解釈していて問題ありません)ICollectionインターフェイスを実装しているクラスは、要素数を把握可能である事を示します(他にも機能はあるのですが、省略)。要素数は、皆さんおなじみの「Count」プロパティで取得できます。ICollectionはIEnumerableを継承しています。そのため、ICollectionにキャスト出来た場合は、勿論「列挙が可能」と言う事になります。含めた、全ての操作が可能である事を示します。このインターフェイスは、ICollectionを継承しているので、要素数の把握も可能(つまり、Countプロパティが使える)で、更にIEnumerableも継承していることになるので、列挙も可能です。1.0、つまり最初から存在するインターフェイスです。気づいたかもしれませんが、これらのインターフェイスには、要素の型が含まれていません。IEnumerableインターフェイスで列挙しようとすると、System.Objectが要素の型となります。これでは列挙した後、適切な型へのキャストを行わなければなりません。実際、「foreach」を使用して列挙する場合、ループ変数の型を明示的に指定する事で、暗黙にキャストしていました。意味的には以下のようになります。...}ところで、配列の基底型であるSystem.Arrayが、IListインターフェイスを実装しているのはおかしいと思うかもしれません。配列の要素数は、一度生成されたら「不変」な筈です。そのため、ICollectionインターフェイスの実装までが妥当と思われますが、そうなっていません。実際、配列のインスタンスをIListインターフェイスにキャストし、Addメソッドを呼び出して要素を追加しようとしても、例外がスローされます。このような設計になっている理由は公式には明らかになっていないと思いますが、IListインターフェイスには「インデクサ」が定義されています。このインデクサを使用可能にするため、敢えてIListインターフェイスを実装しているのではないかと推測しています。(しかし、結局これは誤った設計だったと個人的には思っています。2.0上図では、Arrayを継承してintの配列を生成しています。Arrayは列挙可能であったり、要素数の把握が可能と言う事になっています。その要素の型はSystem.Objectであり、intへのキャストが必要である事は説明した通りです。配列を対象とした場合、IEnumerable<int>のような、要素型が明示されたインターフェイスでは使用出来ないという事でしょうか?そんな事はありません。2.0でジェネリック型がサポートされ、このように配列の実装インターフェイスが拡張されました。「Array<T>」クラスが導入されたわけではなく、配列を定義すると自動的に「IEnumerable<T>」インターフェイス・「ICollection<T>」インターフェイス・「IList<T>」インターフェイスが、直接配列の型に実装されます。「IEnumerable<T>」は「IEnumerable」も継承しているので、根本的な「列挙可能」という性質は受け継いでいると言えます。「ICollection<T>」は「ICollection」を継承しておらず、「IList<T>」も「IList」を継承していません。この理由についても推測するしかありませんが、「ICollection<T>」は設計上、ICollectionのジェネリックバージョンではないと考えられます。ICollectionには存在しない「Add」等の要素数の変化を伴う操作メソッドが追加されています。「IList」があまりに多くの役割を担い過ぎている事への反省でしょうか。これが原因で、ICollectionやIListの対比が分かりにくくなっています。IList<T>を実装するクラスを見た場合、暗黙的にIListも実装しているのではないか?と考えてしまいますが、そうなっていません(ここで示した配列や、List<T>クラスは、わざわざIList「も」実装する事で、この問題に対処しています)。それにしても、普段コードを書いていると、「ICollection<T>」ほど「触らない」インターフェイスも珍しいです。問題はやはり「インデクサ」が定義されていない事で、わざわざICollection<T>を操作する機会がほとんどありません。対照的に、「IList<T>」は良く使いました。3.5さて、このようなコレクション操作の抽象化を行う下積みがあり、とうとうLINQが登場します。LINQは、IList<T>のような高機能なインターフェイスではなく、List<T>クラスのような個々のコレクションクラスへの直接拡張でもなく、「IEnumerable<T>」インターフェイスだけを要求してきました。ここに「拡張メソッド」のサポートが加わり、「列挙可能な要素群」を包括的に操作可能となったのです。LINQが.NETやC#と切り離せない関係にあるのは、ここで改めて説明するまでも無いでしょう。IEnumerable<T>しか要求しないので、ここまでに解説したほとんどのインターフェイスは、実装さえしていれば即LINQで応用が可能と言う事になります。IEnumerable・ICollection・IListの「非ジェネリック」版インターフェイスについては、残念ながらLINQでそのまま使う事が出来ません。大きな問題は、列挙可能な要素の「型」が分からない事です。型を明示的に与えれば、LINQ演算子を適用する事が出来ます。0);IEnumerableは、列挙される要素の型が分からないため、必ず想定されるインスタンスが取得されるとは限りません。OfType演算子はそのような場合に使用することが出来ます。Cast演算子を使用しながら、異なる型が取得された場合は、例外がスローされます。Formsクラスのコレクションで、IEnumerableやIListだけが実装されているクラスがあります。それらのインスタンスに対しても、この演算子を使えばLINQで操作可能となります。3.5の世代では、一連のインターフェイスに新たに付け加えられたインターフェイスはありませんでした。4.5を待つ事になります。4.5がリリースされ、突如として「列挙可能」インターフェイスに新顔が追加されました。それが「IReadOnlyCollection<T>」インターフェイスと「IReadOnlyList<T>」インターフェイスです。名前から想像出来るように、これらのインターフェイスは、要素の読み取りのためのインターフェイスで、IReadOnlyCollection<T>は要素数「だけ」が取得可能、IReadOnlyList<T>は読み取り専用インデクサが定義されています。このインターフェイスを初見した時の率直な感想としては、「遅すぎた」でした。1.0世代で書きましたが、すでにこの時「読み取りしか行わないけど、インデクサが欲しい」という状態は露呈していました。1.0においても既に分かっていた事のように思います。理由として、「IsReadOnly」プロパティの存在が挙げられます。このプロパティを追加する際に、当然IReadOnlyCollectionやIReadOnlyListを検討する事も可能だった、と思うと残念なのです。残念な出自もさることながら、残念な現実もあります(だから余計に残念)。ICollection<T>やIList<T>インターフェイスの継承階層の適切な位置に、これらの新しいインターフェイスを潜り込ませる事は「出来ません」。そんな事をすれば、既存のICollection<T>やIList<T>等のインターフェイスを実装しているクラスが破綻してしまいます。インターフェイスは契約の一種です。後から変更されるとバイナリ互換性が失われます。図に示したように、新しいインターフェイスは配列の具象型が「直接」実装する事になります。これまでのインターフェイスとは全く融合せず、唯一IEnumerable<T>を継承する事で「型明示で列挙可能」である事は表明しています。同様に、List<T>クラスも、クラスが直接新しいインターフェイスを実装する事で、互換性を失わないようにしています。1.0の頃はまだJavaとの対比が強く、「マルチ言語」に強くこだわっていました。インデクサという機能は、様々な言語で再現出来ない可能性があり、C#やVB.net特有の機能とみなされており、IL上でも特別な扱いを受けていました。そのような機能を、mscorlib内のスタンダードなインターフェイスとして定義するのははばかられたのかも知れません。ぶっちゃけ、素でインデクサを表現できないなら、「Item」メソッドで良いじゃん?と思わなくもないんですがwインターフェイスバリエーションの物理的な側面IEnumerableにまつわるインターフェイスの歴史的な側面を挙げてきました。物理的な側面についても触れておきます。多くのインターフェイス定義の、どれを選択するのか?単に列挙するだけならIEnumerable<T>、リストの参照や操作を行う場合はIList<T>型を指定させ、意図の違いをコード上に表現します。};SumEvents(sourceDatas);基底となるインターフェイスで引数を定義する、と言うのは意図の明確化でもあり、応用性を高める点でも重要です。上記の例では、引数にはあらゆる「列挙可能」なインスタンスを指定する事が出来ます。仮に具象クラスや具象クラスに近いインターフェイスを指定した場合はどうなるでしょうか。内部では列挙しているだけなのだから、IList<T>である必要はない。IList<T>で渡していると、リストが書き換えられる可能性がある、ように見える。};SumEventsFromList(sourceDatas);大したことではない、と思うかもしれません。IEnumerable<T>のように、より制限された型を指定しておくと、そのような負担から解放され、少しでも楽になります。4.5以上であれば、IReadOnlyList<T>も使い分けの対象に入れると良いでしょう。このインターフェイスはまだまだ実装クラスが少ないですが、見ると安心感が違います(少なくともIList<T>を引数に渡す時には、書き換えの可能性が頭をよぎります)。共変性と読み取り専用インターフェイス読み取り専用として定義されたインターフェイスが残念な事になっているのは説明した通りです。このインターフェイスがわざわざ遅れて導入された理由と思われるものとして、要素型の「共変性」があります。C」が詳しいので参照してください。読み取り専用であれば(或は、読み取り専用として定義する事により)、インスタンスの代入互換性が高まるという事が趣旨にあります。1.0からありました。配列についてだけは、要素型の暗黙的なキャストが成立する場合において、代入互換があることになっていました。今までの話は、全てコンパイル時に型を特定する「型安全」についてフォーカスしたものです。LINQのパフォーマンスについての疑念の話をしましたが、実際にはより最適なアルゴリズムを採用するために、実行時のインスタンスの型を調査しています。LINQ:ElementAtのように、実行時に様々なインターフェイスにキャスト可能かどうかを判定して、より効率の高いアルゴリズムを採用する、という手法です。Countの例はIReadOnlyCollection<T>に対応していなかったので、書き直してみます。count;}残念なインターフェイスの通り、IReadOnlyCollection<T>とICollectionは、互いに全く関係がありません。そのため、キャストが成立するかどうかを別々に確かめる必要があります。あとがきちょっと、頭の中を整理したい事もあって、詳しく書き出してみたつもりだったのですが、要所でコード書いて確認してみないと自身が無い部分とかあり、良いリハビリになったなと思いました。Calendarは次週も担当です。́Д`)それではまた。次は「a_taihei」さんです。WordPress%d人のブロガーが「いいね」をつけました。