はじめに
この記事は祝 .NET 6 GA!.NET 6 での開発 Tips や試してみたことなど、あなたの「いち推し」ポイントを教えてください【PR】日本マイクロソフト Advent Calendar 2021の20日目の記事になります。
概要
.NET6.0でSystem.Linqに対してなされた改善や新しく実装されたoverloadあたりについて紹介する記事になります。
LINQは、任意のシーケンスに対して検索、フィルタリング、部分取得など様々な処理をメソッドチェーンやクエリ式を使って表現できるC#の言語機能です。
基本的にはEnumerable
クラスにIEnumerable<T>
に対する拡張メソッドとして実装されています。
本編
Index/Rangeによるアクセスをサポート
C#8.0から追加されたIndex/Range型を引数にElementAtやTakeなどのメソッドが動作するようになりました。
これで下記のコードのように、末尾からのアクセスや、一定範囲の取得がより簡単に記述できるようになっています。
※Index/Rangeについてはこちらの記事が参考になります
var source = Enumerable.Range(1, 10);
var nine = source.ElementAt(^2);
// RangeのサポートによりTakeでできることが広がった
// source.Skip(3)のかわりに
var range2 = source.Take(3..);
// source.Take(7).Skip(2)のかわりに
var range3 = source.Take(2..7);
// source.TakeLast(3)のかわりに
var range4 = source.Take(^3..);
// source.SkipLast(3)のかわりに
var range5 = source.Take(..^3);
// source.TakeLast(7).SkipLast(3);のかわりに
var range6 = source.Take(^7..^3);
enumerate処理を行わずシーケンスのサイズが取得可能に
TryGetNonEnumeratedCount
メソッドが実装され、内部的にenumerate処理を伴わずシーケンスのサイズが取得可能な場合のみ値を取得できるようになりました。
実装としては特定のインタフェースを実装している場合のみ低コストでサイズが取得可能なため、その場合のみ値を返すような実装になっています。
下記のようなコードの場合、10というサイズが取得可能です。
var source = Enumerable.Range(1, 10);
List<int> buffer = source.TryGetNonEnumeratedCount(out int count) ? new List<int>(count) : new List<int>();
しかし、下記のコードのようにWhere
などを挟むと、enumerate処理を行わない限り要素数が確定できないためTryGetNonEnumeratedCount
はfalseを返します。
// この場合はenumerate処理を行わないと判定できないのでfalseになる
var query = source.Where(i => i % 2 == 0);
if (query.TryGetNonEnumeratedCount(out int _))
{
Console.WriteLine($"{nameof(query)} can get enumeratedCount");
}
else
{
Console.WriteLine($"{nameof(query)} can't get enumeratedCount");
}
DistinctBy/UnionBy/IntersectBy/ExceptByの追加
引数にキーセレクターを渡して処理を挟めるようなAPIが新しく追加されました。
var distinct = Enumerable.Range(1, 20).DistinctBy(i => i % 2 == 0);
MinBy/MaxByも追加
同様にMin/Maxに対してもキーセレクターを渡せるAPIが新しく追加されました。
var people = new (string Name, int Age)[] { ("Hoge", 10), ("Huga", 20) };
var max = people.MaxBy(person => person.Age);
Chunkの追加
シーケンスを一定間隔ごとに切り出して新しいシーケンスとして返すChunk
メソッドが追加されました。
IEnumerable<int[]> chunks = Enumerable.Range(0, 10).Chunk(3);
foreach (var chunk in chunks)
{
Console.WriteLine("Chunk");
foreach (var i in chunk)
{
Console.WriteLine(i);
}
}
実行すると下記の結果が出力されます。
Chunk
0
1
2
Chunk
3
4
5
Chunk
6
7
8
Chunk
9
FirstOrDefault/LastOrDefault/SingleOrDefaultにデフォルトパラメータを渡せるオーバーロードの追加
デフォルト値を引数で指定できるようになりました。
var defaultValue = Enumerable.Empty<int>().SingleOrDefault<int>(-1);
Console.WriteLine($"defaultValue: {defaultValue}");
ただし、注意点としてデフォルト値は引数に渡す必要があるため、下記のようなコードの場合、従来の方法と比べて不要なGC.Allocの原因となってしまいます。
// FirstOrDefaultの結果がnullの場合のみ、Personクラスの生成処理が実行される
var hoge = people.FirstOrDefault() ?? new Person();
// 関数呼び出しの時点でPersonクラスの生成が行われてしまうため、不要なGC.Allocが発生する
var huga = people.FirstOrDefault(new Person());
Zipに3つシーケンスを渡せるようになった
Zipが3つIEnumerableを受け取れるようになりました。
var xs = Enumerable.Range(1, 10);
var ys = xs.Select(x => x.ToString());
var zs = xs.Select(x => x % 2 == 0);
foreach ((int x, string y, bool z) in Enumerable.Zip(xs, ys, zs))
{
Console.WriteLine($"x: {x}, y: {y}, z: {z}");
}
さいごに
LINQに対するパフォーマンス改善や新しいオーバーロード、APIの追加が行われたことで、LINQがさらに強力な言語機能となりました。
コードの表現の幅が広がったので、使いどころを選びつつぜひ使っていきたいです。