IReadOnlyコレクションを操作するためのLINQの改善

ご存じのように、コレクションが暗示されるIEnumerable <>インターフェイスを使用すると、問題が発生する可能性があります(たとえば、 LSPに対する IEnumerableおよびLINQの 使用に関する問題を参照)。 幸いなことに、2012年の.NET v4.5(少し遅れましたが、決して遅れることはありません)、 IReadOnlyCollection <>IReadOnlyList <>IReadOnlyDictionary <>インターフェイスが登場しました(以降、これらを総称してIReadOnlyインターフェイスと呼びます)。 IEnumerable <>とは異なり、 IReadOnlyインターフェイスを使用すると、コレクション機能を不要な要件なしで非常に簡単に示すことができます。これにより、コレクションが読み取られる場所であれば、 IEnumerable <>の代わりに使用を推奨できます。 しかし、ここには1つの困難があります。 コレクションを使用および作成する重要なコンポーネントの1つはLINQであり、特にその一部である「オブジェクトへのLINQ」です。 残念ながら、 IReadOnly-インターフェースはLINQの5年後に登場し、使用されていません。 LINQ操作のすべての入力および出力コレクションには、基本型IEnumerable <>があります。機能が制限されているため、多くの操作には追加コストがかかります。完全な順次検索または入力コレクションの中間コピーの作成です。 さらに、LINQでは、操作から同じIEnumerable <>を返すため、結果をさらに使用する場合、再度徹底的な検索を使用して中間コピーを作成する必要があります。 この点で、私は長い間、 IReadOnlyインターフェイスを使用して「友達を作る」というLINQのアイデアを持っていました。



このトピックに関する広大なインターネット開発で見つかったものは私を満足させませんでした。 たとえば、 Linq to Collectionsライブラリは、 IReadOnlyList <>インターフェイスに対してのみ一部のLINQ操作のみの効果的な置換を提供し、最適化が不十分であり、何らかの理由でベースタイプに多くのあいまいな拡張メソッドを強制的に追加します。 このテーマの別のプロジェクトであるCountableSharpは、 IReadOnlyCollection <>インターフェイスに対してのみ少数の最適化されたLINQ操作を提供します。この場合、ベースSystem.Linq.Enumerableへのすべての呼び出しを委任するデコレーターが返され、 Countプロパティのみが事前に計算され、完全なコレクションを繰り返し処理します。



さらに、 Collection.LINQライブラリを使用することをお勧めします。これは、 IReadOnlyインターフェイスを実装するコレクションのほとんどの操作の効率的な実装を提供することにより、LINQをオブジェクトに最適化します。



まず第一に、私は自分自身の小さな余談を許可します。 おそらくご存じのように、 IReadOnlyシリーズのインターフェイスには明らかにセット用のインターフェイスがありません。 したがって、このような明白な形式で自分作成します



public interface IReadOnlyFiniteSet<T> : IReadOnlyCollection<T> { bool Contains (T item); }
      
      





ここで曖昧なのは、 IReadOnlyCollection <>の継承に関連付けられたセットの有限性だけです。 または、 IEnumerable <>から継承したIReadOnlySet <>の無限セットの中間インターフェイスを作成できます。 ただし、無限のセットは学術的な関心のみであり、どこで使用できるかを想像するのは難しいため、実用的な必要はありません。 基本クラスライブラリに存在するすべてのセットは、 IReadOnlyFiniteSet <>に必要なメソッドを既に実装しているため、実装に問題はありません。 次に、 IReadOnlyFiniteSet <>インターフェイスを含むLINQ操作の最適化について説明し、いつかはベースライブラリの一部となることを期待します。



そのため、 Collection.LINQで実装される操作と、それらが「オブジェクトへのLINQ」の効率をどの程度正確に向上させるかを検討します。



最適化が不要な操作を指定することから始めます。 集約メソッドAggregate()、Average()、Min()、Max()、Sum()は、 IEnumerable <>が必要で十分なインターフェイスであり、コレクションではなく値を返すため、最適化は必要ありません。 同じ理由で、オーバーロードメソッドAll()、Any()、Count()、LongCount()、First()、FirstOrDefault()、Last()、LastOrDefault()、Single()、SingleOrDefault()、最適化する必要はありません。述語フィルターパラメーターを取得します。 述語の存在は、 IEnumerable <>で十分な徹底的な検索の必要性を示します。 明らかに、ToArray()、ToList()、ToDictionary()、ToLookup()メソッドは、セマンティック上、完全な列挙とコピーの作成を意味するため、最適化も必要ありません。 ToArray()メソッドで配列を作成する場合のみ、コレクション内の要素の数を事前に知ることでわずかに最適化できます。



Empty()、Range()、およびRepeat()コレクションを作成するメソッドには、明らかな最適化が1つ必要です。これらは、ベースのIEnumerable <>ではなく、特定のIReadOnlyインターフェイスを返す必要があります。



