1
6

More than 3 years have passed since last update.

C#のラムダ式とLinqついて

Last updated at Posted at 2020-12-31

C#のラムダについて勉強したことをまとめる。
Java8での場合についても記載する。

1. C#のラムダまでの歴史

以下の例題でラムダへの経緯について記述する。

例題:Stringの配列に対して、文字列の長さでデータを抽出する。

ラムダ無し
        private void button2_Click(object sender, EventArgs e)
        {
            var values = new string[] { "a", "bb", "ccc", "dddd", "eeeee" };
            var result = GetData(values);
            Console.WriteLine(string.Join(",", result));
        }

        private string[] GetData(string[] values)
        {
            var result = new List<string>();
            foreach (var val in values)
            {
                if (val.Length > 2)
                {
                    result.Add(val);
                }
            }
            return result.ToArray();
        }
実行結果
ccc,dddd,eeeee

上記の例ではサイズが2より大きいものを抽出しているが、3以下の場合を抽出したい場合に、対応できない。
「if (val.Length > 2)」の2を引数にすることはできるが、以上、以下、などの等号を変更することができない。

1-1.delegate(デリゲート)

delegate

        private void button2_Click(object sender, EventArgs e)
        {
            var values = new string[] { "a", "bb", "ccc", "dddd", "eeeee" };
            var result = GetData(values, Check1);
            Console.WriteLine(string.Join(",", result));
        }

        private string[] GetData(string[] values, LengthCheck check)
        {
            var result = new List<string>();
            foreach (var val in values)
            {
                if (check(val))
                {
                    result.Add(val);
                }
            }
            return result.ToArray();
        }

        delegate bool LengthCheck(string value);

        bool Check1(String value)
        {
            return value.Length > 2;
        }

delegateを使うことにより式を渡すことができるようになった。
そのため、3以下を抽出したい場合は以下の用にCheck2メソッドを作成して、それを渡せばよい。

delegate2
       private void button2_Click(object sender, EventArgs e)
        {
            var values = new string[] { "a", "bb", "ccc", "dddd", "eeeee" };
            var result = GetData(values, Check2);
            Console.WriteLine(string.Join(",", result));
        }

        private string[] GetData(string[] values, LengthCheck check)
        {
            var result = new List<string>();
            foreach (var val in values)
            {
                if (check(val))
                {
                    result.Add(val);
                }
            }
            return result.ToArray();
        }

        delegate bool LengthCheck(string value);

        bool Check1(String value)
        {
            return value.Length > 2;
        }

        bool Check2(String value)
        {
            return value.Length <= 3;
        }

1-2.匿名メソッド

上の例では、メソッドを作る必要があり、メソッド名を考える必要がある。
また、呼び出している「GetData(values, Check2)」と実際の宣言が離れているので、何をやっているかがわかりにくいという問題がある。

匿名メソッド
        private void button2_Click(object sender, EventArgs e)
        {
            var values = new string[] { "a", "bb", "ccc", "dddd", "eeeee" };
            var result = GetData(values, 
                delegate(string s) { return s.Length <= 3; }) ;
            Console.WriteLine(string.Join(",", result));
        }

        private string[] GetData(string[] values, LengthCheck check)
        {
            var result = new List<string>();
            foreach (var val in values)
            {
                if (check(val))
                {
                    result.Add(val);
                }
            }
            return result.ToArray();
        }

        delegate bool LengthCheck(string value);

その問題を解決したのが匿名メソッド。匿名メソッドは.NET 2.0で導入された。

1-3.Predicate(プレディケート)

.NET 2.0でジェネリックが導入され、その結果、bool Predicate<in T>を使えばdelegateの宣言も不要になった。

Predicate
        private void button2_Click(object sender, EventArgs e)
        {
            var values = new string[] { "a", "bb", "ccc", "dddd", "eeeee" };
            var result = GetData(values, 
                delegate(string s) { return s.Length <= 3; }) ;
            Console.WriteLine(string.Join(",", result));
        }

        private string[] GetData(string[] values, Predicate<string> check)
        {
            var result = new List<string>();
            foreach (var val in values)
            {
                if (check(val))
                {
                    result.Add(val);
                }
            }
            return result.ToArray();
        }

1-4.ラムダ式

匿名メソッドで「delegate(string s) { return s.Length <= 3; }」と書いていたのをラムダ演算子「=>]
で書くと以下の用になる。

ラムダ式
        private void button2_Click(object sender, EventArgs e)
        {
            var values = new string[] { "a", "bb", "ccc", "dddd", "eeeee" };
            var result = GetData(values, s => s.Length <= 3) ;
            Console.WriteLine(string.Join(",", result));
        }

        private string[] GetData(string[] values, Predicate<string> check)
        {
            var result = new List<string>();
            foreach (var val in values)
            {
                if (check(val))
                {
                    result.Add(val);
                }
            }
            return result.ToArray();
        }

