BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ ニュース .NET 6 LINQの改良

.NET 6 LINQの改良

原文(投稿日:2021/04/29)へのリンク

100件を数える.NET 6のAPI変更を取り上げたシリーズの続きとして、今回はLINQライブラリの拡張に注目する。

IEnumerable<T>のインデクス操作

当初、IEnumerable<T>IList<T>を区別していた大きな特徴のひとつは、例えばコレクションの5番目というような、インデクスによる操作を後者がサポートしていたことだった。これは、高速な(O(1)またはそれに近い)インデクス操作をサポートするコレクションのみがIList<T>を実装する、という考え方だ。理屈の上からは、例え可能であったとしても速度の遅さが予想されるため、IEnumeratable<T>ではインデクス操作を行うべきでなかったのだが、

LINQの導入により、これら前提の多くが過去のものになった。Enumeratable.Count()Enumeretable.ElementAt()といった拡張メソッドにより、現実にはすべての要素をカウントしているとしても、任意のEnumerableなコレクションを、あたかもカウントや高速なインデクス操作が行えるように扱うことが可能になったのだ。

以下の新たな3つの拡張メソッドは、その傾向を継続するものだ。

public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, Index index);
public static TSource ElementAtOrDefault<TSource>(this IEnumerable<TSource> source, Index index);
public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, Range range);

Range型が新しいC#のrange構文を受け入れるようになった。例えば、

var elements = source2.Take(range: 10..^10)

IEnumerable<T>に対するカウント操作

IEnumerable<T>に対して.Count()をコールすると、2つのことが起きる。最初、LINQライブラリは、Countプロパティを公開するインターフェースへのキャストを試みる。それができない場合は、シーケンス全体を走査して要素数をカウントするのだが、

サイズの大きなコレクションでは、このカウントが大きな負荷を伴う場合がある。データベースクエリのようにIQueryableが関係している場合には、カウントの取得に数分、あるいはそれ以上を要する可能性もあるのだ。そこで開発者が求めたのが、"安全な"カウント関数だ。この関数は高速なCountプロパティをチェックして、それが見つからなければ何も返さない。この新しい関数は次のように定義されている。

public static bool TryGetNonEnumeratedCount(this IEnumerable<T> source, out int count);

TryGetNomEnumeratedCountという長い名称は開発者に対して、彼らが一種奇妙なことを行っている、ということを暗示するものだ。理想を言えば、リストを返すAPIは、厳密に名前付けられたコレクション(CustomerCollectionなど)、高パフォーマンスのList<T>IList<T>IreadOnlyList<T>など高レベルインターフェースのいずれかを返すべきである。これらの中のひとつが行われていれば、TryGetNonEnumeratedCountは必要ないはずだ。ただし、開発者が自身の呼び出しているAPIをコントロールできない場合もある。

Three-way Zip拡張メソッド

Zip拡張メソッドでは、2つのシーケンスを列挙することで、それらを結合することが可能になった。例えば、リスト1,2,3とリストA,B,Cがある場合、結果として得られるシーケンスは(1,A), (2,B), (3,C)になる。

プロポーザル "More arities for tuple returning zip extension method"の提案に従えば、3つのシーケンスを直接組み合わせることも可能になる。

public static IEnumerable<(TFirst First, TSecond Second, TThird Third)> Zip<TFirst, TSecond, TThird>(this IEnumerable<TFirst> first, IEnumerable<TSecond> second, IEnumerable<TThird> third);
public static IQueryable<(TFirst First, TSecond Second, TThird Third)> Zip<TFirst, TSecond, TThird>(this IQueryable<TFirst> source1, IEnumerable<TSecond> source2, IEnumerable<TThird> source3);

厳密に言えば、単にZip(...)を複数回適用すればよいのだから、このようなZipの新バージョンは必要ないはずだが、レビュアーたちはこの3値シーケンス版について、ライブラリへの追加を正当化できる使用頻度がある、と判断したようだ。その他の4つや5つといったアリティ(arity)については、一般的でないという理由で拒否された。

注記: アリティ(arity、複数形arities)とは、関数の取得する引数ないしオペランドの数を意味する。Zip関数拡張メソッドでは、アリティは3ということになる。

バッチシーケンス

シーケンスを個別のバッチやチャンクに分割しなければならない場合は少なくない。例えば、一度に100行ずつデータベースに送信する処理を行うことが、1行ずつ送信したり、あるいは全部を一括送信するよりもパフォーマンス的に優れている場合がある。

このコードを記述する上で特に難しい部分はないのだが、エラーの多い傾向がある。アイテム数がバッチサイズで割り切れない場合に、最後のバッチでミスを犯しやすいのだ。その便宜を図るためにChunk拡張メソッドが追加された。

