【C#】LINQでわかるデータ操作テクニック総まとめ
LINQを使うと、コレクションやDB、XMLなど異なるデータ源へも同一のクエリ構文でアクセスでき、Where
やSelect
などの拡張メソッドを連鎖させてフィルタ・変換・集計が簡潔になります。
遅延評価により不要な処理を避けつつ、ToList
等で即時実行も選べ、ラムダ式と組み合わせて可読性と保守性が向上します。
ただし大規模データではメモリ消費と実行タイミングに注意が必要です。
LINQの基礎知識
LINQ(Language Integrated Query)は、C#に組み込まれたデータ操作のための強力な機能です。
配列やリスト、データベース、XMLなど、さまざまなデータソースに対して統一的な方法でクエリを記述できるため、コードの可読性や保守性が大幅に向上します。
ここではLINQの基礎となる概念を丁寧に解説いたします。
.NET標準クエリ演算子とは
.NET標準クエリ演算子は、LINQの中核をなすメソッド群で、System.Linq
名前空間に定義されています。
これらの演算子は、IEnumerable<T>やIQueryable<T>に対して適用でき、データのフィルタリング、並べ替え、投影、結合、集計など多彩な操作を可能にします。
代表的な標準クエリ演算子には以下のようなものがあります。
演算子名 | 機能説明 |
---|---|
Where | 条件に合致する要素を抽出する |
Select | 要素を別の形に変換する |
OrderBy | 昇順で並べ替える |
OrderByDescending | 降順で並べ替える |
GroupBy | キーに基づいて要素をグループ化する |
Join | 2つのシーケンスを結合する |
Aggregate | カスタム集計を行う |
Count | 要素の数を数える |
Sum | 要素の合計を計算する |
Any | 条件に合致する要素が存在するか判定する |
First | 条件に合致する最初の要素を取得する |
これらの演算子はメソッドチェーンとして連結でき、複雑なデータ操作も簡潔に記述できます。
例えば、リストから偶数だけを抽出し、昇順に並べ替えて2倍に変換する場合は以下のように書けます。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new List<int> { 1, 3, 8, 27, 4, 6 };
var query = numbers
.Where(x => x % 2 == 0) // 偶数を抽出
.OrderBy(x => x) // 昇順に並べ替え
.Select(x => x * 2); // 2倍に変換
foreach (var num in query)
{
Console.WriteLine(num);
}
}
}
8
12
16
このように.NET標準クエリ演算子は、データ操作の基本的なビルディングブロックとして機能します。
IEnumerableとIQueryableの違い
LINQを使う際に重要なインターフェースとしてIEnumerable<T>
とIQueryable<T>
があります。
どちらもシーケンス(コレクション)を表しますが、用途や動作に違いがあります。
特徴 | IEnumerable<T> | IQueryable<T> |
---|---|---|
名前空間 | System.Collections.Generic | System.Linq |
実行場所 | クライアント側(メモリ上) | データソース側(例:データベース) |
遅延実行 | はい | はい |
クエリの翻訳 | なし(LINQ to Objects) | クエリ式を式ツリーに変換し翻訳可能 |
主な用途 | メモリ内コレクションの操作 | リモートデータソースへのクエリ発行 |
パフォーマンス | 小規模データに適している | 大規模データやDBアクセスに適している |
IEnumerable<T>
は、メモリ上のコレクションに対してLINQを適用する際に使います。
例えば、配列やList<T>などが該当します。
LINQのクエリはC#のコードとして実行され、すべての要素を列挙しながら処理します。
一方、IQueryable<T>
は、LINQ to SQLやEntity Frameworkのように、データベースなどの外部データソースに対してクエリを発行する際に使います。
クエリは式ツリーとして表現され、SQLなどのネイティブクエリに変換されてデータベース側で実行されます。
これにより、必要なデータだけを効率的に取得できます。
以下は簡単な例です。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// IEnumerableの例(メモリ内コレクション)
IEnumerable<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
Console.WriteLine("IEnumerableで偶数を抽出:");
foreach (var n in evenNumbers)
{
Console.WriteLine(n);
}
// IQueryableの例(通常はORMなどで使用)
// ここでは簡単のためIQueryableに変換してみるだけ
IQueryable<int> queryableNumbers = numbers.AsQueryable();
var queryableEven = queryableNumbers.Where(n => n % 2 == 0);
Console.WriteLine("IQueryableで偶数を抽出:");
foreach (var n in queryableEven)
{
Console.WriteLine(n);
}
}
}
IEnumerableで偶数を抽出:
2
4
IQueryableで偶数を抽出:
2
4
実際のデータベースアクセスでは、IQueryable
のクエリはSQLに変換されてデータベース側で実行されるため、パフォーマンス面で大きなメリットがあります。
クエリ構文とメソッド構文の比較
LINQには2つの記述スタイルがあります。
1つはSQLに似た「クエリ構文」、もう1つはメソッドチェーンを使う「メソッド構文」です。
どちらも同じ処理を実現できますが、使い分けることでコードの可読性や柔軟性が変わります。
クエリ構文
クエリ構文は、from
、where
、select
などのキーワードを使い、SQLのような見た目でクエリを記述します。
初心者にもわかりやすく、複雑な結合やグループ化も直感的に書けます。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new List<int> { 1, 3, 8, 27, 4, 6 };
var query = from x in numbers
where x % 2 == 0
orderby x
select x * 2;
foreach (var num in query)
{
Console.WriteLine(num);
}
}
}
8
12
16
メソッド構文
メソッド構文は、Where
、OrderBy
、Select
などの拡張メソッドを連結して書きます。
ラムダ式を使うため柔軟で、メソッドチェーンの途中で条件分岐やカスタム処理を挟みやすい特徴があります。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new List<int> { 1, 3, 8, 27, 4, 6 };
var query = numbers
.Where(x => x % 2 == 0)
.OrderBy(x => x)
.Select(x => x * 2);
foreach (var num in query)
{
Console.WriteLine(num);
}
}
}
8
12
16
使い分けのポイント
- クエリ構文はSQLに慣れている方や、複雑な結合・グループ化をわかりやすく書きたい場合に適しています
- メソッド構文はラムダ式の柔軟性を活かしたい場合や、動的にクエリを組み立てる場合に便利です
実際には両者を混在させることも可能で、クエリ構文の中でメソッド構文を使うこともできます。
デリゲートとラムダ式の役割
LINQのメソッド構文では、条件や変換処理を指定するためにデリゲートやラムダ式を多用します。
これらは関数を変数のように扱う仕組みで、LINQの柔軟性を支える重要な要素です。
デリゲートとは
デリゲートは、メソッドの参照を保持できる型で、メソッドを引数として渡したり、変数に代入したりできます。
LINQの標準クエリ演算子は、条件や変換のロジックをデリゲートとして受け取ります。
例えば、Where
メソッドはFunc<T, bool>
型のデリゲートを受け取り、条件に合う要素だけを抽出します。
Func<int, bool> isEven = delegate(int x) { return x % 2 == 0; };
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(isEven);
foreach (var n in evenNumbers)
{
Console.WriteLine(n);
}
2
4
ラムダ式とは
ラムダ式は匿名関数の一種で、より簡潔にデリゲートを記述できます。
上記の例は以下のように書き換えられます。
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(x => x % 2 == 0);
foreach (var n in evenNumbers)
{
Console.WriteLine(n);
}
ラムダ式は引数リストの後に=>
を置き、その右側に処理内容を記述します。
引数が1つの場合は括弧を省略でき、処理が単一式なら波括弧も省略可能です。
デリゲートとラムダ式の関係
- デリゲートは型として存在し、メソッドの参照を保持します
- ラムダ式は匿名関数の記法で、デリゲート型に暗黙的に変換されます
LINQのメソッドは、Func<T, TResult>
やFunc<T, bool>
などのデリゲートを引数に取るため、ラムダ式を使うことで簡潔に条件や変換処理を指定できます。
デリゲートとラムダ式はLINQの柔軟なクエリ記述を支える基盤です。
ラムダ式を使いこなすことで、複雑な条件や変換もシンプルに表現でき、コードの可読性が向上します。
LINQを使う際は、これらの仕組みを理解しておくことが重要です。
主要データソース別LINQ
LINQは多様なデータソースに対して統一的にクエリを記述できる点が大きな魅力です。
ここでは代表的なデータソースごとにLINQの使い方やポイントを具体的に解説いたします。
配列とList
配列やList<T>
はLINQの基本的な対象であり、IEnumerable<T>
を実装しているため、LINQの標準クエリ演算子をそのまま使えます。
例えば、整数の配列から偶数だけを抽出し、昇順に並べ替える例です。
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 5, 2, 9, 1, 6, 4 };
var evenSorted = numbers
.Where(n => n % 2 == 0)
.OrderBy(n => n);
foreach (var num in evenSorted)
{
Console.WriteLine(num);
}
}
}
2
4
6
List<T>
でも同様に使えます。
LINQは遅延実行のため、クエリを定義した時点では処理は行われず、列挙時に実行されます。
DictionaryとLookup
Dictionary<TKey, TValue>
はキーと値のペアを保持するコレクションで、LINQでの操作も可能ですが、Dictionary
自体はIEnumerable<KeyValuePair<TKey, TValue>>
を実装しているため、キーや値にアクセスする際はKey
やValue
プロパティを使います。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var dict = new Dictionary<string, int>
{
["apple"] = 3,
["banana"] = 5,
["orange"] = 2
};
// 値が3以上の要素を抽出
var filtered = dict.Where(kv => kv.Value >= 3);
foreach (var kv in filtered)
{
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
}
}
apple: 3
banana: 5
一方、Lookup<TKey, TElement>
はキーに対して複数の要素をグループ化したコレクションで、GroupBy
の結果として得られます。
Lookup
は読み取り専用で、キーごとに複数の値を効率的に取得できます。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var fruits = new[]
{
new { Name = "apple", Color = "red" },
new { Name = "banana", Color = "yellow" },
new { Name = "cherry", Color = "red" },
new { Name = "lemon", Color = "yellow" }
};
var lookup = fruits.ToLookup(f => f.Color);
foreach (var group in lookup)
{
Console.WriteLine($"Color: {group.Key}");
foreach (var fruit in group)
{
Console.WriteLine($" {fruit.Name}");
}
}
}
}
Color: red
apple
cherry
Color: yellow
banana
lemon
Lookup
はキーでグループ化されたデータを高速に検索したい場合に便利です。
DataTableとDataSet
DataTable
やDataSet
はADO.NETの代表的なデータ構造で、データベースから取得した表形式のデータを扱います。
LINQ to DataSetを使うことで、これらのデータに対してLINQクエリを記述できます。
DataTable
の行はDataRowCollection
として保持されており、AsEnumerable()
拡張メソッドを使うことでLINQのIEnumerable<DataRow>
に変換できます。
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main()
{
var table = new DataTable();
table.Columns.Add("Id", typeof(int));
table.Columns.Add("Name", typeof(string));
table.Columns.Add("Age", typeof(int));
table.Rows.Add(1, "Alice", 30);
table.Rows.Add(2, "Bob", 25);
table.Rows.Add(3, "Charlie", 35);
var query = table.AsEnumerable()
.Where(row => row.Field<int>("Age") > 28)
.OrderBy(row => row.Field<string>("Name"))
.Select(row => new
{
Id = row.Field<int>("Id"),
Name = row.Field<string>("Name"),
Age = row.Field<int>("Age")
});
foreach (var item in query)
{
Console.WriteLine($"{item.Id}: {item.Name} ({item.Age})");
}
}
}
1: Alice (30)
3: Charlie (35)
DataSet
は複数のDataTable
を保持でき、テーブル間のリレーションも管理可能です。
LINQで複数テーブルを結合する場合は、DataTable.AsEnumerable()
を使ってそれぞれのテーブルをLINQで操作し、Join
などの演算子で結合します。
XMLとXDocument
LINQ to XMLは、XMLドキュメントをオブジェクトとして扱い、LINQで簡単に検索や編集ができる機能です。
XDocument
やXElement
を使ってXMLを読み込み、要素や属性をクエリできます。
using System;
using System.Linq;
using System.Xml.Linq;
class Program
{
static void Main()
{
string xml = @"
<Books>
<Book Id='1' Title='C#入門' Price='3000' />
<Book Id='2' Title='LINQ実践' Price='3500' />
<Book Id='3' Title='ASP.NET Core' Price='4000' />
</Books>";
var doc = XDocument.Parse(xml);
var expensiveBooks = doc.Descendants("Book")
.Where(b => (int)b.Attribute("Price") > 3200)
.Select(b => new
{
Id = (int)b.Attribute("Id"),
Title = (string)b.Attribute("Title"),
Price = (int)b.Attribute("Price")
});
foreach (var book in expensiveBooks)
{
Console.WriteLine($"{book.Id}: {book.Title} - {book.Price}円");
}
}
}
2: LINQ実践 - 3500円
3: ASP.NET Core - 4000円
XDocument
はXMLの読み書きに便利で、LINQの柔軟なクエリ機能と組み合わせて使うことで、複雑なXML構造も簡単に操作できます。
JSONと匿名型
JSONデータは.NET標準のSystem.Text.Json
やNewtonsoft.Json
などのライブラリでパースし、LINQで操作可能なオブジェクトに変換できます。
匿名型を使うことで、必要なプロパティだけを抽出して扱いやすくできます。
以下はSystem.Text.Json
を使った例です。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
class Program
{
static void Main()
{
string json = @"
[
{ ""Id"": 1, ""Name"": ""Alice"", ""Age"": 30 },
{ ""Id"": 2, ""Name"": ""Bob"", ""Age"": 25 },
{ ""Id"": 3, ""Name"": ""Charlie"", ""Age"": 35 }
]";
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var people = JsonSerializer.Deserialize<List<Person>>(json, options);
var adults = people
.Where(p => p.Age >= 30)
.Select(p => new { p.Name, p.Age });
foreach (var person in adults)
{
Console.WriteLine($"{person.Name} ({person.Age}歳)");
}
}
class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
}
Alice (30歳)
Charlie (35歳)
匿名型を使うことで、必要な情報だけを抽出し、型安全に扱えます。
JSONの動的な構造に対してはJsonDocument
やJObject
を使い、LINQでクエリをかける方法もあります。
Entity FrameworkとLINQ to Entities
Entity Framework(EF)はORM(Object-Relational Mapper)で、データベースのテーブルをC#のクラスとして扱い、LINQ to Entitiesを使ってデータベースクエリを記述できます。
Entity Framework
は、Nugetからインストールする必要があります。
「Entity Framework」と検索してインストールするようにしてください。

