C#入門編

C#入門編(16)LINQ ~データ操作を効率的に行う~

今回は「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を学ぶ前提知識としてはコレクション、ジェネリック型、デリゲート、ラムダ式の基礎知識が必要になります。ぜひ、まず以下の記事もみてください。

C#入門編(13)コレクションとジェネリック型 ~リストと辞書で要素を動的に変更する~ 今回は「コレクションとジェネリック型」について解説します。 コレクションとは、複数のオブジェクトを一括して扱うための仕組みです。...
C#入門編(15)デリゲートとラムダ式 ~メソッドの部品化と再利用!~ 今回は「デリゲートとラムダ式」について解説します。 デリゲートはメソッド(関数)を部品化して再利用可能にするC#の機能です。ラム...

演習では、これまで作ってきたレストランメニュー表生成プログラムへメニュー分析機能を追加します。

これらの機能によって、レストラン経営者がメニューのバランス調整をしたり、売上の計算をしたりすることが可能になりますね。

プロ太

LINQについて一緒に学んでいきましょう!

GitHubに演習コード一式を置いてあるので、参考にしてください。

YouTubeの動画も作成しています。

前編

後編

講義:LINQとは何か?

LINQの特徴

LINQ(Language Integrated Query)は、C#に統合されたクエリ言語機能であり、主な特徴は以下のとおりです。

  1. 可読性の高いコード: SQLライクな(宣言的な)構文により、データ操作の意図が明確になります。
  2. 遅延実行: クエリは実際に結果が必要になるまで実行されません。これにより、パフォーマンスが向上します。
  3. 統一的なクエリ構文: データソースの種類に関係なく、同じ構文でクエリを書けます。

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の実践活用!~

作る機能

これまで作ってきたレストランメニュー表生成プログラムへメニュー分析機能を追加します。

具体的には以下の機能を追加し、それぞれ分析結果をコンソールへ出力します。

  1. ベジタリアンメニューの割合を計算 (where, selectを使う)
  2. メニューごとの売上を計算 (joinを使う)
  3. メニューカテゴリ別の平均価格を計算(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を使用してベジタリアンメニューの割合を計算しています。

  1. まず、where句を使ってMainMenu型のメニューだけを抽出します。
  2. 次に、where句を再度使用して、ベジタリアンメニュー(IsVegetariantrueのもの)の数を数えます。
  3. 最後に、ベジタリアンメニューの数をメインメニューの総数で割り、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句を使用して、メニューデータと販売数量データを結合し、各メニューの売上を計算しています。

  1. まず、販売数量データをサンプルとして定義します。(実際はCSVファイルなどから読み込みますが、ここではハードコーディングしています)
  2. join句を使用して、メニューデータと販売数量データをNameMenuNameで結合します。
  3. select句で新しい匿名型を作成し、メニュー名、価格、販売数量を格納します。
  4. 結果を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を使用してメニューをカテゴリ(メニューの型)ごとにグループ化し、各カテゴリの平均価格を計算しています。

  1. group by句を使用して、メニューをGetType().Name(クラス名)でグループ化します。(図の赤枠に相当)
  2. select句で新しい匿名型を作成し、カテゴリ名(Key)と平均価格(Average()メソッドを使用)を格納します。(図の青枠に相当)
  3. 結果をforeachループで巡回し、各カテゴリの平均価格を表示します。

LINQのgroup byAverage()メソッドを組み合わせることで、複雑なデータ集計を簡潔に記述できます。匿名型を使用することで、一時的なデータ構造を作成しています。

サンプル入力データに対する実行結果は以下です。

カテゴリ別の平均価格:
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の活用)の記事も参考にぜひ参考にしてください。

Tips編:C# LINQの学習方法 ~Entity Framework Coreとの連携も~ Entity Framework Core (EF Core) を使用したデータベース操作において、LINQ(Language Int...

まとめ

LINQは、C#でのデータ操作を大幅に簡素化し、コードの可読性と保守性を向上させる強力な機能です。

今回の演習で見たように、複雑なデータ操作も少ないコードで実現できます。

LINQは配列やリストなどのコレクションだけでなく、JSONやDBの操作など様々な場面で活用できます。

Webアプリ開発編で、Entity Framework CoreとLINQを組み合わせたDBアクセスについても扱っていくので、こちらもご興味があればご覧ください。

【C#、Blazor】Webアプリ開発入門編(4)「Todoアプリ」でデータベース作成&データ表示 ~データベース操作のフレームワークを学ぶ~【ASP.NET Core】 今回から具体的なTodoリストアプリ開発を題材として、Blazorの基本機能について紹介します。 初心者がBlazorでWebア...

LINQを使いこなせるようになると、データ操作が非常に簡単に行えるようになります。ぜひ、様々なシナリオで活用してみてください!

プロ太

引き続き、C#やプログラミングの考え方を一緒に学んでいきましょう!

ABOUT ME
プロ太
プログラミングを勉強している人へ情報を発信していきます! ・情報工学分野で博士(工学)の学位取得 ・言語:C# 、Java、C/C++、Python、JavaScript/TypeScript等 ・仕事は主に上流工程(WF開発・Agile開発、OSS開発経験あり) ・趣味で開発:3Dゲーム、Webアプリ、言語処理系等

ご依頼・ご相談について

プログラミング学習のご相談、お仕事のご依頼については、
こちらのお問い合わせページをご確認ください。