122
132

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C# LINQ・ラムダ式をわかりやすく解説

Last updated at Posted at 2018-04-05

はじめに

ラムダ式・LINQが全く分からなくて困っている方に向けて、ラムダ式の仕組みを理解し、LINQが使えるようになるための記事です。
ラムダ式・LINQを勉強して理解するまでの過程をかみ砕いてわかりやすく説明しました。

この記事を書くにあたって、何故ラムダ式・LINQを理解できなかったのかを考えてみました。

  • 自分の関わった案件でLINQをほとんど使用していなかった
  • 使用していたとしても、どこかのサイトから理解せずにコピペされている
  • 周りに理解している、積極的に使っている人がいない
  • => に禍々しさを感じる
  • for文を使えばラムダ式なんか使わなくても大体の事はできてしまう
  • ググっても理解してる人向けの御託を並べたページばかりヒットする

ラムダ式を理解するために「C# ラムダ式」でgoogle検索したら、以下のように書かれているサイトがヒットしました。

ラムダ式は、 デリゲート 型または 式ツリー 型を作成するために使用できる 匿名関数 です。
ラムダ式を使用すると、引数として渡したり>関数呼び出しの結果値として返すことができる
ローカル関数を記述できます。 ラムダ式は、LINQ クエリ式を記述する場合に特に便利です。