dotnet add package Microsoft.EntityFrameworkCore
また、下記のサンプルではIn-Memory データベースを使用しているため、Microsoft.EntityFrameworkCore.InMemoryのインストールも必要です。
dotnet add package Microsoft.EntityFrameworkCore.InMemory
EFはIQueryable<T>
を返すため、LINQのクエリはSQLに変換されてデータベース側で実行されます。
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
class Program
{
static void Main()
{
using var context = new SampleContext();
// データをシード
if (!context.Products.Any())
{
context.Products.AddRange(
new Product { Name = "ProductA", Price = 1500 },
new Product { Name = "ProductB", Price = 2000 },
new Product { Name = "ProductC", Price = 800 }
);
context.SaveChanges();
}
// 価格が1000円を超える商品を名前順に取得
var expensiveProducts = context.Products
.Where(p => p.Price > 1000)
.OrderBy(p => p.Name)
.Select(p => new { p.Name, p.Price });
// 結果を出力
foreach (var product in expensiveProducts)
{
Console.WriteLine($"{product.Name}: {product.Price}円");
}
}
}
public class SampleContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseInMemoryDatabase("SampleDB");
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public int Price { get; set; }
}
ProductA: 1500円
ProductB: 2000円
EFはLINQのクエリをSQLに変換し、必要なデータだけを効率的に取得します。
Include
メソッドで関連エンティティの読み込みも可能です。
Dapper・Micro ORMとの併用
Dapperは軽量なMicro ORMで、SQLを直接記述しつつ、結果をC#オブジェクトにマッピングします。
Dapper自体はLINQを提供しませんが、取得したコレクションに対してLINQを使うことが一般的です。
using System;
using System.Data.SQLite;
using System.Linq;
using Dapper;
using System.Collections.Generic;
class Program
{
static void Main()
{
using var connection = new SQLiteConnection("Data Source=:memory:");
connection.Open();
connection.Execute("CREATE TABLE Users (Id INTEGER PRIMARY KEY, Name TEXT, Age INTEGER)");
connection.Execute("INSERT INTO Users (Name, Age) VALUES ('Alice', 30), ('Bob', 25), ('Charlie', 35)");
var users = connection.Query<User>("SELECT * FROM Users").ToList();
var adults = users.Where(u => u.Age >= 30);
foreach (var user in adults)
{
Console.WriteLine($"{user.Name} ({user.Age}歳)");
}
}
}
class User
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
Alice (30歳)
Charlie (35歳)
DapperはSQLの自由度が高く、パフォーマンスも優れています。
LINQはDapperで取得したデータの後処理やフィルタリングに活用されることが多いです。
Micro ORMの中にはLINQクエリをサポートするものもありますが、DapperはシンプルにSQLとLINQを組み合わせるスタイルが主流です。
基本操作
LINQの基本操作は、データの抽出や変換、並べ替え、グループ化、集合演算、結合、集計など多岐にわたります。
ここでは代表的な操作を具体的なコード例とともに詳しく解説いたします。
フィルタリング
Whereの使い方
Where
は条件に合致する要素だけを抽出するための演算子です。
引数に条件を表すラムダ式を渡します。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 10);
// 偶数だけ抽出
var evens = numbers.Where(n => n % 2 == 0);
foreach (var n in evens)
{
Console.WriteLine(n);
}
}
}
2
4
6
8
10
Where
は遅延実行されるため、列挙時に条件が評価されます。
インデックス付きWhere
Where
には要素の値だけでなく、インデックスも受け取るオーバーロードがあります。
これを使うと、要素の位置に基づく条件も指定可能です。
using System;
using System.Linq;
class Program
{
static void Main()
{
var fruits = new[] { "apple", "banana", "cherry", "date" };
// インデックスが偶数の要素だけ抽出
var filtered = fruits.Where((fruit, index) => index % 2 == 0);
foreach (var fruit in filtered)
{
Console.WriteLine(fruit);
}
}
}
apple
cherry
投影
Selectと匿名型
Select
は要素を別の形に変換する演算子です。
匿名型と組み合わせることで、必要なプロパティだけを抽出したり、新しいオブジェクトを作成したりできます。
using System;
using System.Linq;
class Program
{
static void Main()
{
var people = new[]
{
new { Name = "Alice", Age = 30, City = "Tokyo" },
new { Name = "Bob", Age = 25, City = "Osaka" },
new { Name = "Charlie", Age = 35, City = "Nagoya" }
};
var namesAndAges = people.Select(p => new { p.Name, p.Age });
foreach (var item in namesAndAges)
{
Console.WriteLine($"{item.Name} ({item.Age}歳)");
}
}
}
Alice (30歳)
Bob (25歳)
Charlie (35歳)
SelectManyによるフラット化
SelectMany
は、各要素から複数の要素を生成し、それらを一つのシーケンスに平坦化します。
ネストされたコレクションを扱う際に便利です。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var students = new[]
{
new { Name = "Alice", Scores = new[] { 80, 90 } },
new { Name = "Bob", Scores = new[] { 70, 85, 88 } },
new { Name = "Charlie", Scores = new[] { 95 } }
};
// 全てのスコアを一つのシーケンスにまとめる
var allScores = students.SelectMany(s => s.Scores);
foreach (var score in allScores)
{
Console.WriteLine(score);
}
}
}
80
90
70
85
88
95
並べ替え
OrderByとOrderByDescending
OrderBy
は昇順、OrderByDescending
は降順で要素を並べ替えます。
キーを指定するラムダ式を渡します。
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new[] { 5, 2, 9, 1, 6 };
var ascending = numbers.OrderBy(n => n);
var descending = numbers.OrderByDescending(n => n);
Console.WriteLine("昇順:");
foreach (var n in ascending) Console.WriteLine(n);
Console.WriteLine("降順:");
foreach (var n in descending) Console.WriteLine(n);
}
}
昇順:
1
2
5
6
9
降順:
9
6
5
2
1
複合キーによる並べ替え
複数のキーで並べ替えたい場合は、ThenBy
やThenByDescending
を使います。
using System;
using System.Linq;
class Program
{
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string City { get; set; }
}
static void Main()
{
var people = new[]
{
new Person { Name = "Alice", Age = 30, City = "Tokyo" },
new Person { Name = "Bob", Age = 25, City = "Osaka" },
new Person { Name = "Charlie", Age = 30, City = "Osaka" },
new Person { Name = "David", Age = 25, City = "Tokyo" }
};
var sorted = people
.OrderBy(p => p.Age)
.ThenBy(p => p.City);
foreach (var p in sorted)
{
Console.WriteLine($"{p.Name}: {p.Age}歳, {p.City}");
}
}
}
Bob: 25歳, Osaka
David: 25歳, Tokyo
Charlie: 30歳, Osaka
Alice: 30歳, Tokyo
グループ化
GroupBy基本形
GroupBy
は指定したキーで要素をグループ化します。
戻り値はキーとグループ化された要素のコレクションです。
using System;
using System.Linq;
class Program
{
static void Main()
{
var words = new[] { "apple", "banana", "apricot", "blueberry", "cherry" };
var groups = words.GroupBy(w => w[0]); // 先頭文字でグループ化
foreach (var group in groups)
{
Console.WriteLine($"Key: {group.Key}");
foreach (var word in group)
{
Console.WriteLine($" {word}");
}
}
}
}
Key: a
apple
apricot
Key: b
banana
blueberry
Key: c
cherry
先頭要素抽出と集計
グループごとに先頭要素を取得したり、集計を行うこともよくあります。
using System;
using System.Linq;
class Program
{
static void Main()
{
var people = new[]
{
new { Name = "Alice", City = "Tokyo", Age = 30 },
new { Name = "Bob", City = "Osaka", Age = 25 },
new { Name = "Charlie", City = "Tokyo", Age = 35 },
new { Name = "David", City = "Osaka", Age = 40 }
};
var grouped = people.GroupBy(p => p.City)
.Select(g => new
{
City = g.Key,
Count = g.Count(),
Oldest = g.OrderByDescending(p => p.Age).First()
});
foreach (var group in grouped)
{
Console.WriteLine($"{group.City}: {group.Count}人, 最年長は{group.Oldest.Name}({group.Oldest.Age}歳)");
}
}
}
Tokyo: 2人, 最年長はCharlie(35歳)
Osaka: 2人, 最年長はDavid(40歳)
集合演算
Distinctで重複排除
Distinct
はシーケンス内の重複要素を取り除きます。
既定ではEquals
とGetHashCode
で比較します。
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new[] { 1, 2, 2, 3, 4, 4, 5 };
var distinct = numbers.Distinct();
foreach (var n in distinct)
{
Console.WriteLine(n);
}
}
}
1
2
3
4
5
Union・Intersect・Except
これらは集合演算を行います。
Union
: 2つのシーケンスの和集合(重複なし)Intersect
: 2つのシーケンスの共通部分Except
: 1つ目のシーケンスから2つ目のシーケンスの要素を除外
using System;
using System.Linq;
class Program
{
static void Main()
{
var set1 = new[] { 1, 2, 3, 4 };
var set2 = new[] { 3, 4, 5, 6 };
var union = set1.Union(set2);
var intersect = set1.Intersect(set2);
var except = set1.Except(set2);
Console.WriteLine("Union:");
foreach (var n in union) Console.WriteLine(n);
Console.WriteLine("Intersect:");
foreach (var n in intersect) Console.WriteLine(n);
Console.WriteLine("Except:");
foreach (var n in except) Console.WriteLine(n);
}
}
Union:
1
2
3
4
5
6
Intersect:
3
4
Except:
1
2
結合
内部結合Join
Join
は2つのシーケンスをキーで結合し、共通のキーを持つ要素の組み合わせを生成します。
using System;
using System.Linq;
class Program
{
class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
class Department
{
public int EmployeeId { get; set; }
public string DeptName { get; set; }
}
static void Main()
{
var employees = new[]
{
new Employee { Id = 1, Name = "Alice" },
new Employee { Id = 2, Name = "Bob" },
new Employee { Id = 3, Name = "Charlie" }
};
var departments = new[]
{
new Department { EmployeeId = 1, DeptName = "HR" },
new Department { EmployeeId = 2, DeptName = "IT" }
};
var query = employees.Join(
departments,
e => e.Id,
d => d.EmployeeId,
(e, d) => new { e.Name, d.DeptName }
);
foreach (var item in query)
{
Console.WriteLine($"{item.Name}: {item.DeptName}");
}
}
}
Alice: HR
Bob: IT
左外部結合GroupJoinとDefaultIfEmpty
GroupJoin
は左外部結合を実現し、結合先がない場合は空のコレクションになります。
DefaultIfEmpty
を使うと、結合先がない場合にデフォルト値を返せます。
using System;
using System.Linq;
class Program
{
class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
class Department
{
public int EmployeeId { get; set; }
public string DeptName { get; set; }
}
static void Main()
{
var employees = new[]
{
new Employee { Id = 1, Name = "Alice" },
new Employee { Id = 2, Name = "Bob" },
new Employee { Id = 3, Name = "Charlie" }
};
var departments = new[]
{
new Department { EmployeeId = 1, DeptName = "HR" },
new Department { EmployeeId = 2, DeptName = "IT" }
};
var query = employees.GroupJoin(
departments,
e => e.Id,
d => d.EmployeeId,
(e, ds) => new { e.Name, Depts = ds.DefaultIfEmpty() }
);
foreach (var item in query)
{
foreach (var dept in item.Depts)
{
var deptName = dept?.DeptName ?? "なし";
Console.WriteLine($"{item.Name}: {deptName}");
}
}
}
}
Alice: HR
Bob: IT
Charlie: なし
クロス結合SelectMany
SelectMany
は2つのシーケンスの全組み合わせ(クロス結合)を作成できます。
using System;
using System.Linq;
class Program
{
static void Main()
{
var colors = new[] { "Red", "Green" };
var shapes = new[] { "Circle", "Square" };
var crossJoin = colors.SelectMany(
color => shapes,
(color, shape) => $"{color} {shape}"
);
foreach (var item in crossJoin)
{
Console.WriteLine(item);
}
}
}
Red Circle
Red Square
Green Circle
Green Square
集計
Count・LongCount
Count
は要素数を返します。
LongCount
は要素数が非常に多い場合に64ビット整数で返します。
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 100);
Console.WriteLine($"Count: {numbers.Count()}");
Console.WriteLine($"LongCount: {numbers.LongCount()}");
}
}
Count: 100
LongCount: 100
Sum・Average
Sum
は数値の合計、Average
は平均値を計算します。
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new[] { 1, 2, 3, 4, 5 };
Console.WriteLine($"Sum: {numbers.Sum()}");
Console.WriteLine($"Average: {numbers.Average()}");
}
}
Sum: 15
Average: 3
Min・Max
Min
は最小値、Max
は最大値を返します。
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new[] { 10, 20, 5, 30, 15 };
Console.WriteLine($"Min: {numbers.Min()}");
Console.WriteLine($"Max: {numbers.Max()}");
}
}
Min: 5
Max: 30
カスタム集計Aggregate
Aggregate
はシーケンスの要素を累積的に処理し、カスタム集計を行えます。
例えば、文字列の連結や複雑な計算に使います。
using System;
using System.Linq;
class Program
{
static void Main()
{
var words = new[] { "LINQ", "is", "powerful" };
var sentence = words.Aggregate((acc, word) => acc + " " + word);
Console.WriteLine(sentence);
}
}
LINQ is powerful
Aggregate
は初期値を指定するオーバーロードもあり、より複雑な処理も可能です。
コントロールフロー系演算子
LINQには、データの取得範囲を制限したり、条件に基づいて要素を選択したり、特定の要素を取得するためのコントロールフロー系演算子が用意されています。
ここでは代表的な演算子の使い方や特徴を具体例とともに解説いたします。
取得制限
Take・Skip
Take
はシーケンスの先頭から指定した数だけ要素を取得します。
一方、Skip
は先頭から指定した数の要素をスキップし、それ以降の要素を取得します。
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 10);
var firstThree = numbers.Take(3);
var skipThree = numbers.Skip(3);
Console.WriteLine("Take(3):");
foreach (var n in firstThree)
{
Console.WriteLine(n);
}
Console.WriteLine("Skip(3):");
foreach (var n in skipThree)
{
Console.WriteLine(n);
}
}
}
Take(3):
1
2
3
Skip(3):
4
5
6
7
8
9
10
Take
とSkip
は組み合わせて使うことで、データの一部を抽出したり、ページング処理に活用できます。
ページング実装例
ページングは大量データを分割して表示する際に使います。
Skip
で前のページ分をスキップし、Take
でページサイズ分だけ取得します。
using System;
using System.Linq;
class Program
{
static void Main()
{
var items = Enumerable.Range(1, 50);
int pageSize = 10;
int pageNumber = 3; // 3ページ目(0ベースではなく1ベース)
var pageItems = items
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
Console.WriteLine($"Page {pageNumber}:");
foreach (var item in pageItems)
{
Console.WriteLine(item);
}
}
}
Page 3:
21
22
23
24
25
26
27
28
29
30
このようにSkip
とTake
を組み合わせることで、簡単にページング処理が実装できます。
条件系
TakeWhile・SkipWhile
TakeWhile
は条件が真の間、先頭から要素を取得し続けます。
条件が初めて偽になった時点で取得を停止します。
SkipWhile
は条件が真の間、先頭から要素をスキップし、条件が偽になった以降の要素を取得します。
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new[] { 2, 4, 6, 7, 8, 10 };
var takeWhileEven = numbers.TakeWhile(n => n % 2 == 0);
var skipWhileEven = numbers.SkipWhile(n => n % 2 == 0);
Console.WriteLine("TakeWhile (偶数の間):");
foreach (var n in takeWhileEven)
{
Console.WriteLine(n);
}
Console.WriteLine("SkipWhile (偶数の間スキップ):");
foreach (var n in skipWhileEven)
{
Console.WriteLine(n);
}
}
}
TakeWhile (偶数の間):
2
4
6
SkipWhile (偶数の間スキップ):
7
8
10
TakeWhile
は条件が途切れたら終了するため、途中に条件を満たさない要素があるとそこで止まります。
SkipWhile
は条件が途切れたら以降すべて取得します。
Any・All・Contains
Any
はシーケンスに条件を満たす要素が1つでもあればtrue
を返します。条件を省略すると要素が1つでもあればtrue
ですAll
はすべての要素が条件を満たす場合にtrue
を返しますContains
は指定した値がシーケンスに含まれているか判定します
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new[] { 1, 3, 5, 7, 9 };
bool anyEven = numbers.Any(n => n % 2 == 0);
bool allOdd = numbers.All(n => n % 2 != 0);
bool containsFive = numbers.Contains(5);
Console.WriteLine($"Any even? {anyEven}");
Console.WriteLine($"All odd? {allOdd}");
Console.WriteLine($"Contains 5? {containsFive}");
}
}
Any even? False
All odd? True
Contains 5? True
要素取得
First・FirstOrDefault
First
は条件に合う最初の要素を返します。
条件を省略すると最初の要素を返します。
条件に合う要素がない場合は例外が発生します。
FirstOrDefault
は条件に合う要素がなければ既定値(参照型ならnull
、値型ならdefault
)を返します。
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new[] { 2, 4, 6, 8 };
int firstEven = numbers.First(n => n % 2 == 0);
int firstGreaterThanTen = numbers.FirstOrDefault(n => n > 10);
Console.WriteLine($"First even: {firstEven}");
Console.WriteLine($"First > 10 or default: {firstGreaterThanTen}");
}
}
First even: 2
First > 10 or default: 0
Single・SingleOrDefault
Single
は条件に合う要素がちょうど1つの場合にその要素を返します。
0個または複数ある場合は例外が発生します。
SingleOrDefault
は条件に合う要素が0個の場合は既定値を返し、複数ある場合は例外が発生します。
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new[] { 5 };
int single = numbers.Single();
int singleOrDefault = numbers.SingleOrDefault(n => n > 10);
Console.WriteLine($"Single: {single}");
Console.WriteLine($"SingleOrDefault (条件に合わず): {singleOrDefault}");
}
}
Single: 5
SingleOrDefault (条件に合わず): 0
複数要素がある場合にSingle
を使うと例外になるため、ユニークな要素を取得したい場合に使います。
ElementAt
ElementAt
は指定したインデックスの要素を取得します。
インデックスが範囲外の場合は例外が発生します。
ElementAtOrDefault
は範囲外の場合に既定値を返します。
using System;
using System.Linq;
class Program
{
static void Main()
{
var letters = new[] { "a", "b", "c" };
string second = letters.ElementAt(1);
string fifthOrDefault = letters.ElementAtOrDefault(4);
Console.WriteLine($"ElementAt(1): {second}");
Console.WriteLine($"ElementAtOrDefault(4): {(fifthOrDefault ?? "null")}");
}
}
ElementAt(1): b
ElementAtOrDefault(4): null
例外発生タイミングの違い
First
やSingle
は、条件に合う要素がない場合や複数ある場合に例外をスローしますFirstOrDefault
やSingleOrDefault
は例外をスローせず、既定値を返しますElementAt
はインデックスが範囲外の場合に例外をスローし、ElementAtOrDefault
は既定値を返します
これらの違いを理解し、用途に応じて使い分けることが重要です。
例えば、必ず要素が存在するとわかっている場合はFirst
やSingle
を使い、存在しない可能性がある場合はFirstOrDefault
やSingleOrDefault
を使うと安全です。
生成演算子
LINQには、特定のパターンでシーケンスを生成するための便利な演算子が用意されています。
ここではRange
、Repeat
、Empty
の使い方と、遅延実行されるシーケンスの自作方法について解説いたします。
Range・Repeat・Empty
Range
Enumerable.Range
は、指定した開始値から連続した整数のシーケンスを生成します。
例えば、1から10までの整数を生成する場合は以下のように書きます。
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 10);
foreach (var n in numbers)
{
Console.WriteLine(n);
}
}
}
1
2
3
4
5
6
7
8
9
10
Range
は開始値と要素数を指定し、連続した整数を簡単に作成できるため、テストデータの生成やループの代替として便利です。
Repeat
Enumerable.Repeat
は、指定した値を指定回数繰り返すシーケンスを生成します。
例えば、文字列”Hello”を5回繰り返す場合は以下のように書きます。
using System;
using System.Linq;
class Program
{
static void Main()
{
var repeated = Enumerable.Repeat("Hello", 5);
foreach (var s in repeated)
{
Console.WriteLine(s);
}
}
}
Hello
Hello
Hello
Hello
Hello
Repeat
は同じ値を複数回使いたい場合に便利で、初期化処理やデフォルト値の埋め込みなどに活用できます。
Empty
Enumerable.Empty<T>
は、空のシーケンスを返します。
nullではなく空のシーケンスを返すため、nullチェックを省略できる場面で役立ちます。
using System;
using System.Linq;
class Program
{
static void Main()
{
var empty = Enumerable.Empty<int>();
Console.WriteLine($"Count: {empty.Count()}");
}
}
Count: 0
Empty
はメソッドの戻り値として空のシーケンスを返したい場合や、条件によってシーケンスを返すが要素がない場合の安全な返却値として使います。
遅延系シーケンスの自作
LINQのシーケンスは遅延実行されるため、必要なときに要素を生成するカスタムシーケンスを作成できます。
これにはyield return
を使ったイテレーターメソッドが便利です。
以下は、1から指定した数までの偶数だけを遅延生成する例です。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
foreach (var even in GenerateEvenNumbers(10))
{
Console.WriteLine(even);
}
}
static IEnumerable<int> GenerateEvenNumbers(int max)
{
for (int i = 1; i <= max; i++)
{
if (i % 2 == 0)
{
yield return i; // 遅延生成
}
}
}
}
2
4
6
8
10
yield return
を使うことで、呼び出し元が要素を要求したタイミングで値を1つずつ返すため、メモリ効率が良く、大量データの処理に適しています。
また、無限シーケンスを作ることも可能です。
以下は無限に自然数を生成する例です。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
foreach (var n in NaturalNumbers())
{
if (n > 10) break; // 無限ループ防止
Console.WriteLine(n);
}
}
static IEnumerable<int> NaturalNumbers()
{
int i = 1;
while (true)
{
yield return i++;
}
}
}
1
2
3
4
5
6
7
8
9
10
このように遅延系シーケンスの自作は、LINQの柔軟性を最大限に活かすテクニックです。
yield return
を使うことで、複雑な生成ロジックもシンプルに記述できます。
変換演算子
LINQの変換演算子は、クエリの結果を別のコレクション型に変換したり、要素の型を変換したりするために使います。
ここでは代表的なToList
、ToArray
、ToDictionary
、ToLookup
、および型変換のCast
とOfType
について詳しく解説いたします。
ToList・ToArray
ToList
とToArray
は、LINQの遅延実行されるシーケンスを即時実行し、結果をList<T>
や配列T[]
に変換します。
これにより、結果を複数回列挙したり、インデックスアクセスを高速に行ったりできます。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 5);
// ToListでList<int>に変換
List<int> list = numbers.ToList();
// ToArrayでint[]に変換
int[] array = numbers.ToArray();
Console.WriteLine("Listの要素:");
foreach (var n in list)
{
Console.WriteLine(n);
}
Console.WriteLine("配列の要素:");
foreach (var n in array)
{
Console.WriteLine(n);
}
}
}
Listの要素:
1
2
3
4
5
配列の要素:
1
2
3
4
5
ToList
は可変長のコレクションとして扱いたい場合に便利で、Add
やRemove
などの操作が可能です。
ToArray
は固定長の配列として扱いたい場合に使います。
どちらも即時実行されるため、遅延実行のクエリを一度評価して結果をメモリに保持したいときに使います。
ToDictionary・ToLookup
ToDictionary
ToDictionary
はシーケンスの要素をキーと値のペアに変換し、Dictionary<TKey, TValue>
を生成します。
キーの重複があると例外が発生するため、キーはユニークである必要があります。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
static void Main()
{
var people = new[]
{
new Person { Id = 1, Name = "Alice" },
new Person { Id = 2, Name = "Bob" },
new Person { Id = 3, Name = "Charlie" }
};
Dictionary<int, string> dict = people.ToDictionary(p => p.Id, p => p.Name);
foreach (var kv in dict)
{
Console.WriteLine($"Key: {kv.Key}, Value: {kv.Value}");
}
}
}
Key: 1, Value: Alice
Key: 2, Value: Bob
Key: 3, Value: Charlie
ToDictionary
はキーと値の選択子をラムダ式で指定でき、複雑な変換も可能です。
ToLookup
ToLookup
はキーに対して複数の要素をグループ化したILookup<TKey, TElement>
を生成します。
キーの重複を許容し、グループ化されたコレクションとして扱えます。
using System;
using System.Linq;
class Program
{
class Fruit
{
public string Name { get; set; }
public string Color { get; set; }
}
static void Main()
{
var fruits = new[]
{
new Fruit { Name = "Apple", Color = "Red" },
new Fruit { Name = "Strawberry", Color = "Red" },
new Fruit { Name = "Banana", Color = "Yellow" },
new Fruit { Name = "Lemon", Color = "Yellow" }
};
var lookup = fruits.ToLookup(f => f.Color);
foreach (var group in lookup)
{
Console.WriteLine($"Color: {group.Key}");
foreach (var fruit in group)
{
Console.WriteLine($" {fruit.Name}");
}
}
}
}
Color: Red
Apple
Strawberry
Color: Yellow
Banana
Lemon
ToLookup
はグループ化の結果を保持し、キーで高速にアクセスしたい場合に便利です。
データ型の変換Cast・OfType
Cast
Cast<TResult>
は、非ジェネリックなIEnumerable
(例えばArrayList
やIEnumerable
型)をジェネリックなIEnumerable<TResult>
に変換します。
要素が指定した型にキャスト可能でなければ例外が発生します。
using System;
using System.Collections;
using System.Linq;
class Program
{
static void Main()
{
ArrayList list = new ArrayList { 1, 2, 3, 4 };
var casted = list.Cast<int>();
foreach (var n in casted)
{
Console.WriteLine(n);
}
}
}
1
2
3
4
Cast
は型が確実に合っている場合に使います。
OfType
OfType<TResult>
は、シーケンス内の指定した型の要素だけを抽出し、IEnumerable<TResult>
として返します。
型が合わない要素は無視されます。
using System;
using System.Collections;
using System.Linq;
class Program
{
static void Main()
{
ArrayList list = new ArrayList { 1, "two", 3, "four", 5 };
var ints = list.OfType<int>();
foreach (var n in ints)
{
Console.WriteLine(n);
}
}
}
1
3
5
OfType
は混在したコレクションから特定の型だけを安全に抽出したい場合に便利です。
これらの変換演算子を使いこなすことで、LINQの結果を目的に応じたコレクション型に変換し、柔軟かつ効率的にデータを扱えます。
特にToDictionary
やToLookup
はキーによる高速アクセスやグループ化に役立ちますし、Cast
やOfType
は型安全な操作を支援します。
パフォーマンス最適化
LINQは強力で便利な機能ですが、使い方によってはパフォーマンスに影響を与えることがあります。
ここでは遅延実行と即時実行の違いや、多重列挙の回避、メモリとCPUのトレードオフ、並列処理のPLINQ、そしてキャッシュと再利用のテクニックについて詳しく解説いたします。
遅延実行と即時実行
LINQのクエリは基本的に遅延実行されます。
つまり、クエリを定義しただけでは処理は行われず、実際に列挙(foreachなど)したタイミングで初めて処理が実行されます。
一方、ToList
やToArray
などの変換演算子を使うと即時実行され、結果がメモリに格納されます。
ToListの使いどころ
ToList
は遅延実行のクエリを即時実行し、結果をList<T>
としてメモリに保持します。
これにより、複数回の列挙で同じクエリを繰り返し実行することを防ぎ、パフォーマンスを向上させることができます。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 5);
// 遅延実行のままだとforeachごとに処理が走る
Console.WriteLine("遅延実行:");
foreach (var n in numbers.Where(x => x % 2 == 0))
Console.WriteLine(n);
foreach (var n in numbers.Where(x => x % 2 == 0))
Console.WriteLine(n);
// ToListで即時実行し結果をキャッシュ
var cached = numbers.Where(x => x % 2 == 0).ToList();
Console.WriteLine("ToListでキャッシュ:");
foreach (var n in cached)
Console.WriteLine(n);
foreach (var n in cached)
Console.WriteLine(n);
}
}
遅延実行:
2
4
2
4
ToListでキャッシュ:
2
4
2
4
上記の例では結果は同じですが、遅延実行の場合はWhere
の条件が2回評価されます。
ToList
を使うと1回だけ評価し、以降はメモリ上のリストを使うため効率的です。
ただし、ToList
は結果をすべてメモリに保持するため、大量データの場合はメモリ使用量に注意が必要です。
意図しない多重列挙の回避
遅延実行のクエリを複数回列挙すると、そのたびにクエリが再評価されます。
これが意図しない多重列挙で、パフォーマンス低下や副作用の原因になることがあります。
using System;
using System.Linq;
class Program
{
static int counter = 0;
static void Main()
{
var numbers = Enumerable.Range(1, 3).Select(n =>
{
counter++;
return n;
});
// 2回列挙するとSelectの処理が2回走る
foreach (var n in numbers)
Console.WriteLine(n);
foreach (var n in numbers)
Console.WriteLine(n);
Console.WriteLine($"処理回数: {counter}");
}
}
1
2
3
1
2
3
処理回数: 6
このように、2回列挙したためSelect
の処理が6回実行されています。
これを防ぐにはToList
やToArray
で結果をキャッシュするか、Memoize
パターンを使います。
メモリとCPUのトレードオフ
LINQのパフォーマンス最適化では、メモリ使用量とCPU負荷のバランスを考慮する必要があります。
- 遅延実行は必要な要素だけを処理するためメモリ効率が良いですが、クエリが複数回評価されるとCPU負荷が増えます
- 即時実行(ToListなど)は一度にすべての結果をメモリに保持するため、CPU負荷は減りますがメモリ使用量が増えます
大量データを扱う場合は、必要な部分だけを遅延実行で処理しつつ、複数回使う結果はキャッシュするなど、状況に応じて使い分けることが重要です。
PLINQによる並列化
PLINQ(Parallel LINQ)は、LINQクエリを並列処理に変換し、複数のCPUコアを活用して高速化を図る機能です。
AsParallel
メソッドを使ってシーケンスを並列化します。
AsParallelとスケジューラ
using System;
using System.Linq;
using System.Threading;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 20);
var parallelQuery = numbers.AsParallel()
.Select(n =>
{
Console.WriteLine($"Processing {n} on thread {Thread.CurrentThread.ManagedThreadId}");
return n * n;
});
foreach (var result in parallelQuery)
{
Console.WriteLine(result);
}
}
}
Processing 1 on thread 4
Processing 2 on thread 5
Processing 3 on thread 6
...
1
4
9
...
AsParallel
を使うと、LINQの処理が複数スレッドで並列に実行されます。
PLINQは内部でスケジューラを使い、CPUコア数に応じてスレッドを管理します。
ただし、並列化によるオーバーヘッドやスレッド間の競合が発生するため、必ずしも高速化するとは限りません。
処理内容やデータ量に応じて使い分ける必要があります。
スレッドセーフな書き方
PLINQで並列処理を行う際は、スレッドセーフでない操作を避ける必要があります。
例えば、共有変数への書き込みや非同期I/Oは注意が必要です。
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 1000);
var results = new ConcurrentBag<int>();
numbers.AsParallel().ForAll(n =>
{
// ConcurrentBagはスレッドセーフ
results.Add(n * n);
});
Console.WriteLine($"結果数: {results.Count}");
}
}
結果数: 1000
ConcurrentBag<T>
などのスレッドセーフなコレクションを使うことで、並列処理中のデータ競合を防げます。
キャッシュと再利用
Memoizeパターン
Memoize
は、遅延実行のシーケンスの結果を一度だけ評価し、その後はキャッシュされた結果を返すパターンです。
これにより、多重列挙による無駄な処理を防ぎます。
以下は簡単なMemoize
の実装例です。
using System;
using System.Collections.Generic;
public static class EnumerableExtensions
{
public static IEnumerable<T> Memoize<T>(this IEnumerable<T> source)
{
var cache = new List<T>();
bool fullyEnumerated = false;
foreach (var item in source)
{
cache.Add(item);
yield return item;
}
fullyEnumerated = true;
int index = 0;
while (true)
{
if (index < cache.Count)
{
yield return cache[index++];
}
else if (fullyEnumerated)
{
yield break;
}
}
}
}
class Program
{
static void Main()
{
var numbers = GenerateNumbers();
var memoized = numbers.Memoize();
foreach (var n in memoized)
Console.WriteLine(n);
Console.WriteLine("再列挙:");
foreach (var n in memoized)
Console.WriteLine(n);
}
static IEnumerable<int> GenerateNumbers()
{
Console.WriteLine("生成開始");
for (int i = 1; i <= 3; i++)
{
Console.WriteLine($"生成中: {i}");
yield return i;
}
}
}
生成開始
生成中: 1
1
生成中: 2
2
生成中: 3
3
再列挙:
1
2
3
この例では、Memoize
を使うことで最初の列挙時に生成した結果をキャッシュし、2回目以降の列挙ではキャッシュから返しています。
これにより、生成処理の重複を防げます。
パフォーマンス最適化では、遅延実行の特性を理解し、必要に応じて即時実行やキャッシュを使い分けることが重要です。
また、PLINQを活用して並列化を行う際はスレッドセーフな設計を心がけ、メモリとCPUのバランスを考慮しながら最適な方法を選択しましょう。
エラーと例外処理
LINQを使う際には、データの不整合や型の不一致、null参照などによるエラーや例外に注意が必要です。
ここでは、Null許容の安全なクエリの書き方、OfType
を使った型不一致の回避、そしてTry/Catch
と遅延実行の関係による落とし穴について詳しく解説いたします。
Null許容の安全なクエリ
LINQクエリでnull値を含むデータを扱う場合、null参照例外NullReferenceException
が発生しやすくなります。
特に、オブジェクトのプロパティにアクセスする際はnullチェックを適切に行うことが重要です。
以下はnull許容の安全なクエリの例です。
using System;
using System.Linq;
class Program
{
class Person
{
public string Name { get; set; }
public string City { get; set; }
}
static void Main()
{
var people = new[]
{
new Person { Name = "Alice", City = "Tokyo" },
new Person { Name = "Bob", City = null },
new Person { Name = "Charlie", City = "Osaka" },
null
};
// nullチェックを含めた安全なクエリ
var filtered = people
.Where(p => p != null && !string.IsNullOrEmpty(p.City))
.Select(p => p.Name + " lives in " + p.City);
foreach (var s in filtered)
{
Console.WriteLine(s);
}
}
}
Alice lives in Tokyo
Charlie lives in Osaka
この例では、p != null
でnullオブジェクトを除外し、!string.IsNullOrEmpty(p.City)
でCityプロパティがnullまたは空文字の要素を除外しています。
これにより、null参照例外を防ぎつつ安全にクエリを実行できます。
また、C# 6.0以降のnull条件演算子?.
を使うと、より簡潔にnullチェックが可能です。
var filtered = people
.Where(p => !string.IsNullOrEmpty(p?.City))
.Select(p => p.Name + " lives in " + p.City);
null条件演算子は、左辺がnullの場合にnullを返し、例外を防ぎます。
OfTypeで型不一致を防ぐ
LINQのシーケンスに異なる型のオブジェクトが混在している場合、特定の型だけを抽出したいことがあります。
OfType<T>
は指定した型にキャスト可能な要素だけを抽出し、型不一致による例外を防ぎます。
using System;
using System.Collections;
using System.Linq;
class Program
{
static void Main()
{
ArrayList mixedList = new ArrayList { 1, "two", 3, "four", 5 };
// int型の要素だけ抽出
var ints = mixedList.OfType<int>();
foreach (var n in ints)
{
Console.WriteLine(n);
}
}
}
1
3
5
OfType
は内部でis
演算子を使って型チェックを行い、キャスト可能な要素だけを返します。
これにより、Cast<T>
のように不適切な型が混じっているときに発生するInvalidCastException
を回避できます。
Try/Catchと遅延実行の落とし穴
LINQのクエリは遅延実行されるため、Try/Catch
でクエリ定義時に例外を捕捉しようとしても、実際の例外は列挙時に発生します。
これにより、例外処理のタイミングを誤ることがあります。
以下の例で説明します。
using System;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new int[] { 1, 2, 0, 4 };
// 0で割るため例外が発生する可能性があるクエリ
var query = numbers.Select(n => 10 / n);
try
{
// クエリ定義時は例外は発生しない
Console.WriteLine("クエリ定義完了");
// 列挙時に例外が発生する
foreach (var result in query)
{
Console.WriteLine(result);
}
}
catch (DivideByZeroException ex)
{
Console.WriteLine("例外をキャッチ: " + ex.Message);
}
}
}
クエリ定義完了
10
5
例外をキャッチ: Attempted to divide by zero.
この例では、Select
の中の除算は列挙時に実行されるため、Try/Catch
は列挙の部分で例外を捕捉しています。
クエリ定義時には例外は発生しません。
もし例外をクエリ定義時に捕捉したい場合は、ToList
やToArray
などで即時実行させる必要があります。
try
{
var results = numbers.Select(n => 10 / n).ToList(); // 即時実行
}
catch (DivideByZeroException ex)
{
Console.WriteLine("例外をキャッチ: " + ex.Message);
}
このように、遅延実行の特性を理解し、例外処理のタイミングを適切に設計することが重要です。
特にデータベースアクセスやファイル操作など副作用のある処理をLINQで行う場合は注意が必要です。
カスタム拡張メソッド
LINQの標準クエリ演算子は非常に便利ですが、特定の業務ロジックや再利用したい処理を自分で拡張メソッドとして実装することも可能です。
ここではユーザー定義演算子の作り方、パイプラインの再利用方法、そしてパフォーマンス面での注意点について詳しく解説いたします。
ユーザー定義演算子の作り方
LINQの拡張メソッドは、IEnumerable<T>
やIQueryable<T>
を受け取り、同じくIEnumerable<T>
やIQueryable<T>
を返すメソッドとして実装します。
これにより、メソッドチェーンの一部として自然に組み込めます。
以下は、偶数だけを抽出するカスタム演算子WhereEven
の例です。
using System;
using System.Collections.Generic;
using System.Linq;
public static class LinqExtensions
{
public static IEnumerable<int> WhereEven(this IEnumerable<int> source)
{
if (source == null) throw new ArgumentNullException(nameof(source));
foreach (var item in source)
{
if (item % 2 == 0)
yield return item;
}
}
}
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 10);
var evens = numbers.WhereEven();
foreach (var n in evens)
{
Console.WriteLine(n);
}
}
}
2
4
6
8
10
ポイントは以下の通りです。
- 拡張メソッドは
static
クラス内のstatic
メソッドとして定義し、第一引数にthis
修飾子を付けて対象の型を指定します - 入力の
IEnumerable<T>
を遅延実行で処理するためにyield return
を使います null
チェックを行い、引数がnull
の場合は例外をスローして安全性を確保します
このようにカスタム演算子を作ることで、業務ロジックに特化したフィルタリングや変換を簡潔に再利用可能な形で提供できます。
パイプラインの再利用
LINQのメソッドチェーンはパイプラインとして表現でき、複数のクエリ処理を組み合わせて再利用することが可能です。
カスタム拡張メソッドを使うことで、共通の処理をまとめてパイプライン化し、コードの重複を減らせます。
例えば、文字列のリストから空文字やnullを除外し、トリムして大文字に変換する共通処理を拡張メソッドとしてまとめます。
using System;
using System.Collections.Generic;
using System.Linq;
public static class LinqExtensions
{
public static IEnumerable<string> CleanAndUpper(this IEnumerable<string> source)
{
if (source == null) throw new ArgumentNullException(nameof(source));
return source
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => s.Trim().ToUpperInvariant());
}
}
class Program
{
static void Main()
{
var words = new List<string> { " apple ", null, " ", "banana", "Cherry " };
var cleaned = words.CleanAndUpper();
foreach (var word in cleaned)
{
Console.WriteLine(word);
}
}
}
APPLE
BANANA
CHERRY
このようにパイプラインを拡張メソッドとして切り出すことで、複数の場所で同じ処理を簡単に再利用でき、保守性が向上します。
パフォーマンスへの配慮
カスタム拡張メソッドを作成する際は、パフォーマンスにも注意が必要です。
特に以下の点を意識しましょう。
- 遅延実行を維持する
yield return
を使い、必要なときに必要な要素だけを生成する遅延実行を維持することで、メモリ使用量を抑えられます。
即時実行のToList
やToArray
を不用意に使うとメモリ消費が増大します。
- 引数の
null
チェックを行う
早期にArgumentNullException
をスローすることで、バグの原因を特定しやすくなります。
- 不要なコレクションの生成を避ける
例えば、Select
やWhere
を複数回連続で呼ぶ場合、可能な限り一つのメソッド内で処理をまとめるとオーバーヘッドを減らせます。
- 複雑な処理は分割してテストしやすく
複雑なロジックを一つの拡張メソッドに詰め込みすぎると可読性やテスト性が低下します。
適切に分割し、単体テストを充実させましょう。
- 並列処理との相性を考慮する
並列処理(PLINQ)で使う場合は、スレッドセーフな設計が必要です。
状態を持つ処理や副作用のある処理は避けるか、適切に同期を行いましょう。
カスタム拡張メソッドはLINQの柔軟性をさらに高め、業務ロジックの共通化やコードの簡潔化に役立ちます。
パフォーマンスや安全性に配慮しつつ、適切に設計・実装することが重要です。
非同期LINQパターン
非同期プログラミングは、I/O待ちや長時間かかる処理を効率的に扱うために重要です。
LINQでも非同期対応のパターンがあり、特にデータベースアクセスやストリーム処理で活用されます。
ここではAsAsyncEnumerable
を使ったストリーム処理と、ToListAsync
を使ったデータベース呼び出しについて詳しく解説いたします。
AsAsyncEnumerableでストリーム処理
AsAsyncEnumerable
は、同期的なIQueryable
やIEnumerable
を非同期のIAsyncEnumerable<T>
に変換し、非同期ストリームとして処理できるようにします。
これにより、データを逐次的に非同期で取得しながら処理でき、メモリ効率や応答性が向上します。
以下は、Entity Framework CoreのDbSet<T>
をAsAsyncEnumerable
で非同期ストリーム処理する例です。
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
class Program
{
static async Task Main()
{
using var context = new SampleContext();
// データベースとシードデータを作成
context.Database.EnsureCreated();
// データを非同期ストリームで取得
await foreach (var product in context.Products.AsAsyncEnumerable())
{
Console.WriteLine($"{product.Id}: {product.Name} - {product.Price}円");
}
}
}
public class SampleContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseInMemoryDatabase("SampleDB");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().HasData(
new Product { Id = 1, Name = "ProductA", Price = 1500 },
new Product { Id = 2, Name = "ProductB", Price = 2000 },
new Product { Id = 3, Name = "ProductC", Price = 1200 }
);
}
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public int Price { get; set; }
}
1: ProductA - 1500円
2: ProductB - 2000円
3: ProductC - 1200円
ポイントは以下の通りです。
AsAsyncEnumerable
はIQueryable<T>
をIAsyncEnumerable<T>
に変換し、await foreach
で非同期に逐次処理できます- 大量データを一度にメモリに読み込まず、ストリームとして処理できるためメモリ効率が良いです
- EF Coreの非同期APIと組み合わせて使うことで、データベースアクセスの待機時間を効率的に扱えます
ToListAsyncでデータベース呼び出し
ToListAsync
は、非同期にクエリを実行して結果をリストに格納します。
データベースからのデータ取得時に使われ、UIスレッドのブロックを防ぎつつ、結果を即時取得したい場合に便利です。
以下はEF CoreでToListAsync
を使った例です。
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
class Program
{
static async Task Main()
{
using var context = new SampleContext();
// データベースとシードデータを作成
context.Database.EnsureCreated();
// 非同期にクエリを実行しリストを取得
List<Product> products = await context.Products
.Where(p => p.Price > 1300)
.OrderBy(p => p.Price)
.ToListAsync();
foreach (var product in products)
{
Console.WriteLine($"{product.Name}: {product.Price}円");
}
}
}
public class SampleContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseInMemoryDatabase("SampleDB");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().HasData(
new Product { Id = 1, Name = "ProductA", Price = 1500 },
new Product { Id = 2, Name = "ProductB", Price = 2000 },
new Product { Id = 3, Name = "ProductC", Price = 1200 }
);
}
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public int Price { get; set; }
}
ProductA: 1500円
ProductB: 2000円
ポイントは以下の通りです。
ToListAsync
はクエリを非同期に実行し、結果をList<T>
として取得します- UIアプリケーションやWebアプリケーションで、データベースアクセスの待機時間中にスレッドをブロックしないために使います
- クエリの途中に
Where
やOrderBy
などのLINQ演算子を組み合わせて柔軟に条件指定が可能です
非同期LINQパターンを活用することで、I/O待ちの効率化やメモリ使用量の最適化が可能になります。
AsAsyncEnumerable
はストリーム処理に適し、ToListAsync
は結果をまとめて取得したい場合に適しています。
用途に応じて使い分けることが重要です。
実務での活用例
LINQは実務のさまざまなシーンでデータ操作を効率化し、コードの可読性や保守性を高める強力なツールです。
ここでは、ログ分析の抽出と集計、ランキング生成、不正データフィルタリングの具体的な活用例を示します。
ログ分析の抽出と集計
大量のログデータから特定の条件に合致する情報を抽出し、集計する作業はよくある業務です。
LINQを使うと、複雑な条件指定や集計処理を簡潔に記述できます。
以下は、ログエントリのリストからエラーレベルのログを抽出し、日付ごとに件数を集計する例です。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
class LogEntry
{
public DateTime Timestamp { get; set; }
public string Level { get; set; }
public string Message { get; set; }
}
static void Main()
{
var logs = new List<LogEntry>
{
new LogEntry { Timestamp = DateTime.Parse("2024-06-01 10:00"), Level = "Info", Message = "Start process" },
new LogEntry { Timestamp = DateTime.Parse("2024-06-01 10:05"), Level = "Error", Message = "NullReferenceException" },
new LogEntry { Timestamp = DateTime.Parse("2024-06-01 11:00"), Level = "Error", Message = "TimeoutException" },
new LogEntry { Timestamp = DateTime.Parse("2024-06-02 09:00"), Level = "Info", Message = "Process completed" },
new LogEntry { Timestamp = DateTime.Parse("2024-06-02 09:30"), Level = "Error", Message = "InvalidOperationException" }
};
var errorCountsByDate = logs
.Where(log => log.Level == "Error")
.GroupBy(log => log.Timestamp.Date)
.Select(g => new { Date = g.Key, Count = g.Count() })
.OrderBy(x => x.Date);
foreach (var item in errorCountsByDate)
{
Console.WriteLine($"{item.Date:yyyy-MM-dd}: {item.Count}件のエラー");
}
}
}
2024-06-01: 2件のエラー
2024-06-02: 1件のエラー
この例では、Where
でエラーレベルのログだけを抽出し、GroupBy
で日付ごとにグループ化、Count
で件数を集計しています。
OrderBy
で日付順に並べ替え、見やすいレポートを作成しています。
ランキング生成
ランキングは売上やスコア、アクセス数などの順位付けに使われます。
LINQの並べ替えや集計機能を活用すると、簡単にランキングを生成できます。
以下は、商品の売上データから売上金額の高い順にランキングを作成する例です。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
class ProductSales
{
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
static void Main()
{
var sales = new List<ProductSales>
{
new ProductSales { ProductName = "商品A", Quantity = 10, UnitPrice = 1500m },
new ProductSales { ProductName = "商品B", Quantity = 5, UnitPrice = 3000m },
new ProductSales { ProductName = "商品C", Quantity = 20, UnitPrice = 800m }
};
var ranking = sales
.Select(s => new
{
s.ProductName,
TotalSales = s.Quantity * s.UnitPrice
})
.OrderByDescending(x => x.TotalSales)
.Select((x, index) => new
{
Rank = index + 1,
x.ProductName,
x.TotalSales
});
foreach (var item in ranking)
{
Console.WriteLine($"{item.Rank}位: {item.ProductName} - 売上 {item.TotalSales:C}");
}
}
}
1位: 商品C - 売上 ¥16,000
2位: 商品A - 売上 ¥15,000
3位: 商品B - 売上 ¥15,000
この例では、Select
で売上金額を計算し、OrderByDescending
で降順に並べ替えています。
さらに、Select
のインデックス付きオーバーロードを使って順位を付けています。
不正データフィルタリング
データの中に不正な値や異常値が混入している場合、LINQで条件を指定してフィルタリングし、除外や修正を行うことができます。
以下は、ユーザー情報のリストからメールアドレスが不正なデータを除外する例です。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
class User
{
public string Name { get; set; }
public string Email { get; set; }
}
static void Main()
{
var users = new List<User>
{
new User { Name = "Alice", Email = "alice@example.com" },
new User { Name = "Bob", Email = "bob[at]example.com" },
new User { Name = "Charlie", Email = "charlie@example.com" },
new User { Name = "David", Email = "david.example.com" }
};
var emailPattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+$";
var validUsers = users
.Where(u => !string.IsNullOrEmpty(u.Email) && Regex.IsMatch(u.Email, emailPattern));
foreach (var user in validUsers)
{
Console.WriteLine($"{user.Name}: {user.Email}");
}
}
}
Alice: alice@example.com
Charlie: charlie@example.com
この例では、正規表現を使ってメールアドレスの形式を検証し、不正なメールアドレスを持つユーザーを除外しています。
Where
で条件を指定し、IsMatch
でパターンマッチングを行っています。
これらの実務例は、LINQの強力なクエリ機能を活用して複雑なデータ操作を簡潔に実装する方法を示しています。
ログ分析やランキング生成、不正データのフィルタリングは多くの業務で頻繁に発生するため、LINQを使いこなすことで効率的かつ保守性の高いコードを書くことが可能です。
よくある落とし穴
LINQは便利な機能ですが、使い方を誤るとパフォーマンス低下やバグの原因になることがあります。
ここでは特に注意したい「Select
先での副作用」「GroupBy
のメモリ肥大」「データベース側のN+1問題」について詳しく解説いたします。
Select先での副作用
LINQのSelect
は本来、要素の変換や投影を行う純粋な関数として使うべきですが、処理の中で副作用(状態変更や外部への影響)を起こすコードを入れてしまうケースがあります。
これは予期せぬ動作やバグの原因となるため注意が必要です。
例えば、以下のようにSelect
内で変数の値を変更するコードは副作用を伴います。
using System;
using System.Linq;
class Program
{
static void Main()
{
int counter = 0;
var numbers = Enumerable.Range(1, 5);
var query = numbers.Select(n =>
{
counter++; // 副作用:外部変数の変更
return n * 2;
});
// クエリは遅延実行なので、列挙時に副作用が発生
foreach (var item in query)
{
Console.WriteLine(item);
}
Console.WriteLine($"処理回数: {counter}");
}
}
2
4
6
8
10
処理回数: 5
この例ではcounter
がSelect
の中で増加していますが、クエリが複数回列挙されると副作用も複数回発生し、予期しない結果になる恐れがあります。
副作用を避けるためには、Select
内では純粋な変換処理だけを行い、状態変更や外部への影響は別の場所で行うことが望ましいです。
GroupByのメモリ肥大
GroupBy
は要素をキーでグループ化する便利な演算子ですが、大量データに対して使うとメモリ使用量が急増することがあります。
これはGroupBy
が内部で全要素を一時的にメモリに保持し、グループごとにまとめるためです。
例えば、数百万件のデータをGroupBy
でグループ化すると、メモリ不足やパフォーマンス低下を招くことがあります。
// 大量データの例(イメージ)
var largeData = Enumerable.Range(1, 10_000_000);
var grouped = largeData.GroupBy(n => n % 10);
// groupedの列挙時に大量のメモリを消費
対策としては以下の方法があります。
- 必要なデータだけを事前に絞り込む
Where
で条件を絞り、グループ化する要素数を減らす。
- ストリーム処理やバッチ処理を検討する
一度に全件をグループ化せず、分割して処理します。
- データベース側でグループ化を行う
LINQ to Entitiesなどを使い、SQLのGROUP BY
で集計してから取得します。
- メモリ使用量を監視し、必要に応じて環境を調整する
データベース側のN+1問題
N+1問題は、ORMやLINQ to Entitiesなどでよく発生するパフォーマンス問題です。
親テーブルのN件のデータを取得した後、関連する子テーブルのデータを1件ずつ個別に問い合わせるため、合計でN+1回のSQLが発行されてしまいます。
例えば、以下のようなコードはN+1問題を引き起こします。
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
class Program
{
static void Main()
{
using var context = new SampleContext();
var orders = context.Orders.ToList(); // 親テーブルを取得
foreach (var order in orders)
{
// 子テーブルのデータを遅延ロードで1件ずつ取得 → N回のSQL発行
Console.WriteLine($"Order {order.Id} has {order.OrderItems.Count} items");
}
}
}
この場合、Orders
の取得で1回、OrderItems
の取得でN回のSQLが発行され、合計N+1回の問い合わせが発生します。
対策としては、Eager Loadingを使い、関連データを一度のクエリでまとめて取得します。
Entity FrameworkではInclude
メソッドを使います。
var ordersWithItems = context.Orders
.Include(o => o.OrderItems)
.ToList();
foreach (var order in ordersWithItems)
{
Console.WriteLine($"Order {order.Id} has {order.OrderItems.Count} items");
}
これにより、1回のSQLで親子テーブルのデータを取得でき、N+1問題を回避できます。
これらの落とし穴はLINQを使う上でよく遭遇する問題です。
副作用のある処理を避け、メモリ使用量に注意し、データベースアクセスの効率化を図ることで、安定かつ高速なアプリケーションを実現できます。
バージョン別の注意点
C#とLINQはバージョンアップを重ねるごとに機能が拡充され、より表現力豊かで安全なコードが書けるようになっています。
ここではC# 3.0から11.0までの主な進化と、近年重要視されているnullable
コンテキストへの適応について解説します。
C# 3.0から11.0までの進化
C# 3.0(2007年)
- LINQの登場
C# 3.0でLINQが初めて導入されました。
- クエリ構文とメソッド構文の両方が利用可能に
- 匿名型、拡張メソッド、ラムダ式、式ツリーなどLINQを支える基盤機能が追加
IEnumerable<T>
を対象にしたLINQ to Objectsが主流
C# 4.0(2010年)
- 動的型付けの導入
dynamic
型が追加され、LINQの柔軟性が向上。
- 名前付き引数、オプション引数
メソッド呼び出しの可読性が向上。
C# 5.0(2012年)
- 非同期プログラミングの強化
async
/await
キーワードが導入され、非同期LINQパターンの基礎が整備。
- LINQクエリの非同期実行がより自然に
C# 6.0(2015年)
- null条件演算子
?.
の追加
LINQでのnull安全なクエリ記述が簡潔に。
- 文字列補間
クエリ結果の表示やログ出力が楽に。
C# 7.0(2017年)
- タプルの強化
LINQの投影で複数値を返す際に便利。
- パターンマッチングの導入
switch
式やis
パターンで条件分岐が強化。
C# 8.0(2019年)
- 非同期ストリーム
IAsyncEnumerable<T>
の追加
await foreach
で非同期LINQが可能に。
- nullable参照型の導入(オプション)
null安全性の向上。
- インデックスと範囲演算子
配列やリストの部分取得が簡単に。
C# 9.0(2020年)
- レコード型の追加
不変データ構造の表現が容易に。
- トップレベルステートメント
簡潔なプログラム記述が可能です。
C# 10.0(2021年)
- グローバル
using
ディレクティブ
名前空間の冗長な記述を削減。
- 構造体の改良
パフォーマンス向上。
C# 11.0(2022年)
- リストパターン
LINQのパターンマッチングでリストの条件判定が強化。
- raw文字列リテラル
複雑な文字列(SQLやJSONなど)を簡単に記述可能です。
nullableコンテキストへの適応
C# 8.0で導入されたnullable参照型は、参照型の変数がnull
を許容するかどうかをコンパイル時に明示的に区別し、null参照例外の発生を減らすための機能です。
LINQを使う際もこの機能に適応することが重要です。
nullableコンテキストの有効化
プロジェクトの.csproj
ファイルに以下を追加することで有効化します。
<Nullable>enable</Nullable>
これにより、参照型はデフォルトでnull非許容となり、nullを代入すると警告が出ます。
nullを許容したい場合はstring?
のように?
を付けます。
LINQクエリでの影響
nullableコンテキストが有効な場合、LINQのクエリ内でnullチェックを厳密に行う必要があります。
例えば、以下のようにnull許容型を扱う場合は、null条件演算子やnullチェックを適切に入れます。
string?[] names = { "Alice", null, "Bob" };
var nonNullNames = names
.Where(name => !string.IsNullOrEmpty(name))
.Select(name => name!.ToUpper()); // null許容を解除
foreach (var name in nonNullNames)
{
Console.WriteLine(name);
}
name!
はnull許容を解除する演算子で、コンパイラに「ここはnullでない」と伝えます。
ただし、実際にnullが入る可能性がある場合はWhere
でしっかり除外することが必須です。
メソッドシグネチャの変更
拡張メソッドやカスタムメソッドの引数や戻り値もnullable対応を行う必要があります。
例えば、
public static IEnumerable<string?> FilterNullable(this IEnumerable<string?> source)
{
return source.Where(s => s != null);
}
のように、引数や戻り値に?
を付けてnull許容を明示します。
既存コードの移行
既存のコードをnullableコンテキストに対応させる際は、以下の点に注意します。
- nullチェックを追加し、警告を解消します
!
演算子でnull許容を解除する箇所を最小限に抑えます- 可能な限りnull非許容型を使い、null安全な設計を心がける
C#のバージョンアップに伴い、LINQの書き方や安全性も進化しています。
特にnullableコンテキストはコードの品質向上に寄与するため、最新の言語機能を理解し適切に対応することが重要です。
便利な補助API
LINQの標準機能は非常に強力ですが、より高度な操作や非同期処理を簡単に実現するための補助的なライブラリも存在します。
ここでは、拡張LINQライブラリとして有名なMoreLINQ
の主要機能と、非同期LINQをサポートするSystem.Linq.Async
について詳しく解説いたします。
MoreLINQの主要機能
MoreLINQ
は、標準LINQにない便利な拡張メソッドを多数提供するオープンソースライブラリです。
複雑なシーケンス操作やパターンを簡潔に記述できるため、実務でのデータ処理を効率化します。

