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(デリゲート)
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メソッドを作成して、それを渡せばよい。
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の宣言も不要になった。
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型であることがわかるため。
なお、以下の用に書いてもエラーにはならない。
GetData(values, (string s) => s.Length <= 3) ;
パラメータの数によって書式が異なる。
パラメータが0個の場合は()が必要。
() => hoge
パラメータが1個の場合は()は不要。あってもOK。
s => hoge
パラメータが複数の場合は()で囲ってカンマで区切る。
(a,b) => hoge
2-2.式or文(「=>」の右側)
式のみの場合はreturnが不要
GetData(values, s => s.Length <= 3) ;
文の場合は{}の中に記述し、returnが必要
GetData(values, s => { return s.Length <= 3; }) ;
3.FuncとAction
Funcは戻り値あり、Actionは戻り値なし。
Func<T,T,Tresult>で山かっこの最後が戻り値の型。パラメータの数は上限が16個。
Funcが出来たので、Predicateは使わなくなった。
3-1.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.抽出とソート
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));
}
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
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}");
}
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.遅延実行
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.即時実行
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も抽出されるので注意。
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は実体を持っていない。必要になったときに呼ばれて値を返すだけ。