public static IEnumerable<T[]> Chunk(this IEnumerable<T> source, int size);
public static IQueryable<T[]> Chunk(this IQueryable<T> source, int size);

.NET 6を待ちたくなければ、オープンソースライブラリのMoreLINQには、この操作がBatchという名称で含まれている。

注目すべきは、この機能と前述のZip拡張メソッドについて、いずれもIEnumerableIQueryableの2バージョンが存在することだ。IEnumerableメソッドを返す新APIはすべて、対応するIQueryableバージョンを含む必要がある。これによって、意図しないままクエリを通常のシーケンスに変更することを防止しているのだ。

アナライザチェック

APIそれ自体がコードの不適切な使用を防止できない場合、ライブラリの開発者はアナライザへの依存度を高めることになる。アナライザにはC#コンパイラに組み込まれたものと、NetAnalyzers(以前のFXCop)やサードパーティ製のRoslynatorのようにライブラリ経由で追加されるものがある。

最初の新しいアナライザはOfType<T>拡張メソッドを扱うものだ。これは入力シーケンスをフィルタして、型<T>で指示された項目だけを返す。入力の型が出力の型にキャストできない場合、現状の動作では単に空シーケンスを返しているが、"do not use OfType<T>() with impossible types"プロポーザルが採用されれば、コンパイラ警告が出力されるようになる。

"use AsParallel() correctly"プロポーザルは、AsParallelをコールした直後にシーケンスの列挙を開始した場合に対処するものだ。APIからは明確ではないが、AsParallel()は、マッピングやフィルタリングといった並列化可能なオペレーションより前になければならない。このミスに対しても、同じくコンパイラ警告が報告されるようになる。

*By演算子

*By演算子とは、DistinctByExceptByIntersectByUnionByMinByMaxByの総称である。最初の4つでは、keySelectorを指定することにより、操作の比較に関する部分を、値全体ではなく、値の一部を使って行うことが可能になる。これはパフォーマンスの改善や、元の値を失うことなく独自の動作を追加するために使用することができる。オプションとしてcomparerを指定することも可能である。

MinByMaxByでは、keySelectorの代わりにselectorを使用する。こちらについても、オプションでcomparerを指定することが可能だ。(完全を期すために述べておくと、Min演算子とMax演算子でもオプションとしてcomparerを指定できるようになった。)

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector);
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey>?comparer);

public static IEnumerable<TSource> ExceptBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TKey> keySelector);
public static IEnumerable<TSource> ExceptBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey>?comparer);
public static IEnumerable<TSource> ExceptBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelectorFirst);
public static IEnumerable<TSource> ExceptBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelectorFirst, IEqualityComparer<TKey>?comparer);

public static IEnumerable<TSource> IntersectBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TKey> keySelector);
public static IEnumerable<TSource> IntersectBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey>?comparer);
public static IEnumerable<TSource> IntersectBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelectorFirst);
public static IEnumerable<TSource> IntersectBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelectorFirst, IEqualityComparer<TKey>?comparer);

public static IEnumerable<TSource> UnionBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TKey> keySelector);
public static IEnumerable<TSource> UnionBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey>?comparer);

public static TSource MinBy<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector);
public static TSource MinBy<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector, IComparer<TResult>?comparer);

public static TSource MaxBy<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector);
public static TSource MaxBy<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector, IComparer<TResult>?comparer);

public static TResult Min<TSource, TResult>(this IEnumerable<TSource> source, IComparer<TResult>?comparer);

public static TResult Max<TSource, TResult>(this IEnumerable<TSource> source, IComparer<TResult>?comparer);

それぞれのIEnumerableメソッドには、対応するIQueryableメソッドが同じシグネチャで存在する。

*OrDefaultの拡張

*OrDefault演算子のバリアントは、SingleFirstLastの各演算子に空の列挙が与えられた場合のデフォルト値を指定するために使用される。In this feature, the default value returned can now be overridden.

public static TSource SingleOrDefault<TSource>(this IEnumerable<TSource> source, TSource defaultValue);
public static TSource SingleOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, TSource defaultValue);

public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, TSource defaultValue);
public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, TSource defaultValue);

public static TSource LastOrDefault<TSource>(this IEnumerable<TSource> source, TSource defaultValue);
public static TSource LastOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, TSource defaultValue);

こちらのIEnumerableメソッドにも、対応するIQueryableメソッドが同じシグネチャで存在する。

シリーズの過去の記事については、以下のリンクを参照して頂きたい。

この記事に星をつける

おすすめ度
スタイル

BT