LoginSignup
119
129

More than 3 years have passed since last update.

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

Last updated at Posted at 2018-04-05

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

なぜ私はLINQ(およびラムダ式)が理解できなかったのか?

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

ラムダ式について調べようと、「C# ラムダ式」で検索したら、以下のように書かれているサイトがヒットしました。

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

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

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

LINQ構成する3つの要素

LINQは以下の3つの要素で成り立っています。

  • デリゲートとラムダ式
  • ジェネリック
  • 拡張メソッド

本記事では、名前、数学の点数、物理の点数を保存するクラスのリストから、
数学と物理の平均点を求めるメソッドを作成しながら、上記3要素についてひとつずつ説明していきます。

テスト受験者、数学および物理の点数を保存するTestResultクラスのリストがあります。
resultsには3人分の試験結果のデータ(TestResultクラスのインスタンス)が格納されています。
受験者全員の数学の平均点を求めるメソッドをProgramクラス内に作ってみましょう。

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; }
    }
}

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型を使います。

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

例3のメソッドは、DateTimeとdouble型の値を受け取り、string型の値を返すメソッドです。
戻り値があるメソッドなので、Funcを、引数はふたつなので
Funcを用います。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. と打てば、候補にCalcAverageが出てきます)
第一引数の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メソッドはとても便利だと思いませんか?
平均を求めたい変数を外部から指定でき、どんなクラスでも使える。
こんな便利なメソッド、あらかじめ用意してくれていたらいいのに。

・・・そんな要望を叶えてくれるのがLINQです。
System.Linqをusingし、Mainメソッドに以下の一行を追加してください
double mathAveLINQ = results.Average(tr => tr.Math);
今回作ったCalcAverageメソッドとほぼ同様のメソッドが、既にSystem.Linq.Averageとして存在しているのです。
(厳密にはLINQのAverageメソッドは第一引数がListではなくIEnumerableですが、今回は説明は省略します)
LINQには、その他最大値(Max)、最小値(Min)を求めるメソッドや、Listから特定の条件のものに絞り込むWhere等があります。
それらの紹介は別の記事で紹介するかもしれません。

119
129
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
119
129