次に、主な最適化について説明します。 コレクションを返す操作では、結果はデコレーターとして作成されます。デコレーターは、予備の反復や要素のコピーを作成することなく、メンバーへの呼び出しを入力コレクションに直接委任します。 複数の操作を順番に適用するときにこの最適化が機能するためには、返されるコレクションもIReadOnly型であることも重要です。 LINQ to Objectsには、同様の内部最適化が既に実装されています。多くのメソッドは、まず入力コレクションによって一部のインターフェイスの実装をチェックし、それらを使用してアクションをより効率的に実行します。 ただし、もちろん、 IReadOnlyインターフェイス(LINQが登場した時点では存在していなかった)は使用されず、返されるコレクションには常に基本型IEnumerable <>のみが含まれます。 私のライブラリでは、次のLINQ操作で直接装飾最適化が使用されています。
タイプIReadOnlyCollection <>のコレクションの場合
Any()、Count()、LongCount() Countプロパティを使用してすぐに結果を返します
DefaultIfEmpty() Countプロパティ、ソースまたは単一要素に応じて、IReadOnlyCollection <>を返します。
()、Zip()を選択します return IReadOnlyCollection <>-デコレータ
スキップ()、テイク() Countプロパティ、オリジナル、空、またはデコレータに応じて、IReadOnlyCollection <>を返します。
連結() ソースまたはデコレータのCountプロパティに応じて、IReadOnlyCollection <>を返します
リバース() IReadOnlyCollection <>を返します。Countプロパティ、列挙の要求時にコレクションの完全なコピーを作成するソースまたはデコレータに応じて
OrderBy()、OrderByDescending()、ThenBy()、ThenByDescending() IReadOnlyCollection <>を返します。Countプロパティ、列挙の要求時にコレクションの完全なコピーとソートされたインデックスを作成するソースまたはデコレーターに応じて
タイプIReadOnlyFiniteSet <>のセットIReadOnlyCollection <>コレクションで利用可能なものに加えて)
含む() Contains()メソッドを使用してすぐに結果を返します
個別() 元のIReadOnlyFiniteSet <>を返します-セット
()、Intersect()、Union()を除く IReadOnlyFiniteSet <>を返します。小さい方の入力セットで反復処理を作成する場合、 Countプロパティ、ソースプロパティの1つ、空またはデコレータに応じて
DefaultIfEmpty() Countプロパティ、ソースまたは単一要素に応じて、IReadOnlyFiniteSet <>を返します。
リバース() IReadOnlyFiniteSet <>を返します。Countプロパティ、セットの完全なコピーを作成するソースまたはデコレーターに応じて
タイプIReadOnlyList <>のリストの場合 (コレクションIReadOnlyCollection <>で使用できるものに加えて)
ElementAt()、ElementAtOrDefault()、First()、FirstOrDefault()、Last()、LastOrDefault() IReadOnlyList <>メソッドを使用して結果をすぐに返す
DefaultIfEmpty() Countプロパティ、ソースまたは単一要素に応じて、IReadOnlyList <>を返します。
スキップ()、テイク()、セレクト()、連結()、ジップ()、リバース() return IReadOnlyList <>-デコレータ
OrderBy()、OrderByDescending()、ThenBy()、ThenByDescending() IReadOnlyList <>を返します。Countプロパティに応じて、ソートされたインデックスを作成するソースまたはデコレーターは、位置番号と列挙によって要素の受信を委任します
残念ながら、一部のLINQ操作では、 IReadOnlyコレクションの機能によって効率を上げることはできません。 これらは、入力コレクション内の要素の値をフィルター処理することにより取得されるコレクションをもたらす操作です。 完全な検索とコピーなしに行う方法はありません。 フィルター操作SkipWhile()、TakeWhile()、Where()、および条件付きグループ化および結合Join()、GroupJoin()、GroupBy()、SelectMany()のメソッドは最適化されません。



さらに、 IReadOnlyインターフェイスに関連しないCollection.Linqのいくつかの最適化について説明します。

System.Linq.Enumerableライブラリよりも何も見逃さず、より効率的にしたことを確認するために、 ソースを常に確認しました。



Collection.LINQライブラリを使用するのは簡単です。コードを1行ずつ記述するだけで十分です。

 using System.Linq;
      
      





行を追加

 using BusinessClassLibrary.Collections.Linq;
      
      





クエリ構文の形式または流な形式でのLINQ to Objectsの使用方法は関係ありません。 ライブラリを接続した後、 IReadOnlyインターフェイスを実装するコレクションがinputである場合、LINQコードは自動的に最適なメソッドを使用します 。 唯一の例外は、拡張メソッドではないSystem.Linq.Enumerableクラスのコレクションを作成するためのメソッドです:Enumerable.Empty()、Enumerable.Range()、およびEnumerable.Repeat()。 これらのメソッドは、それぞれ手動でReadOnlyFiniteSet.Empty()/ ReadOnlyList.Empty()、ReadOnlyFiniteSet.Range()/ ReadOnlyList.Range()およびReadOnlyList.Repeat()に置き換える必要があります。



ライブラリ全体は、githubの公開プロジェクトで提示されます。 読者の要望に応じて、ライブラリを含むヌゲットパッケージも作成されました。



All Articles