ラムダ式は.NET 3.0で導入された。
ラムダ演算子は「Goes to」(ゴーズ・トゥ)と読む。
左側がパラメータで右側が式or文

1-5.おまけ

今回のサンプルについては、以下のコードでも同じ結果になる

おまけ
        private void button2_Click(object sender, EventArgs e)
        {
            var values = new string[] { "a", "bb", "ccc", "dddd", "eeeee" };
            var result = values.Where(s => s.Length <= 3);
            Console.WriteLine(string.Join(",", result));
        }

2.ラムダ式の文法

2-1.パラメータ(「=>」の左側)

パラメータの型は書かなくて良い。

以前は「delegate(string s) { return s.Length <= 3; })」のように「delegate」と「string」を書いていたが不要になった。
「GetData(values, s => s.Length <= 3)」の第二パラメータは「private string[] GetData(string[] values, Predicate check)」とstring型であることがわかるため。

なお、以下の用に書いてもエラーにはならない。

型を明記してもOK
GetData(values, (string s) => s.Length <= 3) ;

パラメータの数によって書式が異なる。

パラメータが0個の場合は()が必要。

パラメータ0個
() => hoge

パラメータが1個の場合は()は不要。あってもOK。

パラメータ1個
s => hoge

パラメータが複数の場合は()で囲ってカンマで区切る。

パラメータ複数個
a,b) => hoge

2-2.式or文(「=>」の右側)

式のみの場合はreturnが不要

return不要
GetData(values, s => s.Length <= 3) ;

文の場合は{}の中に記述し、returnが必要

returnあり
GetData(values, s => { return s.Length <= 3; }) ;

3.FuncとAction

Funcは戻り値あり、Actionは戻り値なし。

Func<T,T,Tresult>で山かっこの最後が戻り値の型。パラメータの数は上限が16個。
Funcが出来たので、Predicateは使わなくなった。

3-1.Action

重たいメソッドに対して、開始と終了のログを出力して、非同期で動かすサンプル。

Actionサンプル
        private void button2_Click(object sender, EventArgs e)
        {
            Wrap(Execute);
        }

        private void Execute()
        {
            Console.WriteLine("重たい処理実行");
            System.Threading.Thread.Sleep(5000);
        }

        private async void Wrap(Action action)
        {
            Console.WriteLine("START");
            await Task.Run(() => action());
            Console.WriteLine("END");
        }

4.int型のリストに対しての処理

4-1.抽出とソート

C#
        private void button2_Click(object sender, EventArgs e)
        {
            int[] values = new int[] { 2, 5, 3, 1, 8, 4, 2, 6 };
            // 5以上の値を昇順で抽出
            var result = values.Where(v => v >= 5).OrderBy(v => v);
            Console.WriteLine(string.Join(",", result));
        }
Java
import java.util.Arrays;
import java.util.stream.IntStream;

public class Sample {

    public static void main(String[] args) {
        int[] values = new int[] { 2, 5, 3, 1, 8, 4, 2, 6 };
        // 5以上の値を昇順で抽出
        IntStream stream = Arrays.stream(values);
        stream.filter(v -> v >= 5).sorted().forEach(v -> System.out.print(v + " "));
    }
}

4-2.Max,Min,Average

C#
        private void button2_Click(object sender, EventArgs e)
        {
            int[] values = new int[] { 2, 5, 3, 1, 8, 4, 2 };
            var max = values.Max();
            var min = values.Min();
            var avg = values.Average();
            Console.WriteLine($"Max={max}, Min={min}, Average={avg}");
        }
Java
import java.util.Arrays;

public class Sample {

    public static void main(String[] args) {
        int[] values = new int[] { 2, 5, 3, 1, 8, 4, 2, 6 };
        int max = Arrays.stream(values).max().getAsInt();
        int min = Arrays.stream(values).min().getAsInt();
        double avg = Arrays.stream(values).average().getAsDouble();
        System.out.println(String.format("max=%d, min=%d, average=%f", max, min, avg));
    }
}

5.遅延実行

Linqは遅延実行だが、ちょっとわかりにくくてバグに繋がる可能性もあるので整理する。

サンプルとして、1~10の数値を5倍して、偶数だけを抽出し、その合計を出すプログラムを作成した。