引用元:ラムダ式 (C# プログラミング ガイド)

「そもそもラムダ式が海のものとも山のものともわかってない私」にとって、これらのサイトを見ても全く理解できませんでした。
他のサイトも色々見たものの、「LINQが登場する前のC#しか知らない・書けない」人向けのLINQの記事というのがあまりに少なすぎると感じました。(私が調べた2014年年末当時)
そこで、本記事は「ラムダ式の仕組みを理解し、LINQを使えるようになる」事を目標としました。
遅延実行や式ツリーの話を省略し、理解しやすさ・分かりやすさを最優先しました。遅延実行や式ツリーの話はこの記事の内容を理解してから他の記事・サイトで勉強しても遅くないからです。
記事自体は長めですが、大半はコードとなっています。コードをコンソールアプリケーションのプロジェクトにコピペして、デバッグを行いながら記事を読み進めることを強くお勧めします。

サンプルコードで学ぶLINQ

LINQ構成する3つの要素

LINQは以下の3つの要素で構成されています。
1. デリゲートとラムダ式
2. ジェネリック
3. 拡張メソッド

テストの平均点を求めるサンプルコード

受験者の氏名、数学の点数、物理の点数を保存するクラスと、クラスのリストがあります。
数学と物理の平均点を求めるメソッドを作成し、Main関数から呼び出します。

Program.cs
using System;
using System.Collections.Generic;

namespace ConsoleApplication5
{
    class Program
    {
        static void Main(string[] args)
        {
            List<TestResult> results = new List<TestResult>()
            {
                new TestResult(){ Name = "Suzuki Ichiro", Math = 80, Physics = 60 },
                new TestResult(){ Name = "Akagawa jiro", Math = 70, Physics = 90 },
                new TestResult(){ Name = "Mikawa Saburo", Math = 90, Physics = 100 },
            };
            // ここで、受験者全員の数学の平均点を求めたい
        }
    }

    /// <summary>
    /// 受験者の氏名と試験結果のクラス
    /// </summary>
    public class TestResult
    {
        /// <summary> テストを受けた者の名前 </summary>
        public string Name { get; set; }

        /// <summary> 数学の点数 </summary>
        public double Math { get; set; }

        /// <summary> 物理の点数 </summary>
        public double Physics { get; set; }
    }
}

テスト受験者、数学および物理の点数を保存するTestResultクラスのリストがあります。resultsには3人分の試験結果のデータ(TestResultクラスのインスタンス)が格納されています。

受験者全員の数学の平均点を求めるメソッドをProgramクラス内に作ります。Programクラスに以下のメソッドを追加します


        /// <summary>
        /// 受験者全員の数学の平均点を求めるメソッド
        /// </summary>
        /// <param name="result"></param>
        /// <returns></returns>
        static double CalcMathAverage(List<TestResult> results)
        {
            double ans = 0;
            foreach (TestResult tr in results)
            {
                double num = tr.Math;
                ans += num;
            }
            ans /= results.Count;
            return ans;
        }

Main関数を修正し、追加したメソッドを呼ぶ

        static void Main(string[] args)
        {
            List<TestResult> results = new List<TestResult>()
            {
                new TestResult(){ Name = "Suzuki Ichiro", Math = 80, Physics = 60 },
                new TestResult(){ Name = "Akagawa jiro", Math = 70, Physics = 90 },
                new TestResult(){ Name = "Mikawa Saburo", Math = 90, Physics = 100 },
            };

            // ここで、受験者全員の数学の平均点を求めたい
            double mathAve = CalcMathAverage(results);
        }

数学の平均点を求めるメソッドが完成しました。では、同様に物理の点数の平均点を求めるメソッドはどのようにして作ればよいでしょうか?


        /// <summary>
        /// 受験者全員の物理の平均点を求めるメソッド
        /// </summary>
        /// <param name="result"></param>
        /// <returns></returns>
        static double CalcPhysicsAverage(List<TestResult> results)
        {
            double ans = 0;
            foreach (TestResult tr in results)
            {
                double num = tr.Physics;
                ans += num;
            }
            ans /= results.Count;
            return ans;
        }

double num = tr.Physics;以外の行は数学の平均点を求めるメソッドと全く同じです。今後科目数が増えると、その都度コピペして似たようなメソッドを追加するのでしょうか。
もしこのメソッドにバグや仕様変更があり修正が入った場合、コピペした全てのメソッドを修正する必要が出てきます。何かいい方法はないでしょうか・・・
そんな時に使えるのが「デリゲート」です。

デリゲートとラムダ式

デリゲートとは

デリゲートを使うと、メソッドを参照することができる

デリゲートとは、「メソッドを参照する型」で、代表的なものに、ActionとFuncがあります。
Actionは戻り値がvoid型のメソッドを参照するため、Funcは戻り値がvoid型以外のメソッドを参照するためのものです。

using System.Collections.Generic;
using System;

namespace ConsoleApplication5
{
    class Program
    {
        static void Main(string[] args)
        {
            // 例1 メソッドHelloを参照するAction型の変数helloに、Helloを参照させる
            Action hello = Hello;
            hello();  // 参照したメソッドを呼び出す

            // 例2 メソッドAddOneを参照するFunc<int, int>型の変数addに、AddOneを参照させる
            Func<int, int> add = AddOne;
            int num = 100;
            num = add(num);  // 参照したメソッドを呼び出す
            Console.WriteLine(num);

            // 例3 メソッドDisplayNikkeiを参照するFunc<DateTime, double, string>型の変数に、DisplayNikkeiを参照させる
            Func<DateTime, double, string> TodaysNikkei = DisplayNikkei;
            Console.WriteLine(TodaysNikkei(DateTime.Today, 23456.7)); // 参照したメソッドを呼び出す

        }

        /// <summary>
        /// 例1 引数なし、戻り値なしのメソッド
        /// </summary>
        static void Hello()
        {
            Console.WriteLine("Hello, World!!");
        }

        /// <summary>
        /// 例2 int型の数値を受け取り、1加えた値を返すメソッド
        /// </summary>
        /// <param name="num">任意の数値</param>
        /// <returns>任意の数値に1加えた値</returns>
        static int AddOne(int num)
        {
            int ret = num + 1;
            return ret;
        }

        /// <summary>
        /// 例3 ある日の日経平均株価の終値を知らせるための文字列を返すメソッド
        /// </summary>
        /// <param name="dt">日付</param>
        /// <param name="price">金額</param>
        /// <returns>表示する内容の文字列</returns>
        static string DisplayNikkei(DateTime dt, double price)
        {
            string str = dt.ToShortDateString() + " の日経平均株価終値は" + price.ToString() + "円です。";
            return str;
        }
    }
}

例1のメソッドは、引数・戻り値のないメソッドです。
戻り値がないメソッドなので、Actionを使います。
Action hello = Hello;とすれば、戻り値・引数のないメソッドHellohelloで参照することができます。

例2のメソッドは、int型の数値を受け取り、int型の数値を返すメソッドです。
戻り値があるメソッドなので、Funcを使います。引数もひとつ存在するので、Func<T,TResult>を用います。引数Tはint型、戻り値TResultもint型なので、Func<int, int>となります。
Func<int, int> add = AddOne;とすれば、addAddOneを参照し、addをメソッドのように使うことができます。

例3のメソッドは、DateTimeとdouble型の値を受け取り、string型の値を返すメソッドです。
戻り値があるメソッドなので、Funcを、引数はふたつなので
Func<T1, T2, TResult>を用います。引数1はDateTime、引数2はdouble、戻り値はstringなので、Func<DateTime, double, string>となります。
Funcは引数が多くなっても、最後が戻り値の型となります。

ラムダ式とは

ラムダ式を使うと、メソッド内部でメソッドを定義できる

メソッドの内部でもメソッドを定義することが書き方です。
デリゲートのサンプルソースでは、例1~例3までそれぞれメソッドを定義していましたが、小さいメソッドを名前付きで一つずつ定義するのは大変なので、メソッドそのものをMainメソッド内部で定義します。

using System.Collections.Generic;
using System;

namespace ConsoleApplication5
{
    class Program
    {
        static void Main(string[] args)
        {
            // 例1
            Action hello = () => { Console.WriteLine("Hello, World!!"); };
            hello();

            // 例2
            Func<int, int> add = (n) => { return n + 1; };
            int num = 100;
            num = add(num);
            Console.WriteLine(num);

            // 例3
            Func<DateTime, double, string> TodaysNikkei = (dt, price) => { return dt.ToShortDateString() + " の日経平均株価終値は" + price.ToString() + "円です。"; };
            Console.WriteLine(TodaysNikkei(DateTime.Today, 23456.7));
        }
    }
}

いきなり=>が出てきて面食らったと思います。順を追って見ていきましょう。

例1について
修正前は
Action hello = Hello;
として、Helloメソッドを参照しました。

修正後は
Action hello = () => { Console.WriteLine("Hello, World!!"); };
と、Helloメソッドの中身まで一行で書いています。

(参考)ラムダ式を使わない場合のHelloメソッド

static void Hello()
{
    Console.WriteLine("Hello, World!!");
}

ラムダ式の=>の左側は引数、右側はメソッドで行う処理を書きます。
Helloメソッドは引数がないので、()としています。引数がある場合は例2、例3で見ていきます。

例2について
Func<int, int> add = (n) => { return n + 1; };
int型の引数nを受け取って(=>の左側)、n + 1を返す(=>の右側)ラムダ式を、addに参照させています。
例2については、引数が1つだけなので、nについているカッコは省略できます。また、右辺も一行なので、括弧・returnを省略して、
Func<int, int> add = n => n + 1;
と書くこともできます。

例3について
Func<DateTime, double, string> TodaysNikkei = (dt, price) => { return dt.ToShortDateString() + " の日経平均株価終値は" + price.ToString() + "円です。"; };
考え方は例2と同様です。引数が2つになったバージョンです。

デリゲート・ラムダ式についてより詳しく学びたい方は、是非下の記事をご覧ください。

プログラム修正

デリゲートとラムダ式の説明が長くなりましたが、話を元に戻して数学と物理の平均点を求めるメソッドをどのように修正するかを再び考えます。

        static double CalcMathAverage(List<TestResult> results)
        {
            double ans = 0;
            foreach (TestResult tr in results)
            {
                double num = tr.Math;
                ans += num;
            }
            ans /= results.Count;
            return ans;
        }

double num = tr.Math;の部分を、デリゲートを使って処理を呼び元で変更します。
TestResultクラスから、double型の数値を抜き出すデリゲートを引数に追加します。
数学以外の平均点を求めることもできるようになったので、メソッド名を変更しました。

        static double CalcAverage(List<TestResult> results, Func<TestResult, double> func)
        {
            double ans = 0;
            foreach (TestResult tr in results)
            {
                ans += func(tr);
            }
            ans /= results.Count;
            return ans;
        }

併せて、呼び元も変更する必要があります。

// 受験者全員の数学の平均点を求めたい
double mathAve = CalcMathAverage(results, tr => tr.Math);

ここでのtr => tr.Mathは、以下のメソッドと同じ意味です。

static double funcMath(TestResult tr)
{
    return tr.Math;
}

これで、数学・物理だけでなく、今後追加される可能性のある科目は全てCalcAverageメソッドを使う事によって平均点を求めることができるようになりました。
ラムダ式とデリゲードを使った結果、サンプルソースは以下のようになりました。

using System.Collections.Generic;
using System;

namespace ConsoleApplication5
{
    class Program
    {
        static void Main(string[] args)
        {
            List<TestResult> results = new List<TestResult>()
            {
                new TestResult(){ Name = "Suzuki Ichiro", Math = 80, Physics = 60 },
                new TestResult(){ Name = "Akagawa jiro", Math = 70, Physics = 90 },
                new TestResult(){ Name = "Mikawa Saburo", Math = 90, Physics = 100 },
            };

            // 受験者全員の数学の平均点を求めたい
            double mathAve = CalcAverage(results, tr => tr.Math);
        }


        /// <summary>
        /// 受験者全員の平均点を求める
        /// </summary>
        /// <param name="results"></param>
        /// <param name="func"></param>
        /// <returns></returns>
        static double CalcAverage(List<TestResult> results, Func<TestResult, double> func)
        {
            double ans = 0;
            foreach (TestResult tr in results)
            {
                ans += func(tr);
            }
            ans /= results.Count;
            return ans;
        }
    }

    /// <summary>
    /// 受験者の氏名と試験結果のクラス
    /// </summary>
    public class TestResult
    {
        /// <summary> テストを受けた者の名前 </summary>
        public string Name { get; set; }

        /// <summary> 数学の点数 </summary>
        public double Math { get; set; }

        /// <summary> 物理の点数 </summary>
        public double Physics { get; set; }
    }
}

ジェネリック

ジェネリックとは

ジェネリックを使うと、クラスやメソッドを様々な型で使うことができる

double型であればどの変数でも平均を求められる便利なメソッドが完成しました。しかし、このメソッドはあくまでTestResultクラスにしか使えません。
ジェネリックを用いて、TestResult以外のクラスでも使えるようにしましょう。
ジェネリックを用いれば、クラスやメソッドを型を特定せずに使用できます。
List<T>クラスはジェネリックをあまり気にせず使っている方も多いはず。
List<T>は、List<string>List<int>List<TestResult>等、あらゆるクラスで使用することができます。

プログラム修正

CalcAverageメソッドをジェネリックに対応します。"TestResult"を"T"に置き換えてみます。

        static double CalcAverage(List<T> results, Func<T, double> func)
        {
            double ans = 0;
            foreach (T tr in results)
            {
                ans += func(tr);
            }
            ans /= results.Count;
            return ans;
        }

すると、以下のようなエラーが発生します。

エラー 型または名前空間名 'T' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足しています。

これだけでは不十分です。メソッド名の最後に<T>を付加しなければいけません。

        static double CalcAverage<T>(List<T> results, Func<T, double> func)
        {
            double ans = 0;
            foreach (T tr in results)
            {
                ans += func(tr);
            }
            ans /= results.Count;
            return ans;
        }

これでエラーが無くなりました。呼び元はTの型を自動的に推測するので、修正は不要です。このメソッドはクラスに依存することなく、さまざまなクラスで使うことが出来ます。

拡張メソッド

拡張メソッドとは

クラスそのものを変更することなく、クラスにメソッドを追加する

拡張メソッドを使うと、ある型に対して型自体を変更することなくメソッドを追加することができます。
今回は、List<T>型に直接、平均を求めるメソッドを拡張メソッドを用いて追加します。
文章だけでは伝わらないと思うので、実際にプログラムを修正しながら見ていきましょう。

拡張メソッドについてより詳しく学びたい方は、是非下の記事をご覧ください。

プログラム修正

せっかく作成した便利なメソッドも、このままではProgramクラス内でしか使えないので、別のクラスに移動させて、様々なクラスから使えるように修正します。
今回は新たに、ListUtilという名前の静的クラスを作成し、平均を求めるメソッドを移動します。

    public static class ListUtil
    {
        /// <summary>
        /// 受験者全員の平均点を求める
        /// </summary>
        /// <param name="results"></param>
        /// <param name="func"></param>
        /// <returns></returns>
        public static double CalcAverage<T>(List<T> results, Func<T, double> func)
        {
            double ans = 0;
            foreach (T tr in results)
            {
                ans += func(tr);
            }
            ans /= results.Count;
            return ans;
        }
    }

同時に呼び元も修正します。

// 受験者全員の数学の平均点を求めたい
double mathAve = ListUtil.CalcAverage(results, tr => tr.Math);

このままでもよいのですが、これだとListUtilクラスの存在を知らないとメソッドが使えないので、拡張メソッドにしてみましょう。

        /// <summary>
        /// 平均を求める
        /// </summary>
        /// <param name="results">平均を求めるリスト</param>
        /// <param name="func">リストのクラスから平均を求めたい要素を選択する</param>
        /// <returns>選択した要素の平均</returns>
        public static double CalcAverage<T>(this List<T> results, Func<T, double> func)
        {
            double ans = 0;
            foreach (T tr in results)
            {
                ans += func(tr);
            }
            ans /= results.Count;
            return ans;
        }

静的クラス内の静的メソッドの第一引数の前にthisをつけると、拡張メソッドになります。
呼び元はそのままでもよいのですが、以下のように変更することも可能です。

// 受験者全員の数学の平均点を求めたい
double mathAve = results.CalcAverage(tr => tr.Math);

まるで、List<T>クラスにあるメソッドのように使うことができます。
これだと、クラス名ListUtilを知らなくてもこのメソッドがIntelliSenseで表示されます。具体的には、results.と打てば、候補にalcAverageが出てきます。
第一引数のresultsが先頭に来るため、引数は一つだけとなります

平均を求める便利なメソッドが完成

最初はTestResultクラスリストから、double型のプロパティ(数学と物理の点数)の平均点を求めるためのメソッドを作っていましたが、ラムダ式を使いdouble型であればどんなプロパティでも平均を求められるように修正し、ジェネリックを使いクラスを限定せずに様々なクラスから使用できるようにしました。

最終的なソースコードは以下のようになりました。

using System.Collections.Generic;
using System;

namespace ConsoleApplication5
{
    class Program
    {
        static void Main(string[] args)
        {
            List<TestResult> results = new List<TestResult>()
            {
                new TestResult(){ Name = "Suzuki Ichiro", Math = 80, Physics = 60 },
                new TestResult(){ Name = "Akagawa jiro", Math = 70, Physics = 90 },
                new TestResult(){ Name = "Mikawa Saburo", Math = 90, Physics = 100 },
            };

            // 受験者全員の数学の平均点を求めたい
            double mathAve = results.CalcAverage(tr => tr.Math);
        }
    }

    /// <summary>
    /// 受験者の氏名と試験結果のクラス
    /// </summary>
    public class TestResult
    {
        /// <summary> テストを受けた者の名前 </summary>
        public string Name { get; set; }

        /// <summary> 数学の点数 </summary>
        public double Math { get; set; }

        /// <summary> 物理の点数 </summary>
        public double Physics { get; set; }
    }

    /// <summary>
    /// 今回作成したList<T>のユーティリティクラス
    /// </summary>
    public static class ListUtil
    {
        /// <summary>
        /// 平均を求める
        /// </summary>
        /// <param name="results">平均を求めるリスト</param>
        /// <param name="func">リストのクラスから平均を求めたい要素を選択する</param>
        /// <returns>選択した要素の平均</returns>
        public static double CalcAverage<T>(this List<T> results, Func<T, double> func)
        {
            double ans = 0;
            foreach (T tr in results)
            {
                ans += func(tr);
            }
            ans /= results.Count;
            return ans;
        }
    }
}

今回作成したCalcAverage<T>メソッドはとても便利だと思いませんか?
平均を求めたい変数を外部から指定でき、どんなクラスでも使える。
こんな便利なメソッド、あらかじめ用意してくれていたらいいのに。

LINQ=便利なメソッドのライブラリ

便利なメソッドのライブラリがLINQです。
System.Linqをusingし、Mainメソッドに以下の一行を追加してください
double mathAveLINQ = results.Average(tr => tr.Math);
今回作ったCalcAverageメソッドとほぼ同様のメソッドが、既にSystem.Linq.Averageとして存在しています。厳密にはLINQのAverageメソッドは第一引数がList<T>ではなくIEnumerable<TSource>ですが、今回は説明を省略します。

LINQには、その他最大値(Max)、最小値(Min)を求めるメソッドや、Listから特定の条件のものに絞り込むWhere等があります。
それらの紹介は別の記事で紹介するかもしれません。

122
132
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
122
132

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?