今回は「LINQ (Language Integrated Query)」について解説します。LINQは、C#でデータの操作や検索を効率的に行うための強力な機能です。
以下がLINQの簡単な例です。整数のリストから偶数のみ抽出してそれぞれ2乗にしたリストを作っています。
using System;
using System.Collections.Generic;
using System.Linq;
int[] numbers = new int[] { 1, 2, 3, 4, 5, 6 };
// LINQを使って偶数を抽出し、2乗する
IEnumerable<int> evenSquaredQuery = from n in numbers // numbersの各要素をnとして
where n % 2 == 0 // nが偶数の場合
select n * n; // その要素を2乗して選択
// クエリの結果をリストに変換
List<int> evenSquaredNumbers = evenSquaredQuery.ToList(); // 結果は{4,16,36}のリスト
LINQってデータベースアクセスで使う「SQL」っぽい書き方だね!
その通りです!
LINQを使うとデータ操作について、SQLライクに簡潔でわかりやすく書けるのです。
LINQを使うと、コレクション、JSONファイル、データベースなど様々なデータソースについて統一的な方法で問い合わせ(クエリ)を記述できます。
様々なライブラリ・フレームワークにおいてLINQは広く採用(例:Entity Framework Core)されており、実践的なC#によるアプリ開発でとても重要な機能です。
LINQを学ぶ前提知識としてはコレクション、ジェネリック型、デリゲート、ラムダ式の基礎知識が必要になります。ぜひ、まず以下の記事もみてください。
演習では、これまで作ってきたレストランメニュー表生成プログラムへメニュー分析機能を追加します。
これらの機能によって、レストラン経営者がメニューのバランス調整をしたり、売上の計算をしたりすることが可能になりますね。
LINQについて一緒に学んでいきましょう!
GitHubに演習コード一式を置いてあるので、参考にしてください。
YouTubeの動画も作成しています。
前編
後編
講義:LINQとは何か?
LINQの特徴
LINQ(Language Integrated Query)は、C#に統合されたクエリ言語機能であり、主な特徴は以下のとおりです。
- 可読性の高いコード: SQLライクな(宣言的な)構文により、データ操作の意図が明確になります。
- 遅延実行: クエリは実際に結果が必要になるまで実行されません。これにより、パフォーマンスが向上します。
- 統一的なクエリ構文: データソースの種類に関係なく、同じ構文でクエリを書けます。
LINQについては、Microsoftの「C#の統合言語クエリ(LINQ)」の記事も参考にしてください。
具体例をみながら、特徴をみていきましょう。
①可読性の高いコード
冒頭のLINQを使った例を再掲します。
…
int[] numbers = new int[] { 1, 2, 3, 4, 5, 6 };
//「What(何)」を行うかを記述(宣言的)
IEnumerable<int> evenSquaredQuery = from n in numbers
where n % 2 == 0
select n * n;
List<int> evenSquaredNumbers = evenSquaredQuery.ToList();
同様の処理をLINQを使わずに記述した例は以下です。
…
int[] numbers = new int[] { 1, 2, 3, 4, 5, 6 };
//「How(どのように)」行うかを記述(手続き)
List<int> evenSquaredNumbers = new List<int>();
foreach (int n in numbers)
{
if (n % 2 == 0)
{
evenSquaredNumbers.Add(n * n);
}
}
LINQを使った場合のほうが「何をやるか(What)」をより明確・簡潔に書けていますね。可読性が高いコードになっています。
手続き的な記述に慣れている場合には、最初に少し慣れが必要かもしれませんね。
これは簡潔な例ですが、複雑な構造をもつデータ(例:JSON、データベース)を操作する場合に、この記述力の違いがより顕著になります。
②遅延実行
LINQのクエリは実際に結果が必要になるまで実行されません。これにより、パフォーマンスが向上します。
以下の例をみてみましょう。さきほどの例を少し変えて、evenSquaredQueryの最初の要素のみを取得しています。
…
int[] numbers = new int[] { 1, 2, 3, 4, 5, 6 };
IEnumerable<int> evenSquaredQuery = from n in numbers
where n % 2 == 0
select n * n;
int first = evenSquaredQuery.First(); //firstの値は4
このとき、LINQの遅延実行によって「2乗する」という計算は最初の偶数である「2」に対してしか行われません。(正確には、行われないと「期待」できます)
他の要素については現時点で計算する必要がないため計算されません。本当に必要になった時点で計算されます。
大量のデータを操作するときに、特に力を発揮します。
LINQでは遅延実行として「可能な限り計算を後回し」にするような仕組みになっていますが、詳細は実装依存となります。
LINQ内部でよきにはからって効率よく計算してくれる…というイメージですね。
例えばDBアクセスをLINQで行うと、複数の操作を1つのSQLクエリにまとめて発行してくれるため非常に強力です。
③統一的なクエリ構文
LINQを使うとコレクションだけではなくデータベース、JSONなど様々なデータソースについて統一的な方法で問い合わせ(クエリ)を記述できます。
以下は、従業員データのJSON形式のテキストから、年齢が30歳以上の人の名前を抽出しています。
using System.Linq;
using System.Collections.Generic;
using System.Text.Json;
// 日本語のJSON文字列
string jsonString = @"{
""従業員"": [
{ ""名前"": ""太郎"", ""年齢"": 30 },
{ ""名前"": ""花子"", ""年齢"": 25 },
{ ""名前"": ""次郎"", ""年齢"": 35 }
]
}";
using JsonDocument jsonDocument = JsonDocument.Parse(jsonString);
JsonElement rootElement = jsonDocument.RootElement;
// LINQを使用して30歳以上の従業員の名前を取得
IEnumerable<string> age30OrAboveQuery = from employee in rootElement.GetProperty("従業員").EnumerateArray()
where employee.GetProperty("年齢").GetInt32() >= 30
select employee.GetProperty("名前").GetString();
// クエリ結果をリストに変換
List<string> namesOfAge30OrAbove = age30OrAboveQuery.ToList(); // {"太郎"、"次郎"}
このコードについては、JSONデータについてもコレクションと同様にLINQでデータを操作できるのだなと雰囲気を感じてもらえればOKです。
このように、様々なデータソースに対して統一的な方法で問い合わせができます。
LINQの記法
LINQには「クエリ構文」と「メソッド構文」という2つの記述方法があります。
以下はクエリ構文です。今回の記事でこれまで見てきたのは全てクエリ構文です。
IEnumerable<int> evenSquaredQuery = from n in numbers
where n % 2 == 0
select n * n;
同じ処理をメソッド構文で書くと以下になります。
IEnumerable<int> evenSquaredQuery = numbers
.Where(n => n % 2 == 0)
.Select(n => n * n);
メソッド構文は、クエリ構文と完全に同等の機能を持ちます。どちらを使うかは個人の好みやチームの規約によって決めることが多いです。
前回の演習ででてきたOrderByメソッド、Whereメソッドはメソッド構文だったんだね!
型推論と匿名型
LINQを使う上で、「型推論」と「匿名型」という2つの概念を理解しておくと便利です。これらの機能を使うことで、LINQをより簡潔に、そして柔軟に書くことができます。
型推論
型推論とは、コンパイラが変数の型を自動的に判断してくれる機能です。C#では var キーワードを使用することで、この機能を利用できます。例をみてみましょう。
int[] numbers = { 1, 2, 3, 4, 5 };
var evenNumbers = from n in numbers
where n % 2 == 0
select n;
ここで var
キーワードを使用していますが、コンパイラはevenNumbers の型が IEnumerable<int>であることを自動的に判断します。
型推論を使うことでコードがより簡潔になり、また型が変更された場合にも柔軟に対応できます。
匿名型
匿名型は、コード内でその場限りの新しい型を作成する機能です。
LINQでは、複数のプロパティを持つ新しいオブジェクトを簡単に作成したい場合によく使用されます。以下が例です。
// 匿名型を使用して人物のリストを作成
var people = new[]
{
new { Name = "太郎", Age = 22, Occupation = "学生" }, // 匿名型: 名前、年齢、職業を持つオブジェクト
new { Name = "花子", Age = 28, Occupation = "会社員" }, // 各要素は同じ構造の匿名型
new { Name = "次郎", Age = 19, Occupation = "学生" },
new { Name = "美香", Age = 35, Occupation = "自営業" }
};
// LINQクエリで匿名型の新しいコレクションを生成
var workers = from p in people
where p.Occupation != "学生"
select new { p.Name, p.Age, Status = "社会人" };
// ↑新しい匿名型: 名前と年齢を元のオブジェクトから取得し、
// 新しいStatusプロパティを追加
foreach (var person in workers)
{
// 匿名型のプロパティにアクセスして出力
Console.WriteLine($"{person.Name}さん({person.Age}歳)は{person.Status}です。");
}
実行結果は以下です。
花子さん(28歳)は社会人です。
美香さん(35歳)は社会人です。
この例では、new { Name = “太郎”, Age = 22, Occupation = “学生” } のような部分が匿名型の生成です。
selectステートメントでも new { p.Name, p.Age, Status = “社会人” } という新しい匿名型を作成しています。
匿名型のプロパティ名を明示的に指定することもできます。
select new { Name = p.Name, Age = p.Age, Status = “社会人” }
明示的に指定しない場合、コンパイラが自動でプロパティ名をつけます。
匿名型を使うことで一時的に必要な型を(クラス定義を書かずに、)簡単に作成できます。
なるほど!型を明示的に書かなくても良いから、コードが短くなるんだね。でも、どんな型なのかわからなくなることもありそう…。
そうですね。過度の使用は逆に可読性を下げる可能性もあるので、チームの規約やコードの複雑さを考慮しながら適切に使用しましょう。
Visual Studioで変数や式にマウスカーソルをあわせると、推論された型や匿名型の構造が表示されるので、これを参考にするのもよいでしょう。
以下では、上述した例におけるpeopleの型の構造を表示しています。
匿名型、型推論(var)についてはMicrosoftの記事も参考にしてください。
参考:LINQの主な操作一覧
LINQにおける操作の分類と主な演算子の例を載せます。これらを組み合わせることで様々な操作を実現可能です。
(こちらのページを参考にして作っています)
LINQ演算子の概要
分類 | 説明 | 主な演算子例 |
---|---|---|
フィルタリング | データの絞り込み | Where, OfType |
射影 | データの変換 | Select, SelectMany |
並べ替え | データの順序変更 | OrderBy, ThenBy, Reverse |
グループ化 | データのグループ化 | GroupBy |
集計 | データの集計 | Count, Sum, Average, Min, Max |
結合 | 複数のデータソースの結合 | Join, GroupJoin |
集合演算 | セット操作の実行 | Distinct, Union, Intersect, Except |
要素の操作 | 特定の要素の取得 | First, Last, ElementAt, Single |
分割 | データの一部を取得 | Take, Skip, TakeWhile, SkipWhile |
変換 | コレクション型の変換 | ToArray, ToList, ToDictionary |
たくさんあって覚えられない…。
全て覚える必要はありません。LINQで何か行いたい操作があった場合に、必要に応じて参照しましょう。
演習:メニュー分析機能を作る ~LINQの実践活用!~
作る機能
これまで作ってきたレストランメニュー表生成プログラムへメニュー分析機能を追加します。
具体的には以下の機能を追加し、それぞれ分析結果をコンソールへ出力します。
- ベジタリアンメニューの割合を計算 (where, selectを使う)
- メニューごとの売上を計算 (joinを使う)
- メニューカテゴリ別の平均価格を計算(group byを使う)
これらの機能によって、レストラン経営者がメニューのバランス調整をしたり、売上の計算をしたりすることが可能になりますね。
前回の演習で、Where、OrderByなど基本的なLINQ操作を学びましたね。
今回はそれらの知識をもとに、より高度なLINQ機能を使ってみましょう!①、②、③と徐々に難しい内容になります。
元となるコードは入門編(15)の演習コードです。コードの構成と機能追加部分は以下になります。
入力データとなるCSVの例は以下になります。この例を用いながら、それぞれの機能について説明をします。
Type,Name,Description,Price,IsVegetarian,IsCold
Main,黒毛和牛ステーキ,ジューシーで柔らかなステーキです。,2500,false,
Main,ベジタブルカレー,野菜をたっぷりと使った、スパイシーなカレーです。,1500,true,
Main,グリルチキン,香ばしく焼き上げたジューシーなチキンです。,1800,false,
Main,ベジタリアンパスタ,新鮮な野菜とハーブを使用したパスタです。,1200,true,
Drink,ホットコーヒー,丁寧に焙煎されたコーヒー豆を使用しています。,300,,false
Drink,メロンソーダ,爽やかな甘さが楽しめるメロンソーダです。,350,,true
表形式だと以下になります。
メニュークラスの定義はこちらを参考にしてください。
演習コード
Program.csの大枠は以下になります。
using Restaurant.Generators;
using Restaurant.Menus;
using Restaurant.Readers;
using System.Linq;
namespace Restaurant
{
internal class Program
{
static void Main(string[] args)
{
try
{
// メニューを読み込む
var menus = LoadMenus();
// 通常のメニュー表を出力
GenerateHtmlFile(menus, "menu.html");
// メニューを分析
AnalyzeMenus(menus);
}
…省略…
}
…省略…
static void AnalyzeMenus(List<Menu> menus)
{
// (1)ベジタリアンメニューの割合を計算
CalculateVegetarianPercentage(menus);
// (2)メニューごとの売上を計算
JoinMenuWithSalesData(menus);
// (3)カテゴリ別の平均価格を計算
CalculateAveragePriceByCategory(menus);
}
…省略…
}
読み込んだメニューのリストに対し、(1),(2),(3)でLINQを用いて機能の実現方法を説明します。
コードとあわせて、CSVのサンプルデータを入力したときの出力結果もあわせて見てみます。
(1)ベジタリアンメニューの割合を計算
コードは以下になります。要点を説明します。
static void CalculateVegetarianPercentage(List<Menu> menus)
{
var mainMenus = from menu in menus
where menu is MainMenu
select menu as MainMenu;
var vegetarianCount = (from menu in mainMenus
where menu.IsVegetarian
select menu).Count();
var totalCount = mainMenus.Count();
var vegetarianPercentage = (double)vegetarianCount / totalCount * 100;
Console.WriteLine($"\nベジタリアンメニューの割合: {vegetarianPercentage:F2}%");
}
このコードでは、LINQを使用してベジタリアンメニューの割合を計算しています。
- まず、
where
句を使ってMainMenu
型のメニューだけを抽出します。 - 次に、
where
句を再度使用して、ベジタリアンメニュー(IsVegetarian
がtrue
のもの)の数を数えます。 - 最後に、ベジタリアンメニューの数をメインメニューの総数で割り、100を掛けて割合を計算します。
LINQのCount()メソッドを使用して要素数を取得し、where句でフィルタリングすることで、簡潔にベジタリアンメニューの割合を求めることができます。
サンプル入力データに対する実行結果は以下です。
ベジタリアンメニューの割合: 50.00%
(2)メニューごとの売上を計算
コードは以下になります。要点を説明します。
static void JoinMenuWithSalesData(List<Menu> menus)
{
//メニューごとの販売数量(本来はCSVファイルから読み込むが、ここではサンプルデータを直接定義)
var salesData = new List<(string MenuName, int Quantity)>
{
("黒毛和牛ステーキ", 50),
("ベジタブルカレー", 30),
("グリルチキン", 40),
("ベジタリアンパスタ", 25),
("ホットコーヒー", 100),
("メロンソーダ", 80)
};
var menuSales =
from menu in menus
join sale in salesData
on menu.Name equals sale.MenuName
select new
{
MenuName = menu.Name,
Price = menu.Price,
QuantitySold = sale.Quantity
};
Console.WriteLine("\nメニューと販売数量の結合結果:");
foreach (var item in menuSales)
{
Console.WriteLine($"{item.MenuName}: 価格 {item.Price:#,0}円, 販売数 {item.QuantitySold}, 売上 {item.Price * item.QuantitySold:#,0}円");
}
}
行っている操作のイメージは以下になります。
このコードでは、LINQのjoin
句を使用して、メニューデータと販売数量データを結合し、各メニューの売上を計算しています。
- まず、販売数量データをサンプルとして定義します。(実際はCSVファイルなどから読み込みますが、ここではハードコーディングしています)
join
句を使用して、メニューデータと販売数量データをName
とMenuName
で結合します。select
句で新しい匿名型を作成し、メニュー名、価格、販売数量を格納します。- 結果を
foreach
ループで巡回し、各メニューの詳細と売上(価格 * 販売数量)を表示します。
LINQの
句を使用することで、異なるデータソース間の関連付けを簡単に行うことができます。これにより、複数のデータセットを組み合わせた複雑な分析が可能になります。join
サンプル入力データに対する実行結果は以下です。
メニューと販売数量の結合結果:
黒毛和牛ステーキ: 価格 2,500円, 販売数 50, 売上 125,000円
ベジタブルカレー: 価格 1,500円, 販売数 30, 売上 45,000円
グリルチキン: 価格 1,800円, 販売数 40, 売上 72,000円
ベジタリアンパスタ: 価格 1,200円, 販売数 25, 売上 30,000円
ホットコーヒー: 価格 300円, 販売数 100, 売上 30,000円
メロンソーダ: 価格 350円, 販売数 80, 売上 28,000円
(3)メニューカテゴリ別の平均価格を計算
コードは以下になります。要点を説明します。
static void CalculateAveragePriceByCategory(List<Menu> menus)
{
var averagePriceByCategory =
from menu in menus2
group menu by menu.GetType().Name into categoryGroup
select new
{
Category = categoryGroup.Key,
AveragePrice = categoryGroup.Average(m => m.Price)
};
Console.WriteLine("\nカテゴリ別の平均価格:");
foreach (var item in averagePriceByCategory)
{
Console.WriteLine($"{item.Category}: {item.AveragePrice:#,0}円");
}
}
行っている操作は以下のイメージです。
(中間データ、計算結果データはJSON風に構造を表現しています)
このコードでは、LINQを使用してメニューをカテゴリ(メニューの型)ごとにグループ化し、各カテゴリの平均価格を計算しています。
group by
句を使用して、メニューをGetType().Name
(クラス名)でグループ化します。(図の赤枠に相当)select
句で新しい匿名型を作成し、カテゴリ名(Key
)と平均価格(Average()
メソッドを使用)を格納します。(図の青枠に相当)- 結果を
foreach
ループで巡回し、各カテゴリの平均価格を表示します。
LINQの
とgroup by
メソッドを組み合わせることで、複雑なデータ集計を簡潔に記述できます。匿名型を使用することで、一時的なデータ構造を作成しています。Average()
サンプル入力データに対する実行結果は以下です。
カテゴリ別の平均価格:
MainMenu: 1,750円
DrinkMenu: 325円
LINQはSQLっぽいて思ったけど、エクセルでも似た機能があったような。
GroupByはピボット、JoinはVLOOKUPがエクセルで似た機能ですね。
LINQ、SQL、EXCELは、データに対して「宣言的な記述」で操作を行うという点で共通のコンセプトを持つのです。
おまけ:LINQのさらなる学習に向けて
LINQは奥が深く、この記事で紹介しているのは導入部分です。実践で使いながら継続的に学習することになるでしょう。
もちろん、書籍等を読んで学習する方法もありますが、以下のようなWeb上の記事・ツールも組み合わせると有効です。
- C# 12 in a NutshellとLINQPadを活用する
- 生成AI(ChatGPT等)を活用する
詳しくは、LINQの効率的な学習法(LINQPadや生成AIの活用)の記事も参考にぜひ参考にしてください。
まとめ
LINQは、C#でのデータ操作を大幅に簡素化し、コードの可読性と保守性を向上させる強力な機能です。
今回の演習で見たように、複雑なデータ操作も少ないコードで実現できます。
LINQは配列やリストなどのコレクションだけでなく、JSONやDBの操作など様々な場面で活用できます。
Webアプリ開発編で、Entity Framework CoreとLINQを組み合わせたDBアクセスについても扱っていくので、こちらもご興味があればご覧ください。
LINQを使いこなせるようになると、データ操作が非常に簡単に行えるようになります。ぜひ、様々なシナリオで活用してみてください!
引き続き、C#やプログラミングの考え方を一緒に学んでいきましょう!