5-1.遅延実行

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    class Class1
    {
        internal static void Main(string[] args)
        {
            int[] numbers = Enumerable.Range(1, 10).ToArray();

            IEnumerable<int> number5 = numbers.Select(n =>
            {
                Console.WriteLine($"{n}を5倍にします。");
                return n * 5;
            });

            Console.WriteLine("5倍処理終了?");

            IEnumerable<int> even = number5.Where(n =>
            {
                Console.WriteLine($"{n}を偶数か判定します。");
                return n % 2 == 0;
            });

            Console.WriteLine("偶数かの判定終了?");

            int sum = even.SumNumber();
            Console.WriteLine($"合計は{sum}");
        }
    }

    public static class SumSample
    {
        public static int SumNumber(this IEnumerable<int> numbers)
        {
            Console.WriteLine("Sum処理開始");
            int sum = 0;
            foreach(var n in numbers)
            {
                sum += n;
                Console.WriteLine($"{n}を足します。(sum:{sum})");
            }
            Console.WriteLine("Sum処理終了");
            return sum;
        }
    }
}
結果
5倍処理終了?
偶数かの判定終了?
Sum処理開始
1を5倍にします。
5を偶数か判定します。
2を5倍にします。
10を偶数か判定します。
10を足します。(sum:10)
3を5倍にします。
15を偶数か判定します。
4を5倍にします。
20を偶数か判定します。
20を足します。(sum:30)
5を5倍にします。
25を偶数か判定します。
6を5倍にします。
30を偶数か判定します。
30を足します。(sum:60)
7を5倍にします。
35を偶数か判定します。
8を5倍にします。
40を偶数か判定します。
40を足します。(sum:100)
9を5倍にします。
45を偶数か判定します。
10を5倍にします。
50を偶数か判定します。
50を足します。(sum:150)
Sum処理終了
合計は150

ループの中でConsoleに出力するようにしているが、「最初に値を5倍にする処理を行って、それが終わってから抽出する処理を行って・・・」という動きになっていない。
SumNumber処理で必要になってから処理が実行されている。(遅延実行)

5-2.即時実行

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    class Class1
    {
        internal static void Main(string[] args)
        {
            int[] numbers = Enumerable.Range(1, 10).ToArray();

            IEnumerable<int> number5 = numbers.Select(n =>
            {
                Console.WriteLine($"{n}を5倍にします。");
                return n * 5;
            }).ToArray();   // 即時実行

            Console.WriteLine("5倍処理終了?");

            IEnumerable<int> even = number5.Where(n =>
            {
                Console.WriteLine($"{n}を偶数か判定します。");
                return n % 2 == 0;
            }).ToArray();   // 即時実行

            Console.WriteLine("偶数かの判定終了?");

            int sum = even.SumNumber();

            Console.WriteLine($"合計は{sum}");
        }
    }

    public static class SumSample
    {
        public static int SumNumber(this IEnumerable<int> numbers)
        {
            Console.WriteLine("Sum処理開始");
            int sum = 0;
            foreach(var n in numbers)
            {
                sum += n;
                Console.WriteLine($"{n}を足します。(sum:{sum})");
            }
            Console.WriteLine("Sum処理終了");
            return sum;
        }
    }
}

結果
1を5倍にします。
2を5倍にします。
3を5倍にします。
4を5倍にします。
5を5倍にします。
6を5倍にします。
7を5倍にします。
8を5倍にします。
9を5倍にします。
10を5倍にします。
5倍処理終了?
5を偶数か判定します。
10を偶数か判定します。
15を偶数か判定します。
20を偶数か判定します。
25を偶数か判定します。
30を偶数か判定します。
35を偶数か判定します。
40を偶数か判定します。
45を偶数か判定します。
50を偶数か判定します。
偶数かの判定終了?
Sum処理開始
10を足します。(sum:10)
20を足します。(sum:30)
30を足します。(sum:60)
40を足します。(sum:100)
50を足します。(sum:150)
Sum処理終了
合計は150

即時実行するためにはToArrayやToListを付ければよい。

5-3.遅延実行の注意点

直観的にわかりにくい。以下の例では2,4,6,8という値が入ったリストに対して5以上のものを抽出している。
6と8だけ抽出されるのではなく、そのあとに追加された7も抽出されるので注意。

C#
        private void button1_Click(object sender, EventArgs e)
        {
            List<int> list = new List<int> { 2, 4, 6, 8 };
            var result = list.Where(x => x > 5);
            list.Add(3);
            list.Add(7);
            Console.WriteLine(string.Join(",", result));
        }
結果
6,8,7

5-4.遅延実行のメリット

5-4-1.メモリ使用量が少なくて済む

5-1にあるnumber5やevenは実体を持っていない。必要になったときに呼ばれて値を返すだけ。

1
6
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
1
6