代表的な機能をいくつか紹介します。
Batch
シーケンスを指定したサイズのチャンク(バッチ)に分割します。
大量データを分割して処理したい場合に便利です。
using System;
using System.Linq;
using MoreLinq;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 10);
var batches = numbers.Batch(3);
foreach (var batch in batches)
{
Console.WriteLine($"Batch: {string.Join(", ", batch)}");
}
}
}
Batch: 1, 2, 3
Batch: 4, 5, 6
Batch: 7, 8, 9
Batch: 10
DistinctBy
特定のキーで重複を排除します。
標準LINQのDistinct
は要素全体で比較しますが、DistinctBy
はキー指定で重複判定が可能です。
using System;
using System.Linq;
using MoreLinq;
class Program
{
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
static void Main()
{
var people = new[]
{
new Person { Name = "Alice", Age = 30 },
new Person { Name = "Bob", Age = 25 },
new Person { Name = "Alice", Age = 35 }
};
var distinctByName = people.DistinctBy(p => p.Name);
foreach (var person in distinctByName)
{
Console.WriteLine($"{person.Name} ({person.Age})");
}
}
}
Alice (30)
Bob (25)
Window
スライディングウィンドウを作成し、連続した要素のグループを生成します。
時系列データの分析などに役立ちます。
using System;
using System.Linq;
using MoreLinq;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 5);
var windows = numbers.Window(3);
foreach (var window in windows)
{
Console.WriteLine($"Window: {string.Join(", ", window)}");
}
}
}
Window: 1, 2, 3
Window: 2, 3, 4
Window: 3, 4, 5
MaxBy / MinBy
指定したキーで最大値・最小値の要素を取得します。
C# 9以降で標準化されましたが、MoreLINQでは以前から利用可能でした。
var oldest = people.MaxBy(p => p.Age);
Console.WriteLine($"Oldest: {oldest.Name} ({oldest.Age})");
MoreLINQ
は他にも多数の便利なメソッドを提供しており、NuGetから簡単に導入できます。
標準LINQの不足を補い、複雑なデータ操作をシンプルに記述したい場合におすすめです。
System.Linq.Asyncで非同期拡張
System.Linq.Async
は、IAsyncEnumerable<T>
に対するLINQ拡張メソッドを提供するライブラリで、非同期ストリームの操作を簡単に行えます。
await foreach
と組み合わせて使うことで、非同期データのフィルタリングや変換、集計が可能です。
代表的なメソッド
ToListAsync()
非同期にシーケンスをリストに変換します。
Where()
非同期シーケンスの要素を条件でフィルタリングします。
Select()
非同期シーケンスの要素を変換します。
FirstAsync()
,SingleAsync()
,AnyAsync()
など
非同期で要素の取得や存在判定を行います。
使用例
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq.Async;
class Program
{
static async Task Main()
{
var asyncNumbers = GetNumbersAsync();
var evenNumbers = asyncNumbers.Where(n => n % 2 == 0);
var list = await evenNumbers.ToListAsync();
foreach (var n in list)
{
Console.WriteLine(n);
}
}
static async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 1; i <= 10; i++)
{
await Task.Delay(100); // 模擬的な非同期処理
yield return i;
}
}
}
2
4
6
8
10
ポイント
System.Linq.Async
は.NET標準の非同期ストリームIAsyncEnumerable<T>
に対応したLINQ演算子を提供します- 非同期I/Oやストリーム処理での待機時間を効率的に扱えます
- NuGetパッケージ
System.Linq.Async
をインストールして利用します
これらの補助APIを活用することで、標準LINQの機能を拡張し、より高度で効率的なデータ操作や非同期処理が可能になります。
特に大量データや非同期I/Oを扱う現代のアプリケーション開発においては、MoreLINQ
やSystem.Linq.Async
は強力な武器となります。
LINQと設計パターン
LINQはデータ操作を簡潔に記述できる強力なツールですが、設計パターンと組み合わせることで、より保守性や拡張性の高いシステムを構築できます。
ここでは、リポジトリパターンとの相性と、クリーンアーキテクチャにおけるクエリ分離について詳しく解説いたします。
リポジトリパターンとの相性
リポジトリパターンは、データアクセスの抽象化を目的とした設計パターンで、ドメイン層からデータソースへのアクセスを分離します。
LINQはこのパターンと非常に相性が良く、リポジトリの実装でクエリを簡潔に記述できるため、コードの可読性と保守性が向上します。
リポジトリパターンの基本構造
- インターフェース
データ操作の契約を定義し、ドメイン層はこれに依存します。
- 実装クラス
実際のデータアクセス(LINQ to EntitiesやLINQ to Objectsなど)を行います。
LINQを使ったリポジトリの例
using System;
using System.Collections.Generic;
using System.Linq;
public interface IProductRepository
{
IEnumerable<Product> GetAll();
IEnumerable<Product> GetByCategory(string category);
Product GetById(int id);
}
public class ProductRepository : IProductRepository
{
private readonly List<Product> _products;
public ProductRepository()
{
// サンプルデータ
_products = new List<Product>
{
new Product { Id = 1, Name = "商品A", Category = "食品", Price = 100 },
new Product { Id = 2, Name = "商品B", Category = "家電", Price = 2000 },
new Product { Id = 3, Name = "商品C", Category = "食品", Price = 150 }
};
}
public IEnumerable<Product> GetAll()
{
return _products.AsEnumerable();
}
public IEnumerable<Product> GetByCategory(string category)
{
return _products.Where(p => p.Category == category);
}
public Product GetById(int id)
{
return _products.FirstOrDefault(p => p.Id == id);
}
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
}
class Program
{
static void Main()
{
IProductRepository repository = new ProductRepository();
var foods = repository.GetByCategory("食品");
foreach (var product in foods)
{
Console.WriteLine($"{product.Name} - {product.Price}円");
}
}
}
商品A - 100円
商品C - 150円
メリット
- データアクセスの一元管理
LINQクエリをリポジトリ内に集約し、変更に強い設計にできます。
- テスト容易性の向上
インターフェースを使うことでモック実装が可能になり、単体テストがしやすい。
- ドメイン層の関心分離
ドメインロジックはデータアクセスの詳細を意識せずに済みます。
注意点
- リポジトリのメソッドが増えすぎると肥大化するため、必要なクエリを適切に整理することが重要でしょう
- LINQの遅延実行により、呼び出し側での列挙タイミングに注意が必要でしょう
クリーンアーキテクチャでのクエリ分離
クリーンアーキテクチャは、システムを層に分割し依存関係を内側に向けることで、保守性や拡張性を高める設計思想です。
LINQを使う際も、クエリ処理を適切な層に分離することが推奨されます。
クリーンアーキテクチャの層構成例
- エンティティ層(ドメインモデル)
ビジネスルールやドメインオブジェクトを定義。
- ユースケース層(アプリケーションサービス)
ビジネスロジックの実装やユースケースの調整。
- インフラ層
データベースアクセスや外部サービスとの連携。
- プレゼンテーション層
UIやAPIの実装。
クエリ分離のポイント
- クエリはインフラ層で実装
LINQ to EntitiesやDapperなどの具体的なデータアクセスはインフラ層に置きます。
- ユースケース層はインターフェースを通じてクエリを呼び出す
ユースケース層はリポジトリやクエリサービスのインターフェースに依存し、実装詳細を知らない。
- クエリ専用のサービスを作成することもある
複雑な読み取り専用クエリは、リポジトリとは別にクエリサービスとして分離し、読み取り専用の最適化を行います。
例:クエリサービスのインターフェース
public interface IProductQueryService
{
IEnumerable<ProductDto> GetProductsByCategory(string category);
}
例:ユースケース層での利用
public class ProductUseCase
{
private readonly IProductQueryService _queryService;
public ProductUseCase(IProductQueryService queryService)
{
_queryService = queryService;
}
public void DisplayProducts(string category)
{
var products = _queryService.GetProductsByCategory(category);
foreach (var p in products)
{
Console.WriteLine($"{p.Name} - {p.Price}円");
}
}
}
メリット
- 関心の分離
読み取り専用クエリと書き込み処理を分けることで、最適化や保守がしやすくなります。
- テストの容易さ
クエリサービスをモック化しやすく、ユースケースのテストが簡単。
- 柔軟なデータアクセス戦略
クエリ専用の最適化やキャッシュ導入がしやすい。
LINQはリポジトリパターンやクリーンアーキテクチャと組み合わせることで、堅牢で拡張性の高いシステム設計に貢献します。
設計パターンの原則を守りつつ、LINQの強力な表現力を活かすことが重要です。
まとめ
本記事では、C#のLINQを基礎から応用まで幅広く解説しました。
LINQの基本構文や主要データソース別の使い方、基本操作やコントロールフロー演算子、生成・変換演算子の活用法を理解できます。
さらに、パフォーマンス最適化やエラー処理、カスタム拡張メソッド、非同期パターン、実務での具体例、よくある落とし穴、バージョン別の注意点、補助API、設計パターンとの連携まで網羅。
LINQの強力な機能を適切に使いこなすことで、効率的で保守性の高いC#開発が可能になります。