【C#】LINQでスマートにデータ抽出!Where句と拡張メソッドで実現する多彩なフィルタリング術
LINQのフィルタリングは、Where
などの拡張メソッドやSQL風クエリ構文で条件抽出し、遅延実行により不要な計算を避けながらデータを効率的に扱える手法です。
ラムダ式で複数条件も簡潔に書け、コレクション操作を読みやすく統一できます。
LINQフィルタリングの基礎
LINQ(Language Integrated Query)は、C#でデータの抽出や操作を簡潔に記述できる機能です。
特にフィルタリングにおいては、Where
拡張メソッドやクエリ式のwhere
句を使うことで、条件に合致する要素だけを効率よく抽出できます。
ここでは、LINQのフィルタリングの基本的な仕組みや特徴について詳しく解説いたします。
Where拡張メソッドの仕組み
Where
メソッドは、LINQの中でも最もよく使われる拡張メソッドの一つです。
これは、IEnumerable<T>
型のコレクションに対して呼び出すことができ、指定した条件に合致する要素だけを抽出して新しいシーケンスを返します。
Where
メソッドのシグネチャは以下のようになっています。
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate
);
source
はフィルタリング対象のコレクションですpredicate
は各要素に対して真偽値を返す関数(ラムダ式など)で、true
を返した要素だけが結果に含まれます
例えば、整数の配列から偶数だけを抽出する場合は以下のように記述します。
int[] numbers = { 1, 2, 3, 4, 5, 6 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
foreach (var num in evenNumbers)
{
Console.WriteLine(num);
}
このコードでは、n => n % 2 == 0
というラムダ式がpredicate
として渡され、偶数の要素だけがevenNumbers
に含まれます。
Where
メソッドは元のコレクションを変更せず、新しいIEnumerable<T>
を返すため、元のデータは安全に保たれます。
また、Where
は遅延実行を行うため、実際に要素を列挙するまで処理は実行されません。
クエリ式のwhere句との違い
LINQには、メソッドチェーンで記述する拡張メソッドスタイルと、SQLに似たクエリ式スタイルの2種類の書き方があります。
Where
メソッドは拡張メソッドスタイルでのフィルタリングを行いますが、クエリ式ではwhere
句を使って同様の処理が可能です。
例えば、先ほどの偶数抽出をクエリ式で書くと以下のようになります。
int[] numbers = { 1, 2, 3, 4, 5, 6 };
var evenNumbers = from n in numbers
where n % 2 == 0
select n;
foreach (var num in evenNumbers)
{
Console.WriteLine(num);
}
クエリ式はfrom
でデータソースを指定し、where
句で条件を記述、select
で結果の形を指定します。
拡張メソッドスタイルと比べてSQLに近い構文なので、SQLに慣れている方には直感的に理解しやすいです。
ただし、クエリ式は内部的には拡張メソッドに変換されるため、パフォーマンスや動作に大きな違いはありません。
好みや可読性に応じて使い分けるとよいでしょう。
遅延実行とイテレーションのタイミング
LINQのWhere
メソッドは遅延実行(Lazy Evaluation)を採用しています。
これは、Where
を呼び出した時点ではまだ実際のフィルタリング処理は行われず、結果のシーケンスが列挙される(foreach
などで要素を取り出す)タイミングで初めて処理が実行されることを意味します。
この遅延実行のメリットは以下の通りです。
- パフォーマンスの向上
不要な処理を避け、必要な要素だけを効率的に取得できます。
例えば、Where
で絞り込んだ後にTake(5)
で最初の5件だけ取得する場合、全件を処理せずに済みます。
- メモリ効率の改善
フィルタリング結果をすぐにリスト化しないため、大量データでもメモリ消費を抑えられます。
- 柔軟なクエリ構築
複数のLINQメソッドを連結しても、最終的に列挙されるまで処理は実行されません。
これにより、動的に条件を追加したり変更したりすることが容易です。
ただし、遅延実行には注意点もあります。
例えば、元のコレクションが列挙されるまでに変更されると、結果に影響が出ることがあります。
また、例外が発生するのも列挙時なので、エラーハンドリングのタイミングが変わることを理解しておく必要があります。
以下のサンプルコードで遅延実行の動作を確認してみましょう。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// Whereメソッドで偶数を抽出(まだ実行されていない)
var evenNumbers = numbers.Where(n => n % 2 == 0);
// 元のリストに要素を追加
numbers.Add(6);
// 列挙時にフィルタリング処理が実行される
foreach (var num in evenNumbers)
{
Console.WriteLine(num);
}
}
}
2
4
6
この例では、Where
の呼び出し時点ではまだ処理は実行されていません。
numbers
に6
を追加した後にevenNumbers
を列挙すると、追加した6
も含めて偶数が抽出されていることがわかります。
このように、LINQのWhere
メソッドは遅延実行の特性を持つため、処理のタイミングや元データの変更に注意しながら使うことが重要です。
必要に応じてToList()
やToArray()
で即時実行(強制評価)することも検討してください。
単純条件での抽出
数値の偶奇判定
数値のコレクションから偶数や奇数を抽出するのは、LINQの基本的な使い方の一つです。
Where
メソッドに条件式を渡すだけで簡単に実現できます。
以下のサンプルコードでは、整数の配列から偶数だけを抽出しています。
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 偶数を抽出
var evenNumbers = numbers.Where(n => n % 2 == 0);
Console.WriteLine("偶数の一覧:");
foreach (var num in evenNumbers)
{
Console.WriteLine(num);
}
}
}
偶数の一覧:
2
4
6
8
10
このコードでは、n % 2 == 0
という条件で割り切れる数だけを抽出しています。
奇数を抽出したい場合は、n % 2 != 0
に変更すればよいです。
また、偶数・奇数の判定は整数型に限らず、long
やshort
などの整数型でも同様に使えます。
文字列の部分一致・前方一致・後方一致
文字列のコレクションから特定の文字列を含む要素を抽出する場合、Where
メソッドの条件式にContains
、StartsWith
、EndsWith
メソッドを使います。
これらは部分一致、前方一致、後方一致の判定に便利です。
以下のサンプルでは、文字列のリストから「apple」を含む要素、先頭が「b」で始まる要素、末尾が「e」で終わる要素をそれぞれ抽出しています。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<string> fruits = new List<string>
{
"apple",
"banana",
"grape",
"pineapple",
"blueberry",
"orange"
};
// 部分一致: "apple"を含む
var containsApple = fruits.Where(f => f.Contains("apple"));
// 前方一致: "b"で始まる
var startsWithB = fruits.Where(f => f.StartsWith("b"));
// 後方一致: "e"で終わる
var endsWithE = fruits.Where(f => f.EndsWith("e"));
Console.WriteLine("「apple」を含む要素:");
foreach (var fruit in containsApple)
{
Console.WriteLine(fruit);
}
Console.WriteLine("\n「b」で始まる要素:");
foreach (var fruit in startsWithB)
{
Console.WriteLine(fruit);
}
Console.WriteLine("\n「e」で終わる要素:");
foreach (var fruit in endsWithE)
{
Console.WriteLine(fruit);
}
}
}
「apple」を含む要素:
apple
pineapple
「b」で始まる要素:
banana
blueberry
「e」で終わる要素:
apple
grape
pineapple
orange
Contains
は部分文字列がどこかに含まれているかを判定し、StartsWith
は文字列の先頭が指定した文字列と一致するか、EndsWith
は末尾が一致するかを判定します。
これらは大文字・小文字を区別するため、必要に応じてStringComparison.OrdinalIgnoreCase
を指定して大文字小文字を無視することも可能です。
日付の範囲指定
日付データのコレクションから特定の期間内に該当する要素を抽出する場合は、DateTime
型の比較演算子を使って範囲指定を行います。
以下のサンプルでは、DateTime
のリストから2023年1月1日から2023年3月31日までの期間に該当する日付を抽出しています。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<DateTime> dates = new List<DateTime>
{
new DateTime(2022, 12, 31),
new DateTime(2023, 1, 15),
new DateTime(2023, 2, 10),
new DateTime(2023, 4, 1),
new DateTime(2023, 3, 31)
};
DateTime startDate = new DateTime(2023, 1, 1);
DateTime endDate = new DateTime(2023, 3, 31);
// 範囲内の日付を抽出
var filteredDates = dates.Where(d => d >= startDate && d <= endDate);
Console.WriteLine("2023年1月1日から3月31日までの日付:");
foreach (var date in filteredDates)
{
Console.WriteLine(date.ToString("yyyy-MM-dd"));
}
}
}
2023年1月1日から3月31日までの日付:
2023-01-15
2023-02-10
2023-03-31
この例では、d >= startDate && d <= endDate
という条件で日付の範囲を指定しています。
DateTime
型は比較演算子がオーバーロードされているため、大小比較が直感的に行えます。
また、DateOnly
型を使う場合も同様に比較演算子で範囲指定が可能です。
日付の範囲抽出は、イベントの期間絞り込みやログの期間検索などでよく使われます。
複数条件の組み合わせ
AND条件での範囲・属性同時チェック
複数の条件をすべて満たす要素を抽出したい場合は、Where
メソッドの条件式内で論理積(AND)演算子&&
を使います。
これにより、範囲指定や複数の属性を同時にチェックできます。
以下のサンプルでは、整数の配列から「3以上かつ7以下」の範囲にあり、かつ偶数である要素を抽出しています。
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 3以上7以下かつ偶数の要素を抽出
var filteredNumbers = numbers.Where(n => n >= 3 && n <= 7 && n % 2 == 0);
Console.WriteLine("3以上7以下かつ偶数の数:");
foreach (var num in filteredNumbers)
{
Console.WriteLine(num);
}
}
}
3以上7以下かつ偶数の数:
4
6
このように、&&
を使うことで複数の条件をすべて満たす要素だけを抽出できます。
文字列やオブジェクトのプロパティに対しても同様に複数条件を組み合わせられます。
例えば、以下は商品リストから価格が1000円以上かつ在庫が10個以上の商品を抽出する例です。
using System;
using System.Collections.Generic;
using System.Linq;
class Product
{
public string Name { get; set; }
public int Price { get; set; }
public int Stock { get; set; }
}
class Program
{
static void Main()
{
var products = new List<Product>
{
new Product { Name = "ペン", Price = 500, Stock = 20 },
new Product { Name = "ノート", Price = 1200, Stock = 5 },
new Product { Name = "ファイル", Price = 1500, Stock = 15 },
new Product { Name = "消しゴム", Price = 300, Stock = 50 }
};
var filteredProducts = products.Where(p => p.Price >= 1000 && p.Stock >= 10);
Console.WriteLine("価格が1000円以上かつ在庫が10個以上の商品:");
foreach (var product in filteredProducts)
{
Console.WriteLine($"{product.Name} - 価格: {product.Price}円, 在庫: {product.Stock}個");
}
}
}
価格が1000円以上かつ在庫が10個以上の商品:
ファイル - 価格: 1500円, 在庫: 15個
OR条件での条件緩和
条件のいずれかを満たす要素を抽出したい場合は、論理和(OR)演算子||
を使います。
これにより、複数の条件のうちどれか一つでも真であれば結果に含めることができます。
以下のサンプルでは、整数の配列から「3未満または7より大きい」要素を抽出しています。
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 3未満または7より大きい要素を抽出
var filteredNumbers = numbers.Where(n => n < 3 || n > 7);
Console.WriteLine("3未満または7より大きい数:");
foreach (var num in filteredNumbers)
{
Console.WriteLine(num);
}
}
}
3未満または7より大きい数:
1
2
8
9
10
文字列の例では、名前が「田中」または「佐藤」で始まる要素を抽出することも可能です。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var names = new List<string> { "田中一郎", "佐藤花子", "鈴木次郎", "高橋三郎" };
var filteredNames = names.Where(name => name.StartsWith("田中") || name.StartsWith("佐藤"));
Console.WriteLine("「田中」または「佐藤」で始まる名前:");
foreach (var name in filteredNames)
{
Console.WriteLine(name);
}
}
}
「田中」または「佐藤」で始まる名前:
田中一郎
佐藤花子
条件式の外部メソッド化と再利用
複雑な条件式をWhere
のラムダ式内に直接書くと可読性が低下し、保守が難しくなります。
そこで、条件判定のロジックを外部メソッドに切り出して再利用する方法が有効です。
以下の例では、商品が「価格が1000円以上かつ在庫が10個以上」という条件を判定するメソッドIsAvailableProduct
を定義し、Where
の引数に渡しています。
using System;
using System.Collections.Generic;
using System.Linq;
class Product
{
public string Name { get; set; }
public int Price { get; set; }
public int Stock { get; set; }
}
class Program
{
static bool IsAvailableProduct(Product p)
{
// 価格が1000円以上かつ在庫が10個以上かどうかを判定
return p.Price >= 1000 && p.Stock >= 10;
}
static void Main()
{
var products = new List<Product>
{
new Product { Name = "ペン", Price = 500, Stock = 20 },
new Product { Name = "ノート", Price = 1200, Stock = 5 },
new Product { Name = "ファイル", Price = 1500, Stock = 15 },
new Product { Name = "消しゴム", Price = 300, Stock = 50 }
};
var filteredProducts = products.Where(IsAvailableProduct);
Console.WriteLine("条件を外部メソッド化した商品抽出:");
foreach (var product in filteredProducts)
{
Console.WriteLine($"{product.Name} - 価格: {product.Price}円, 在庫: {product.Stock}個");
}
}
}
条件を外部メソッド化した商品抽出:
ファイル - 価格: 1500円, 在庫: 15個
この方法のメリットは以下の通りです。
- 条件ロジックがメソッドにまとまるため、コードの見通しが良くなる
- 複数箇所で同じ条件を使い回せる
- 単体テストがしやすくなる
ラムダ式の中に複雑な条件を詰め込むのではなく、外部メソッドに切り出して可読性と保守性を高めることをおすすめします。
インデックスを用いたフィルタリング
Where((value, index) => …)の活用
LINQのWhere
メソッドは、単に要素の値だけでなく、その要素のインデックス(位置)を利用した条件でフィルタリングすることも可能です。
これはWhere
メソッドのオーバーロードで、引数に要素の値とインデックスの両方を受け取るラムダ式を指定します。
シグネチャは以下の通りです。
IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, int, bool> predicate)
TSource
は要素の型predicate
は要素の値とインデックスを受け取り、true
ならその要素を結果に含めます
この機能を使うと、要素の位置に基づくフィルタリングが簡単にできます。
以下のサンプルでは、文字列のリストからインデックスが偶数の要素だけを抽出しています。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var fruits = new List<string> { "apple", "banana", "cherry", "date", "elderberry" };
// インデックスが偶数の要素を抽出
var evenIndexFruits = fruits.Where((fruit, index) => index % 2 == 0);
Console.WriteLine("インデックスが偶数の果物:");
foreach (var fruit in evenIndexFruits)
{
Console.WriteLine(fruit);
}
}
}
インデックスが偶数の果物:
apple
cherry
elderberry
この例では、index % 2 == 0
の条件で0, 2, 4番目の要素を抽出しています。
インデックスは0から始まるため、最初の要素がインデックス0に該当します。
奇数番目・偶数番目要素の抽出
インデックスを使ったフィルタリングは、奇数番目や偶数番目の要素を抽出する際に特に便利です。
ここでの「奇数番目」「偶数番目」は、0始まりのインデックスに基づくため、インデックスが偶数なら「1番目、3番目、5番目…」、インデックスが奇数なら「2番目、4番目、6番目…」の要素を指します。
以下のサンプルでは、整数配列から奇数番目(インデックスが奇数)と偶数番目(インデックスが偶数)の要素をそれぞれ抽出しています。
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 10, 20, 30, 40, 50, 60, 70 };
// 偶数番目の要素(インデックス0,2,4,...)
var evenIndexNumbers = numbers.Where((num, index) => index % 2 == 0);
// 奇数番目の要素(インデックス1,3,5,...)
var oddIndexNumbers = numbers.Where((num, index) => index % 2 == 1);
Console.WriteLine("偶数番目の要素:");
foreach (var num in evenIndexNumbers)
{
Console.WriteLine(num);
}
Console.WriteLine("\n奇数番目の要素:");
foreach (var num in oddIndexNumbers)
{
Console.WriteLine(num);
}
}
}
偶数番目の要素:
10
30
50
70
奇数番目の要素:
20
40
60
このように、Where
のインデックス引数を活用することで、位置に基づく抽出が簡単に行えます。
例えば、リストの偶数番目だけを処理したい場合や、交互に要素を取り出したい場合などに役立ちます。
Nullと例外への配慮
Null許容型の安全な比較
LINQでフィルタリングを行う際、対象のコレクションや要素にnull
が含まれている場合があります。
特に文字列や参照型のプロパティを条件に使うときは、null
参照による例外NullReferenceException
を防ぐために安全な比較を行うことが重要です。
例えば、文字列のリストから「apple」を含む要素を抽出する場合、要素がnull
だとContains
メソッドを呼び出した時に例外が発生します。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<string> fruits = new List<string> { "apple", null, "pineapple", "banana" };
// nullチェックなしでContainsを使うと例外が発生する可能性がある
// var filtered = fruits.Where(f => f.Contains("apple")); // 例外発生
// 安全な比較: nullチェックを先に行う
var filtered = fruits.Where(f => f != null && f.Contains("apple"));
Console.WriteLine("「apple」を含む要素(null安全):");
foreach (var fruit in filtered)
{
Console.WriteLine(fruit);
}
}
}
「apple」を含む要素(null安全):
apple
pineapple
このように、f != null
のチェックを先に行うことで、null
要素を除外し安全にContains
を呼び出せます。
C# 6.0以降では、null条件演算子?.
を使ってさらに簡潔に書くことも可能です。
var filtered = fruits.Where(f => f?.Contains("apple") == true);
この書き方は、f
がnull
の場合はnull
を返し、== true
で真偽値を判定するため、null
要素は自動的に除外されます。
また、null
許容型の値型(int?
やDateTime?
など)を比較する場合も、HasValue
プロパティやGetValueOrDefault
メソッドを使って安全に扱うことが推奨されます。
コレクションがNullの場合のガード節
LINQのWhere
メソッドは、対象のコレクションがnull
の場合に呼び出すとArgumentNullException
が発生します。
したがって、コレクション自体がnull
の可能性がある場合は、事前にnull
チェックを行うか、空のコレクションに置き換えてから処理を行うことが重要です。
以下の例では、items
がnull
の場合に備えてガード節を設けています。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<string> items = null;
// nullチェックを行い、nullなら空のリストに置き換える
var safeItems = items ?? Enumerable.Empty<string>();
var filtered = safeItems.Where(item => item != null && item.StartsWith("A"));
Console.WriteLine("「A」で始まる要素:");
foreach (var item in filtered)
{
Console.WriteLine(item);
}
}
}
「A」で始まる要素:
この例では、items
がnull
のためEnumerable.Empty<string>()
で空のシーケンスに置き換えています。
これにより、Where
メソッドの呼び出し時に例外が発生せず、安全に処理が進みます。
また、メソッドの引数としてコレクションを受け取る場合は、呼び出し元でnull
チェックを行うか、メソッド内で以下のようにガード節を設けることが一般的です。
void ProcessItems(IEnumerable<string> items)
{
if (items == null)
{
Console.WriteLine("コレクションがnullです。処理を中断します。");
return;
}
var filtered = items.Where(item => item != null && item.Length > 0);
// 処理続行...
}
このように、コレクションがnull
である可能性を考慮し、適切にガード節を設けることで例外を防ぎ、堅牢なコードを書くことができます。
文字列特化のフィルタリング
大文字小文字を無視した比較
文字列のフィルタリングで大文字・小文字の違いを無視したい場合、String
クラスの比較メソッドにStringComparison
列挙体を指定する方法が一般的です。
Where
メソッドの条件式内でEquals
やStartsWith
、EndsWith
、Contains
などのメソッドにStringComparison.OrdinalIgnoreCase
やStringComparison.InvariantCultureIgnoreCase
を渡すことで、大文字小文字を区別せずに比較できます。
以下のサンプルでは、文字列リストから「apple」という単語を大文字小文字を無視して含む要素を抽出しています。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var fruits = new List<string>
{
"Apple",
"banana",
"APPLE pie",
"PineApple",
"orange"
};
// 大文字小文字を無視して"apple"を含む要素を抽出
var filtered = fruits.Where(f => f.IndexOf("apple", StringComparison.OrdinalIgnoreCase) >= 0);
Console.WriteLine("「apple」を大文字小文字無視で含む要素:");
foreach (var fruit in filtered)
{
Console.WriteLine(fruit);
}
}
}
「apple」を大文字小文字無視で含む要素:
Apple
APPLE pie
PineApple
IndexOf
メソッドは、指定した文字列が最初に現れる位置を返します。
見つからなければ-1
を返すため、>= 0
で含むかどうかを判定しています。
StringComparison.OrdinalIgnoreCase
を指定することで、大文字小文字を区別しません。
同様に、StartsWith
やEndsWith
も以下のように使えます。
var startsWithA = fruits.Where(f => f.StartsWith("a", StringComparison.OrdinalIgnoreCase));
var endsWithE = fruits.Where(f => f.EndsWith("e", StringComparison.OrdinalIgnoreCase));
これらのメソッドを使うことで、文字列の大文字小文字を気にせず柔軟にフィルタリングできます。
正規表現Regex.IsMatchとの併用
より複雑な文字列パターンでフィルタリングしたい場合は、System.Text.RegularExpressions.Regex
クラスのIsMatch
メソッドを使うと便利です。
正規表現を使うことで、部分一致だけでなく、パターンマッチングや文字種の指定、繰り返しなど多彩な条件を表現できます。
以下のサンプルでは、文字列リストから「apple」または「banana」を含む要素を正規表現で抽出しています。
大文字小文字を無視するためにRegexOptions.IgnoreCase
を指定しています。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
static void Main()
{
var fruits = new List<string>
{
"Apple",
"banana",
"Cherry",
"Pineapple",
"Banana Split",
"Orange"
};
var pattern = "apple|banana"; // "apple"または"banana"にマッチ
var filtered = fruits.Where(f => Regex.IsMatch(f, pattern, RegexOptions.IgnoreCase));
Console.WriteLine("正規表現で「apple」または「banana」を含む要素:");
foreach (var fruit in filtered)
{
Console.WriteLine(fruit);
}
}
}
正規表現で「apple」または「banana」を含む要素:
Apple
banana
Pineapple
Banana Split
正規表現は柔軟性が高い反面、パフォーマンスに影響を与えることがあるため、単純な部分一致で十分な場合はIndexOf
やContains
の方が高速です。
複雑なパターンマッチングが必要な場合にRegex
を使うとよいでしょう。
また、正規表現のパターンは事前にコンパイルしておくとパフォーマンスが向上します。
var regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
var filtered = fruits.Where(f => regex.IsMatch(f));
このように、Regex.IsMatch
とLINQのWhere
を組み合わせることで、強力かつ柔軟な文字列フィルタリングが可能になります。
日付・時刻データの扱い
DateTimeとDateOnlyによる期間絞り込み
C#で日付や時刻を扱う際、DateTime
型は日時情報を持ち、時刻まで含めて管理できます。
一方、DateOnly
型は日付部分のみを扱うため、時刻を無視した期間絞り込みに便利です。
LINQのWhere
メソッドを使って、これらの型で期間を指定してデータを抽出する方法を紹介します。
まず、DateTime
を使った期間絞り込みの例です。
以下のサンプルでは、日時のリストから2023年1月1日0時0分0秒以降かつ2023年1月31日23時59分59秒以前のデータを抽出しています。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var dateTimes = new List<DateTime>
{
new DateTime(2022, 12, 31, 23, 59, 59),
new DateTime(2023, 1, 1, 0, 0, 0),
new DateTime(2023, 1, 15, 12, 30, 0),
new DateTime(2023, 1, 31, 23, 59, 59),
new DateTime(2023, 2, 1, 0, 0, 0)
};
DateTime start = new DateTime(2023, 1, 1, 0, 0, 0);
DateTime end = new DateTime(2023, 1, 31, 23, 59, 59);
var filtered = dateTimes.Where(dt => dt >= start && dt <= end);
Console.WriteLine("2023年1月の日時データ:");
foreach (var dt in filtered)
{
Console.WriteLine(dt.ToString("yyyy-MM-dd HH:mm:ss"));
}
}
}
2023年1月の日時データ:
2023-01-01 00:00:00
2023-01-15 12:30:00
2023-01-31 23:59:59
DateTime
は時刻まで含むため、時間単位での絞り込みが可能です。
次に、DateOnly
を使った例です。
DateOnly
は.NET 6以降で利用可能で、日付だけを扱うため、時刻を気にせずに期間を指定できます。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var dates = new List<DateOnly>
{
new DateOnly(2023, 1, 1),
new DateOnly(2023, 1, 15),
new DateOnly(2023, 1, 31),
new DateOnly(2023, 2, 1)
};
DateOnly start = new DateOnly(2023, 1, 1);
DateOnly end = new DateOnly(2023, 1, 31);
var filtered = dates.Where(d => d >= start && d <= end);
Console.WriteLine("2023年1月の日付データ:");
foreach (var d in filtered)
{
Console.WriteLine(d.ToString("yyyy-MM-dd"));
}
}
}
2023年1月の日付データ:
2023-01-01
2023-01-15
2023-01-31
DateOnly
は日付単位の比較に特化しているため、時刻の影響を受けずに期間絞り込みができます。
DateTime
とDateOnly
は用途に応じて使い分けるとよいでしょう。
タイムゾーン変換を伴う条件指定
日時データを扱う際、タイムゾーンの違いを考慮する必要がある場合があります。
特にサーバーのローカル時間とユーザーのタイムゾーンが異なる場合、正確な期間絞り込みにはタイムゾーン変換が欠かせません。
DateTime
型にはKind
プロパティがあり、Utc
(協定世界時)、Local
(ローカル時間)、Unspecified
(未指定)の3種類があります。
タイムゾーンを考慮した比較を行うには、日時を共通のタイムゾーンに変換してから比較するのが基本です。
以下のサンプルでは、UTC日時のリストを日本標準時(JST、UTC+9)に変換し、JSTの2023年1月1日から1月31日までの期間に該当する日時を抽出しています。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var utcDates = new List<DateTime>
{
new DateTime(2022, 12, 31, 15, 0, 0, DateTimeKind.Utc), // JST: 2023-01-01 00:00:00
new DateTime(2023, 1, 10, 3, 0, 0, DateTimeKind.Utc), // JST: 2023-01-10 12:00:00
new DateTime(2023, 2, 1, 0, 0, 0, DateTimeKind.Utc) // JST: 2023-02-01 09:00:00
};
// JSTのタイムゾーン情報を取得
TimeZoneInfo jstZone = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
DateTime jstStart = new DateTime(2023, 1, 1, 0, 0, 0);
DateTime jstEnd = new DateTime(2023, 1, 31, 23, 59, 59);
var filtered = utcDates.Where(utc =>
{
// UTC日時をJSTに変換
DateTime jstTime = TimeZoneInfo.ConvertTimeFromUtc(utc, jstZone);
return jstTime >= jstStart && jstTime <= jstEnd;
});
Console.WriteLine("JSTの2023年1月の日時データ:");
foreach (var utc in filtered)
{
DateTime jstTime = TimeZoneInfo.ConvertTimeFromUtc(utc, jstZone);
Console.WriteLine(jstTime.ToString("yyyy-MM-dd HH:mm:ss"));
}
}
}
JSTの2023年1月の日時データ:
2023-01-01 00:00:00
2023-01-10 12:00:00
この例では、UTCの日時をJSTに変換してから期間判定を行っています。
Windows環境ではタイムゾーンIDが"Tokyo Standard Time"
ですが、LinuxやmacOSでは"Asia/Tokyo"
など異なる場合があるため、環境に応じて適切なIDを指定してください。
また、DateTimeOffset
型を使うとタイムゾーン情報を含む日時を扱いやすくなります。
DateTimeOffset
は日時とオフセット(UTCとの差)を持つため、変換や比較がより明確になります。
タイムゾーンを考慮した期間絞り込みは、グローバルなアプリケーションや多地域対応のシステムで特に重要です。
日時のKind
やタイムゾーン変換を正しく理解し、適切に処理を行いましょう。
複合オブジェクトの条件抽出
プロパティ深掘りと匿名型投影
LINQで複合オブジェクトの条件抽出を行う際、オブジェクトのプロパティを深掘りして条件を指定することがよくあります。
また、必要に応じて匿名型を使って特定のプロパティだけを抽出・投影(プロジェクション)することも可能です。
以下の例では、Person
クラスの中にAddress
という複合プロパティがあり、その中のCity
プロパティを条件にフィルタリングしています。
また、結果は匿名型で名前と都市名だけを抽出しています。
using System;
using System.Collections.Generic;
using System.Linq;
class Address
{
public string City { get; set; }
public string Street { get; set; }
}
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Address Address { get; set; }
}
class Program
{
static void Main()
{
var people = new List<Person>
{
new Person { Name = "山田太郎", Age = 30, Address = new Address { City = "東京", Street = "千代田区" } },
new Person { Name = "佐藤花子", Age = 25, Address = new Address { City = "大阪", Street = "北区" } },
new Person { Name = "鈴木次郎", Age = 40, Address = new Address { City = "東京", Street = "港区" } }
};
// 東京に住む人を抽出し、名前と都市名だけを匿名型で取得
var filtered = people
.Where(p => p.Address != null && p.Address.City == "東京")
.Select(p => new { p.Name, City = p.Address.City });
Console.WriteLine("東京に住む人:");
foreach (var person in filtered)
{
Console.WriteLine($"名前: {person.Name}, 都市: {person.City}");
}
}
}
東京に住む人:
名前: 山田太郎, 都市: 東京
名前: 鈴木次郎, 都市: 東京
この例では、Where
句でAddress
がnull
でないことを確認しつつ、City
が「東京」であるかを判定しています。
Select
句で匿名型を使い、必要な情報だけを抽出しているため、後続の処理や表示がシンプルになります。
匿名型は型名を定義せずに複数のプロパティをまとめて扱えるため、データの一時的な投影に非常に便利です。
ネストしたコレクションでのAny/All検索
複合オブジェクトの中にさらにコレクションがネストしている場合、LINQのAny
やAll
メソッドを使って条件を指定することが多いです。
Any
はコレクション内に条件を満たす要素が1つでもあればtrue
を返し、All
はすべての要素が条件を満たす場合にtrue
を返します。
以下の例では、Order
クラスの中に複数のOrderItem
があり、注文の中に特定の商品が含まれているかどうかをAny
で判定しています。
using System;
using System.Collections.Generic;
using System.Linq;
class OrderItem
{
public string ProductName { get; set; }
public int Quantity { get; set; }
}
class Order
{
public int OrderId { get; set; }
public List<OrderItem> Items { get; set; }
}
class Program
{
static void Main()
{
var orders = new List<Order>
{
new Order
{
OrderId = 1,
Items = new List<OrderItem>
{
new OrderItem { ProductName = "ペン", Quantity = 3 },
new OrderItem { ProductName = "ノート", Quantity = 5 }
}
},
new Order
{
OrderId = 2,
Items = new List<OrderItem>
{
new OrderItem { ProductName = "消しゴム", Quantity = 2 }
}
},
new Order
{
OrderId = 3,
Items = new List<OrderItem>
{
new OrderItem { ProductName = "ペン", Quantity = 1 },
new OrderItem { ProductName = "定規", Quantity = 4 }
}
}
};
// 注文の中に「ペン」が含まれている注文を抽出
var ordersWithPen = orders.Where(o => o.Items != null && o.Items.Any(item => item.ProductName == "ペン"));
Console.WriteLine("「ペン」が含まれる注文ID:");
foreach (var order in ordersWithPen)
{
Console.WriteLine(order.OrderId);
}
}
}
「ペン」が含まれる注文ID:
1
3
この例では、Where
の条件でItems
がnull
でないことを確認し、Any
を使ってProductName
が「ペン」であるアイテムが存在するかを判定しています。
同様に、すべてのアイテムが特定の条件を満たすかを調べたい場合はAll
を使います。
例えば、以下はすべての注文アイテムの数量が1以上である注文を抽出する例です。
var validOrders = orders.Where(o => o.Items != null && o.Items.All(item => item.Quantity > 0));
Any
やAll
を使うことで、ネストしたコレクションの中身に対して柔軟に条件を指定でき、複雑なデータ構造でも簡潔にフィルタリングが可能です。
コレクション種類別のアプローチ
List<T>とIEnumerable<T>の違いを意識した記述
C#のLINQでよく使われるコレクション型として、List<T>
とIEnumerable<T>
があります。
これらは似ているようで異なる特徴を持つため、フィルタリングや条件式を書く際には違いを理解しておくことが重要です。
List<T>
は可変長のリストであり、要素の追加・削除が可能な具体的なコレクションです。
一方、IEnumerable<T>
は列挙可能なシーケンスを表すインターフェースで、実際のデータ構造を隠蔽し、遅延実行をサポートします。
LINQのWhere
メソッドはIEnumerable<T>
に対して定義されているため、List<T>
もIEnumerable<T>
として扱えますが、以下の点に注意が必要です。
- 遅延実行の影響
IEnumerable<T>
は遅延実行を行うため、Where
で条件を指定しても即座に処理は実行されません。
List<T>
のメソッド(例:Count
やAdd
)は即時実行です。
そのため、IEnumerable<T>
のまま処理を続けると、複数回列挙される可能性があり、パフォーマンスに影響を与えることがあります。
- メソッドの利用制限
List<T>
はインデックスアクセスや要素の追加・削除が可能ですが、IEnumerable<T>
は読み取り専用でインデックスアクセスができません。
そのため、条件式の中でインデックスを使いたい場合はList<T>
や配列などの具体的なコレクションが必要です。
- メモリ効率
IEnumerable<T>
は必要な要素だけを逐次処理できるため、大量データの処理に向いています。
List<T>
は全要素をメモリに保持します。
以下のサンプルでは、List<int>
とIEnumerable<int>
で同じ条件を使ったフィルタリングを行い、ToList()
で即時実行している例です。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> listNumbers = new List<int> { 1, 2, 3, 4, 5 };
IEnumerable<int> enumerableNumbers = listNumbers;
// IEnumerableのままフィルタリング(遅延実行)
var filteredEnumerable = enumerableNumbers.Where(n => n > 2);
// Listに変換して即時実行
var filteredList = filteredEnumerable.ToList();
Console.WriteLine("IEnumerableでフィルタリング後、ToListで即時実行:");
foreach (var num in filteredList)
{
Console.WriteLine(num);
}
}
}
IEnumerableでフィルタリング後、ToListで即時実行:
3
4
5
このように、IEnumerable<T>
のまま処理を続ける場合は遅延実行の特性を意識し、必要に応じてToList()
やToArray()
で即時実行することが望ましいです。
配列・HashSet<T>・Dictionary<TKey,TValue>での条件式例
LINQは配列やHashSet<T>
、Dictionary<TKey,TValue>
など様々なコレクションに対応していますが、条件式の書き方や注意点が少し異なります。
配列での条件式例
配列は固定長のコレクションで、インデックスアクセスが可能です。
Where
メソッドはIEnumerable<T>
として使えます。
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 10, 20, 30, 40, 50 };
// 30以上の要素を抽出
var filtered = numbers.Where(n => n >= 30);
Console.WriteLine("配列から30以上の数:");
foreach (var num in filtered)
{
Console.WriteLine(num);
}
}
}
配列から30以上の数:
30
40
50
HashSet<T>での条件式例
HashSet<T>
は重複を許さない集合で、要素の順序は保証されません。
LINQのWhere
は問題なく使えますが、順序を意識した処理は避けるべきです。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var set = new HashSet<string> { "apple", "banana", "cherry" };
// "a"を含む要素を抽出
var filtered = set.Where(s => s.Contains("a"));
Console.WriteLine("HashSetから'a'を含む要素:");
foreach (var item in filtered)
{
Console.WriteLine(item);
}
}
}
HashSetから'a'を含む要素:
apple
banana
順序は保証されないため、結果の順序に依存する処理は注意してください。
Dictionary<TKey,TValue>での条件式例
Dictionary<TKey,TValue>
はキーと値のペアを保持するコレクションです。
LINQではKeyValuePair<TKey,TValue>
型のシーケンスとして扱われます。
条件式ではキーや値のどちらか、または両方を使ってフィルタリングできます。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var dict = new Dictionary<int, string>
{
{ 1, "東京" },
{ 2, "大阪" },
{ 3, "名古屋" }
};
// キーが2以上の要素を抽出
var filteredByKey = dict.Where(kv => kv.Key >= 2);
Console.WriteLine("キーが2以上の要素:");
foreach (var kv in filteredByKey)
{
Console.WriteLine($"キー: {kv.Key}, 値: {kv.Value}");
}
// 値に「大」が含まれる要素を抽出
var filteredByValue = dict.Where(kv => kv.Value.Contains("大"));
Console.WriteLine("\n値に「大」が含まれる要素:");
foreach (var kv in filteredByValue)
{
Console.WriteLine($"キー: {kv.Key}, 値: {kv.Value}");
}
}
}
キーが2以上の要素:
キー: 2, 値: 大阪
キー: 3, 値: 名古屋
値に「大」が含まれる要素:
キー: 2, 値: 大阪
キー: 3, 値: 名古屋
このように、Dictionary
のKeyValuePair
のKey
やValue
を使って柔軟に条件を指定できます。
コレクションの種類によって特性や使い方が異なるため、LINQの条件式を書く際は対象のコレクションの特徴を理解し、適切な方法でフィルタリングを行うことが重要です。
パフォーマンス最適化
不要なToList()呼び出しを避ける方法
LINQのクエリを実行する際、ToList()
メソッドを使うことでIEnumerable<T>
の遅延実行を即時実行に変え、結果をリストとして取得できます。
しかし、ToList()
を多用すると不要なリストのコピーやメモリ消費が発生し、パフォーマンス低下の原因となります。
例えば、以下のように複数回ToList()
を呼び出すケースは避けるべきです。
var filtered = source.Where(x => x.IsActive).ToList();
var furtherFiltered = filtered.Where(x => x.Score > 50).ToList();
この場合、最初のToList()
で一度リスト化し、次のWhere
でさらに絞り込んで再度リスト化しています。
これにより、2回のリスト生成が発生し、無駄なメモリ消費と処理時間がかかります。
パフォーマンスを最適化するには、可能な限りToList()
の呼び出しを遅らせ、クエリをまとめてから一度だけ実行することが重要です。
var filtered = source.Where(x => x.IsActive)
.Where(x => x.Score > 50)
.ToList();
このようにクエリを合成してからToList()
を呼ぶことで、1回のリスト生成で済み、効率的に処理できます。
クエリ合成による一括評価
LINQは遅延実行の特性を持つため、複数の条件を連結してクエリを合成できます。
これにより、データソースを一度だけ列挙して条件をまとめて評価できるため、パフォーマンスが向上します。
例えば、以下のように複数のWhere
を連結しても、実際のデータ列挙はforeach
やToList()
などで結果を取得するタイミングまで遅延されます。
var query = source.Where(x => x.IsActive)
.Where(x => x.Score > 50)
.Where(x => x.Category == "A");
このクエリは、source
を一度だけ列挙し、各要素に対して3つの条件を順に評価します。
途中で条件を満たさない要素はすぐに除外されるため、無駄な処理が減ります。
クエリ合成のポイントは以下の通りです。
- 条件は可能な限りまとめて書くか、複数の
Where
で分割しても問題ない - クエリの実行は結果を列挙するタイミング(
foreach
やToList()
など)で一度だけ行われる - 不要な中間コレクションの生成を避けられる
この特性を活かして、複雑な条件でも効率的にフィルタリングを行いましょう。
多重列挙を防ぐToArray()の挿入ポイント
LINQのIEnumerable<T>
は遅延実行のため、同じクエリを複数回列挙すると、その都度データソースを走査します。
これを多重列挙と呼び、パフォーマンス低下や副作用の原因になることがあります。
例えば、以下のコードはquery
を2回列挙しているため、2回データソースを走査します。
var query = source.Where(x => x.IsActive);
int count = query.Count();
var list = query.ToList();
この場合、Count()
とToList()
の両方でquery
が列挙されます。
多重列挙を防ぐには、ToArray()
やToList()
を使って一度だけ列挙し、結果をメモリ上に保持する方法があります。
ToArray()
は配列を返し、ToList()
はリストを返しますが、どちらも即時実行を行います。
var array = source.Where(x => x.IsActive).ToArray();
int count = array.Length;
var list = array.ToList();
このように、ToArray()
で一度だけ列挙して配列に格納すれば、その後の操作は配列に対して行われるため、多重列挙を防げます。
ToArray()
を使うタイミングは、以下のような場合が多いです。
- 同じクエリ結果を複数回使う場合
- データソースが遅延実行で副作用がある場合
- パフォーマンスを重視し、複数回の列挙を避けたい場合
ただし、ToArray()
やToList()
はメモリに全要素を保持するため、大量データの場合はメモリ消費に注意してください。
これらのポイントを踏まえ、LINQのパフォーマンスを最適化することで、効率的かつ安定したデータ処理が可能になります。
条件式の動的生成
Func<T,bool>デリゲートの組み立て
LINQのWhere
メソッドでは、条件式をラムダ式として直接記述することが多いですが、動的に条件を組み立てたい場合はFunc<T,bool>
デリゲートを使って条件式を生成し、柔軟に切り替えることが可能です。
例えば、ユーザーの入力や設定に応じてフィルタ条件を変えたい場合、複数の条件を組み合わせてFunc<T,bool>
を作成し、それをWhere
に渡す方法が有効です。
以下のサンプルでは、Person
クラスのリストから年齢や名前の条件を動的に組み立ててフィルタリングしています。
using System;
using System.Collections.Generic;
using System.Linq;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main()
{
var people = new List<Person>
{
new Person { Name = "山田太郎", Age = 25 },
new Person { Name = "佐藤花子", Age = 30 },
new Person { Name = "鈴木次郎", Age = 35 }
};
// 動的に条件を組み立てる例
bool filterByAge = true;
int minAge = 28;
bool filterByName = true;
string nameContains = "佐藤";
Func<Person, bool> predicate = p => true; // 初期は常にtrue
if (filterByAge)
{
var prev = predicate;
predicate = p => prev(p) && p.Age >= minAge;
}
if (filterByName)
{
var prev = predicate;
predicate = p => prev(p) && p.Name.Contains(nameContains);
}
var filtered = people.Where(predicate);
Console.WriteLine("動的条件でフィルタリングした結果:");
foreach (var person in filtered)
{
Console.WriteLine($"{person.Name} - {person.Age}歳");
}
}
}
動的条件でフィルタリングした結果:
佐藤花子 - 30歳
この例では、predicate
を初期化してから条件ごとに前の条件を保持しつつ新しい条件を追加しています。
こうすることで、条件のON/OFFやパラメータの変更に柔軟に対応できます。
Expression<Func<T,bool>>での後処理対応
Func<T,bool>
はメソッドとして実行可能なデリゲートですが、式ツリー(Expression Tree)としての情報は持ちません。
データベースクエリやORM(Entity Frameworkなど)でLINQを使う場合、式ツリーを扱うExpression<Func<T,bool>>
が必要です。
これにより、クエリの解析や変換、最適化が可能になります。
動的に条件式を組み立てる際にExpression<Func<T,bool>>
を使うと、後で式ツリーを解析したり、他の式と合成したりできます。
以下のサンプルは、Expression<Func<T,bool>>
を使って動的に条件を組み立て、Where
に渡す例です。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main()
{
var people = new List<Person>
{
new Person { Name = "山田太郎", Age = 25 },
new Person { Name = "佐藤花子", Age = 30 },
new Person { Name = "鈴木次郎", Age = 35 }
};
bool filterByAge = true;
int minAge = 28;
bool filterByName = true;
string nameContains = "佐藤";
// 初期の式は常にtrueを返す式
Expression<Func<Person, bool>> predicate = p => true;
if (filterByAge)
{
Expression<Func<Person, bool>> ageExpr = p => p.Age >= minAge;
predicate = predicate.AndAlso(ageExpr);
}
if (filterByName)
{
Expression<Func<Person, bool>> nameExpr = p => p.Name.Contains(nameContains);
predicate = predicate.AndAlso(nameExpr);
}
var filtered = people.AsQueryable().Where(predicate);
Console.WriteLine("Expressionで動的条件を組み立てた結果:");
foreach (var person in filtered)
{
Console.WriteLine($"{person.Name} - {person.Age}歳");
}
}
}
// ExpressionのAndAlso拡張メソッド
public static class ExpressionExtensions
{
public static Expression<Func<T, bool>> AndAlso<T>(
this Expression<Func<T, bool>> expr1,
Expression<Func<T, bool>> expr2)
{
var parameter = Expression.Parameter(typeof(T));
var leftVisitor = new ReplaceParameterVisitor(expr1.Parameters[0], parameter);
var left = leftVisitor.Visit(expr1.Body);
var rightVisitor = new ReplaceParameterVisitor(expr2.Parameters[0], parameter);
var right = rightVisitor.Visit(expr2.Body);
var body = Expression.AndAlso(left, right);
return Expression.Lambda<Func<T, bool>>(body, parameter);
}
private class ReplaceParameterVisitor : ExpressionVisitor
{
private readonly ParameterExpression _oldParameter;
private readonly ParameterExpression _newParameter;
public ReplaceParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
{
_oldParameter = oldParameter;
_newParameter = newParameter;
}
protected override Expression VisitParameter(ParameterExpression node)
{
return node == _oldParameter ? _newParameter : base.VisitParameter(node);
}
}
}
Expressionで動的条件を組み立てた結果:
佐藤花子 - 30歳
この例では、Expression<Func<T,bool>>
の式ツリーをAndAlso
拡張メソッドで合成しています。
ReplaceParameterVisitor
は異なるパラメータを統一するために使われ、複数の式を正しく結合できるようにしています。
Expression<Func<T,bool>>
を使うことで、ORMのクエリ変換やSQL生成などの後処理が可能になり、動的クエリの柔軟性とパフォーマンスを両立できます。
並列・非同期シナリオ
PLINQでの並列フィルタリング
PLINQ(Parallel LINQ)は、LINQクエリを並列処理に対応させるための拡張機能です。
大量のデータを複数のCPUコアで同時に処理することで、フィルタリングや集計のパフォーマンスを向上させることができます。
PLINQを使うには、AsParallel()
メソッドを呼び出して並列クエリに変換し、その後に通常のLINQメソッドを続けて記述します。
並列処理は自動的にスレッドプールのスレッドを利用して実行されます。
以下のサンプルでは、1から1000までの整数の中から、3の倍数かつ5の倍数の数をPLINQで並列に抽出しています。
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 1000);
// PLINQで並列フィルタリング
var filtered = numbers.AsParallel()
.Where(n => n % 3 == 0 && n % 5 == 0)
.ToArray();
Console.WriteLine("3の倍数かつ5の倍数の数(PLINQ):");
foreach (var num in filtered)
{
Console.WriteLine(num);
}
}
}
3の倍数かつ5の倍数の数(PLINQ):
15
30
45
60
75
90
105
120
135
150
165
180
195
210
225
240
255
270
285
300
315
330
345
360
375
390
405
420
435
450
465
480
495
510
525
540
555
570
585
600
615
630
645
660
675
690
705
720
735
750
765
780
795
810
825
840
855
870
885
900
915
930
945
960
975
990
PLINQは内部でデータを分割し、複数スレッドで並列に処理するため、CPUリソースを有効活用できます。
ただし、並列処理のオーバーヘッドがあるため、データ量が少ない場合や処理が軽い場合は逆に遅くなることもあります。
また、PLINQのクエリは順序を保証しません。
順序を維持したい場合はAsOrdered()
を追加しますが、パフォーマンスに影響が出ることがあります。
IAsyncEnumerable<T>とawait foreachでの非同期抽出
.NET Core 3.0以降では、非同期ストリームを扱うためにIAsyncEnumerable<T>
インターフェースが導入されました。
これにより、非同期にデータを逐次取得しながら処理できるようになり、特にI/O待ちが発生するシナリオで有効です。
IAsyncEnumerable<T>
はawait foreach
構文で列挙でき、非同期に要素を取得しながら処理を進められます。
LINQのWhere
などの拡張メソッドは標準では非同期対応していませんが、System.Linq.Async
パッケージを使うと非同期LINQメソッドが利用可能です。
以下のサンプルは、非同期にデータを生成し、非同期LINQのWhere
で条件抽出を行い、await foreach
で結果を列挙しています。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using System.Linq.Async; // NuGetパッケージ System.Linq.Async が必要
class Program
{
static async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 1; i <= 20; i++)
{
await Task.Delay(50); // 非同期処理のシミュレーション
yield return i;
}
}
static async Task Main()
{
// 非同期ストリームから偶数だけを抽出
var filtered = GenerateNumbersAsync().Where(n => n % 2 == 0);
Console.WriteLine("非同期ストリームから偶数を抽出:");
await foreach (var num in filtered)
{
Console.WriteLine(num);
}
}
}
非同期ストリームから偶数を抽出:
2
4
6
8
10
12
14
16
18
20
この例では、GenerateNumbersAsync
メソッドが非同期に数値を生成し、Where
で偶数だけを抽出しています。
await foreach
で非同期に結果を受け取りながら順次表示しています。
IAsyncEnumerable<T>
はデータの逐次取得が必要なAPI呼び出しやファイル読み込み、ネットワーク通信などの非同期処理に適しており、UIの応答性を保ちながら大量データを処理できます。
PLINQとIAsyncEnumerable<T>
はそれぞれ並列処理と非同期処理の手法として強力です。
用途やシナリオに応じて使い分けることで、C#のデータ抽出処理をより効率的かつ柔軟に実装できます。
よくある落とし穴と対策
期待しない実行順序の回避
LINQのWhere
メソッドをはじめとする多くのLINQメソッドは遅延実行を採用しているため、クエリの実行タイミングや順序に注意が必要です。
特に、複数のクエリを連結したり、同じクエリを複数回列挙したりすると、期待しない順序や副作用が発生することがあります。
例えば、以下のコードではWhere
の条件に副作用のあるメソッドを使っているため、列挙のたびに副作用が発生し、実行順序が予測しにくくなります。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static int counter = 0;
static bool Condition(int x)
{
counter++;
Console.WriteLine($"Condition called {counter} times for value {x}");
return x % 2 == 0;
}
static void Main()
{
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = numbers.Where(Condition);
Console.WriteLine("1回目の列挙:");
foreach (var num in query)
{
Console.WriteLine(num);
}
Console.WriteLine("2回目の列挙:");
foreach (var num in query)
{
Console.WriteLine(num);
}
}
}
Condition called 1 times for value 1
Condition called 2 times for value 2
2
Condition called 3 times for value 3
Condition called 4 times for value 4
4
Condition called 5 times for value 5
1回目の列挙:
2
4
Condition called 6 times for value 1
Condition called 7 times for value 2
2
Condition called 8 times for value 3
Condition called 9 times for value 4
4
Condition called 10 times for value 5
2回目の列挙:
2
4
このように、query
を2回列挙するとCondition
が2回ずつ呼ばれ、処理が重複しています。
副作用のある処理は予期せぬ動作やパフォーマンス低下の原因になるため注意が必要です。
対策としては、クエリの結果を一度ToList()
やToArray()
で即時実行し、結果をキャッシュしてから複数回利用する方法があります。
var cached = query.ToList();
foreach (var num in cached) { /* 処理 */ }
foreach (var num in cached) { /* 処理 */ }
これにより、条件判定は一度だけ行われ、実行順序や副作用の問題を回避できます。
例外発生時のクリーンアップ手法
LINQのクエリ実行中に例外が発生すると、処理が中断されるだけでなく、リソースの解放や後処理が適切に行われないことがあります。
特に、ファイルやデータベース接続などの外部リソースを扱う場合は、例外発生時のクリーンアップが重要です。
例えば、IEnumerable<T>
を返すメソッド内でファイルを読み込みつつLINQでフィルタリングする場合、例外が発生するとファイルが閉じられない恐れがあります。
対策としては、using
文やtry-finally
ブロックでリソースを確実に解放することが基本です。
また、LINQの遅延実行を考慮し、クエリの実行(列挙)をusing
のスコープ内で行うことが重要です。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
class Program
{
static IEnumerable<string> ReadLines(string path)
{
using var reader = new StreamReader(path);
string line;
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
static void Main()
{
var path = "sample.txt";
try
{
// クエリの実行はusingスコープ内で行う
var lines = ReadLines(path).Where(line => line.Contains("keyword"));
foreach (var line in lines)
{
Console.WriteLine(line);
}
}
catch (IOException ex)
{
Console.WriteLine($"ファイル読み込みエラー: {ex.Message}");
}
}
}
この例では、ReadLines
メソッド内でusing
を使いファイルを開閉していますが、遅延実行のためファイルはforeach
の列挙時に読み込まれます。
したがって、foreach
はusing
のスコープ内で実行される必要があります。
また、例外発生時にリソースを確実に解放するため、try-catch
でエラーハンドリングを行い、必要に応じてログ出力やリトライ処理を実装しましょう。
可読性を保つラムダ式の分割
LINQの条件式はラムダ式で記述しますが、複雑な条件を一つのラムダ式に詰め込むと可読性が低下し、バグの温床になることがあります。
特に複数の論理演算子やネストした条件がある場合は、適切に分割してコードを整理することが重要です。
例えば、以下のように複雑な条件を一行で書くと読みにくくなります。
var filtered = items.Where(x => x.IsActive && (x.Score > 50 || x.Level >= 10) && x.Name.StartsWith("A"));
これを分割して可読性を高める方法の一つは、条件をローカル関数やメソッドに切り出すことです。
bool IsValid(Item x)
{
return x.IsActive && (x.Score > 50 || x.Level >= 10) && x.Name.StartsWith("A");
}
var filtered = items.Where(IsValid);
または、ラムダ式内で複数行に分けて書くことも可能です。
var filtered = items.Where(x =>
{
bool condition1 = x.IsActive;
bool condition2 = x.Score > 50 || x.Level >= 10;
bool condition3 = x.Name.StartsWith("A");
return condition1 && condition2 && condition3;
});
このように分割することで、各条件の意味が明確になり、将来的な修正やデバッグが容易になります。
さらに、条件式が複数箇所で使われる場合は、外部メソッドやFunc<T,bool>
デリゲートとして再利用可能にしておくと保守性が向上します。
これらのポイントを意識してLINQを使うことで、予期せぬ動作や例外のトラブルを防ぎ、読みやすく保守しやすいコードを書くことができます。
実務サンプルケース
JSONデータのフィルタリング
実務では、APIから取得したJSONデータをC#のオブジェクトにデシリアライズし、LINQで条件抽出するケースが多くあります。
System.Text.Json
やNewtonsoft.Json
などのライブラリを使い、JSONをクラスに変換した後、Where
メソッドでフィルタリングを行います。
以下は、Newtonsoft.Json
を使ってJSON文字列をデシリアライズし、特定の条件でフィルタリングする例です。
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
class Product
{
public string Name { get; set; }
public int Price { get; set; }
public bool InStock { get; set; }
}
class Program
{
static void Main()
{
string json = @"
[
{ 'Name': 'ペン', 'Price': 150, 'InStock': true },
{ 'Name': 'ノート', 'Price': 300, 'InStock': false },
{ 'Name': '消しゴム', 'Price': 100, 'InStock': true }
]";
var products = JsonConvert.DeserializeObject<List<Product>>(json);
// 在庫ありかつ価格が200円以下の商品を抽出
var filtered = products.Where(p => p.InStock && p.Price <= 200);
Console.WriteLine("在庫ありかつ価格200円以下の商品:");
foreach (var p in filtered)
{
Console.WriteLine($"{p.Name} - {p.Price}円");
}
}
}
在庫ありかつ価格200円以下の商品:
ペン - 150円
消しゴム - 100円
このように、JSONデータをオブジェクト化してからLINQで条件抽出することで、柔軟かつ直感的にデータ操作が可能です。
データベース取得結果への二次絞り込み
ORM(Object-Relational Mapping)を使ってデータベースから取得した結果に対して、さらにLINQで二次的な絞り込みを行うこともよくあります。
Entity Frameworkなどでは、クエリは遅延実行されるため、必要な条件を追加してから実行するのが効率的です。
以下は、Entity Framework Coreでデータベースからユーザーを取得し、メモリ上でさらに条件を追加して絞り込む例です。
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
class User
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
}
class AppDbContext : DbContext
{
public DbSet<User> Users { get; set; }
// DbContextの設定は省略
}
class Program
{
static void Main()
{
using var context = new AppDbContext();
// データベースからアクティブユーザーを取得(SQLで絞り込み)
var activeUsersQuery = context.Users.Where(u => u.IsActive);
// メモリ上で名前に「田中」を含むユーザーに絞り込み
var filteredUsers = activeUsersQuery.AsEnumerable()
.Where(u => u.Name.Contains("田中"));
foreach (var user in filteredUsers)
{
Console.WriteLine($"{user.Id}: {user.Name}");
}
}
}
この例では、Where
句でまずデータベース側でIsActive
がtrue
のユーザーを絞り込み、AsEnumerable()
でクエリを実行してメモリ上に取得した後、さらに名前に「田中」を含むユーザーをLINQで絞り込んでいます。
パフォーマンスを考慮すると、可能な限りSQL側で条件を指定するのが望ましいですが、複雑な条件やメモリ上での処理が必要な場合は二次絞り込みが有効です。
CSV読み込み後の条件抽出
CSVファイルを読み込んでデータを解析し、LINQで条件抽出するケースも多いです。
System.IO
のFile.ReadLines
やFile.ReadAllLines
でCSVを読み込み、Split
で分割してオブジェクト化し、Where
で絞り込みを行います。
以下は、簡単なCSVファイルを読み込み、価格が100円以上の商品を抽出する例です。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
class Product
{
public string Name { get; set; }
public int Price { get; set; }
}
class Program
{
static void Main()
{
string path = "products.csv";
// CSV例:
// ペン,150
// ノート,80
// 消しゴム,120
var products = File.ReadLines(path)
.Select(line =>
{
var parts = line.Split(',');
return new Product
{
Name = parts[0],
Price = int.Parse(parts[1])
};
});
var filtered = products.Where(p => p.Price >= 100);
Console.WriteLine("価格100円以上の商品:");
foreach (var p in filtered)
{
Console.WriteLine($"{p.Name} - {p.Price}円");
}
}
}
価格100円以上の商品:
ペン - 150円
消しゴム - 120円
この方法は小規模なCSVファイルに適しており、より大規模なファイルや複雑なCSVには専用ライブラリ(CsvHelperなど)の利用を検討してください。
条件式の簡潔さと保守性のバランス
実務でのLINQ条件式は、簡潔に書くことと保守性を両立させることが重要です。
複雑な条件を一行で書きすぎると可読性が落ち、将来的な修正やバグの原因になります。
一方で、冗長すぎるとコードが長くなりすぎて管理が難しくなります。
以下のポイントを意識するとよいでしょう。
- 条件をメソッドやローカル関数に切り出す
複数の場所で使う条件や複雑な条件は名前付きメソッドにまとめると再利用しやすくなります。
- 匿名型や変数で中間結果を保持する
複雑な条件の一部を変数に分割して意味を明確にすることで、読みやすくなります。
- コメントで条件の意図を補足する
特にビジネスロジックに関わる条件は、なぜその条件が必要かをコメントで説明すると保守性が向上します。
bool IsEligible(Product p)
{
// 在庫ありかつ価格が適正範囲内の商品
return p.InStock && p.Price >= 100 && p.Price <= 1000;
}
var filtered = products.Where(IsEligible);
このように、条件式の簡潔さと保守性のバランスを取りながらコードを書くことが実務では求められます。
パフォーマンス対策の優先順位
実務でLINQを使う際のパフォーマンス対策は、以下の優先順位で検討すると効率的です。
- SQLやAPIなどデータ取得元での絞り込み
可能な限りデータベースやAPIのクエリで条件を指定し、不要なデータを取得しない。
- 遅延実行の特性を活かす
必要なタイミングでクエリを実行し、不要な処理を避けます。
- クエリの合成と一括評価
複数の条件をまとめて一度に評価し、中間コレクションの生成を減らす。
- 不要な
ToList()
やToArray()
の多用を避ける
メモリ消費や処理時間の無駄を防ぐ。
- 必要に応じて並列処理(PLINQ)を検討
大量データやCPU負荷の高い処理に対して効果的。
- プロファイリングとボトルネックの特定
実際のパフォーマンスを測定し、問題箇所を特定して最適化。
これらを踏まえ、まずは正確で読みやすいコードを書くことを優先し、必要に応じてパフォーマンス改善を行うのが実務でのベストプラクティスです。
まとめ
この記事では、C#のLINQを使った多彩なデータ抽出方法を解説しました。
基本的なWhere
メソッドの使い方から、複数条件の組み合わせ、インデックス利用、Null安全対策、文字列や日付の特化処理、複合オブジェクトの条件抽出まで幅広く紹介しています。
さらに、パフォーマンス最適化や動的条件生成、並列・非同期処理、実務での具体例やよくある落とし穴の対策も網羅。
これにより、LINQを効果的かつ安全に活用し、保守性と効率性を両立したコードを書くための知識が身につきます。