C#のラムダ式を分かりやすく解説する
はじめに
何回教材読んでもすぐにわすれてしまうラムダ式。そんなラムダ式を 1 からできる限り丁寧に解説していきます。
超簡単にラムダ式とは
ものすごく簡単にいうとその場で使うだけの関数をその場でチャッと作っちゃおう というものです。
超簡単にラムダ式のメリット
その時しか使わないような関数をいちいち定義して名前つけるの面倒くさい~ という怠惰を叶えてくれるというメリットがあります。
ラムダ式を書く前に
ラムダ式をいきなり書いて理解しようとしてもわけわからなくなることが多いです。そこでただの式から少しずつラムダ式に進化させていきましょう。今回は文字列型の配列を用意し、A が入っている値だけにフィルターをして文字列型のリストに格納する という処理を例に進めていきます。
なお、クラスから外側は省略しています。
第一ステップ 手続き型で書く
まずは手続き型で処理を書いていきましょう。
コード
var data = new string[] { "A", "BB", "CCC", "DDDD", "ABCDE" };
var result = new List<string>();
foreach (var value in data)
{
if (value.Contains("A"))
{
result.Add(value);
}
}
Console.WriteLine("出力結果:" + String.Join(",", result));
出力結果:A, ABCDE
解説
非常にシンプルな形です。配列とリストを用意し、foreach で配列を回す。その中でAが入っているモノを探して、true のときだけリストに入れる というながれになります。
第二ステップ メソッドによって共通化する
次は共通化をしていきましょう
コード
public static void Main(string[] args)
{
var data = new string[] { "A", "BB", "CCC", "DDDD", "ABCDE" };
var result = GetValue(data, "A");
Console.WriteLine("出力結果:" + String.Join(",", result));
}
private static string[] GetValue(string[] data, string str)
{
var result = new List<string>();
foreach (var value in data)
{
if (value.Contains(str))
{
result.Add(value);
}
}
return result.ToArray();
}
出力結果:A, ABCDE
解説
ループ処理のところを GetValue メソッドを用意することで共通化しました。特に大きな変化はありませんね。
ただこれのおかげで好きな配列から好きな文字列でフィルターできるようになりました。
ここまではプログラミングの基本の部分です。
さて、いろいろ処理を書くようになると「GetValue の内の if 条件の中身も呼び出すたびに変えてえなあ」となるときもあります。しかし、if 条件の中身の数だけ GetValue のようなメソッドを定義するのは非効率となります。となると、if 条件の中身のメソッドも引数から受け取れれば便利じゃないかということが分かります。
第三ステップ 引数としてメソッドを渡す(デリゲート)
そこで遂にデリゲートの登場です。デリゲートとは超簡単に言うとメソッドを引数として渡せるようにする宣言 だと思ってください。
ココからはわかりやすいように日本語でコードを書いていきます。
コード
delegate bool デリゲート宣言名(string value);
private static bool デリゲートによって渡されるメソッド(string value)
{
return value.Contains("A");
}
private static string[] GetValue(string[] data, デリゲート宣言名 デリゲートによって渡されるメソッドの名前)
{
var result = new List<string>();
foreach (var value in data)
{
if (デリゲートによって渡されるメソッドの名前(value))
{
result.Add(value);
}
}
return result.ToArray();
}
public static void Main(string[] args)
{
// ラムダナシ
var data = new string[] { "A", "BB", "CCC", "DDDD", "ABCDE" };
var result = GetValue(data, デリゲートによって渡されるメソッド);
Console.WriteLine("出力結果:" + String.Join(",", result));
}
出力結果:A, ABCDE
解説
① まずデリゲートの宣言をします。引数として渡したいメソッドの定義をする場所だと思ってください。今回は if 条件の中に入れるので引数に文字列を受け取り、bool を返すメソッドを引数として渡す というデリゲートを定義します。
② 次にデリゲートによって渡される(引数として渡したい)メソッドを定義します。ここで注意するべきなのが、デリゲートで定義した通りにメソッドを作る必要があります。なので、引数に文字列を受け取り、戻り値が bool であるメソッドを作成します。処理自体はなんでも大丈夫です。
③ その次に実際にメソッドを引数として設定していきます。まず、メソッドの引数にデリゲート宣言名を型とした引数名を入れます(今回はメソッドの名前と書きましたが、引数なのでなんでも大丈夫です)。そして、実際にメソッドを渡したい場所= if 条件の中に引数名を書きます。このとき、渡されるメソッドは文字列を引数として受け取るため、渡すべき文字列をしっかり書いてあげましょう。
④ 最後に処理の呼び元です。GetValue を呼び出す際に、先ほど ② で作成したメソッド名を引数として入れてあげます。
これでデリゲートの完成です!
これで例えば次のように if の中身を動的に変えることができます。
delegate bool デリゲート宣言名(string value);
private static bool デリゲートによって渡されるメソッド1(string value)
{
return value.Contains("A");
}
private static bool デリゲートによって渡されるメソッド2(string value)
{
return value.Contains("B");
}
private static bool デリゲートによって渡されるメソッド3(string value)
{
return value.Length >= 3;
}
private static string[] GetValue(string[] data, デリゲート宣言名 デリゲートによって渡されるメソッドの名前)
{
var result = new List<string>();
foreach (var value in data)
{
if (デリゲートによって渡されるメソッドの名前(value))
{
result.Add(value);
}
}
return result.ToArray();
}
public static void Main(string[] args)
{
// ラムダナシ
var data = new string[] { "A", "BB", "CCC", "DDDD", "ABCDE" };
var result1 = GetValue(data, デリゲートによって渡されるメソッド1);
var result2 = GetValue(data, デリゲートによって渡されるメソッド2);
var result3 = GetValue(data, デリゲートによって渡されるメソッド3);
Console.WriteLine("出力結果1:" + String.Join(",", result1));
Console.WriteLine("出力結果2:" + String.Join(",", result2));
Console.WriteLine("出力結果3:" + String.Join(",", result3));
}
出力結果1:A, ABCDE
出力結果2:B, ABCDE
出力結果3:CCC, DDDD, ABCDE
第五ステップ 匿名メソッド
さて、デリゲートが完成してだいぶ融通が効くようになってきました。しかし、その場で使うだけのデリゲートによって渡されるメソッドをいちいち定義しなければならないのは非常に面倒ですし、メソッド名もつけにくくなってきます。
そこで、いちいちメソッド定義をせずに引数に直接ロジックを書き込む匿名メソッドを使います。
コード
delegate bool デリゲート宣言名(string value);
private static string[] GetValue(string[] data, デリゲート宣言名 デリゲートによって渡されるメソッド)
{
var result = new List<string>();
foreach (var value in data)
{
if (デリゲートによって渡されるメソッド(value))
{
result.Add(value);
}
}
return result.ToArray();
}
public static void Main(string[] args)
{
var data = new string[] { "A", "BB", "CCC", "DDDD", "ABCDE" };
var result1 = GetValue(
data,
// デリゲートによって渡されるメソッド
delegate(string value) { return value.Contains("A"); }
);
Console.WriteLine("出力結果1:" + String.Join(",", result1));
}
出力結果1:A, ABCDE
解説
かなりスッキリしました。先ほどと同じようにまずデリケート宣言を行います。GetValue メソッドの引数にデリケートを入れるのも同様です。GetValue の呼び元を見てみましょう。第二引数にロジックが直接埋め込んであるのが分かるかと思います。書き方は、デリケート宣言をして、引数、そしてロジック という順番で書きます。見た目の通り、名前のないメソッドなので匿名メソッドと言います。
戻り値は指定しなくていいのか?という疑問があるかもしれないですが、return の型推論によって判断されるため不要になります。
第六ステップ ラムダ式
さて、ここまででだいぶ記述量がへりました。が、いちいち引数に delegate 宣言するのも引数を書くのも面倒臭い なんてワガママがでるかもしれません。そこでいよいよ登場するのがラムダ式です。
コード
delegate bool デリゲート宣言名(string value);
private static string[] GetValue(string[] data, デリゲート宣言名 デリゲートによって渡されるメソッド)
{
var result = new List<string>();
foreach (var value in data)
{
if (デリゲートによって渡されるメソッド(value))
{
result.Add(value);
}
}
return result.ToArray();
}
public static void Main(string[] args)
{
var data = new string[] { "A", "BB", "CCC", "DDDD", "ABCDE" };
var result1 = GetValue(data,value => value.Contains("A"));
Console.WriteLine("出力結果1:" + String.Join(",", result1));
}
出力結果1:A, ABCDE
解説
GetValue の呼び元の第二引数がかなり短くなりました。順番に見ていきましょう
⓪ 最初に以下は匿名メソッドです。
var result1 = GetValue(
data,
// デリゲートによって渡されるメソッド
delegate(string value) { return value.Contains("A"); }
);
① まず引数内の delegate 宣言と匿名メソッド内の引数の型を消します
(value) { return value.Contains("A"); }
② 次に引数とリターン文の間に=>(goes to)を入れます
(value) => { return value.Contains("A"); }
③ 最後に(){}; return を消します
value => value.Contains("A")
これでラムダ式の完成です。
ちなみにパラメーター(今回で言うところの value)の数や処理の長さによって書き方が少し変わります
① パラメーターなし(引数ナシの場合)
() だけ書きます
() => Console.WriteLine("ナシ")
② パラメーター 1 つ
()なしで書けます
value => value.Contains("A")
③ パラメーター 2 つ以上
()をつけてカンマ区切りで書きます
(value,str) => value.Contains(str)
④ 処理が二行以上になる場合
goes to の後に{}を付け、returnをつけます
(value,str) =>
{
if (value[0] == 'A')
{
return value.Contains(str);
}
return false;
}
応用ステップ 1 Predicate
今回のように戻り値が bool であるメソッドをデリゲートしたい場合は、プレディケートを使用することでわざわざデリゲート宣言をしなくてもよくなります。
コード
private static string[] GetValue(string[] data, Predicate<string> プレディケート)
{
var result = new List<string>();
foreach (var value in data)
{
if (プレディケート(value))
{
result.Add(value);
}
}
return result.ToArray();
}
public static void Main(string[] args)
{
var data = new string[] { "A", "BB", "CCC", "DDDD", "ABCDE" };
var result1 = GetValue(data,value => value.Contains("A"));
Console.WriteLine("出力結果1:" + String.Join(",", result1));
}
出力結果1:A, ABCDE
解説
最初のデリゲート宣言が消えました。その代わりに GetValue メソッド内でプレディケート宣言をしています。
プレディケートは戻り値が bool であればどのような引数でも(カスタムクラスでも)メソッドとして受け取る事のできる宣言になります。今回は文字列を受け取りたいので、string を設定しています。
応用ステップ 2 Func
プレディケートは戻り値が bool の時しか使えないという弱点があります。とうぜん bool 以外の戻り値がほしいときもあるはずです。そこで Func を使用します。
コード
private static string[] GetValue(string[] data, Func<string, bool> ファンクで渡されるメソッド)
{
var result = new List<string>();
foreach (var value in data)
{
if (ファンクで渡されるメソッド(value))
{
result.Add(value);
}
}
return result.ToArray();
}
public static void Main(string[] args)
{
var data = new string[] { "A", "BB", "CCC", "DDDD", "ABCDE" };
var result1 = GetValue(data, value => value.Contains("A"));
Console.WriteLine("出力結果1:" + String.Join(",", result1));
}
解説
プレディケートで記載していた箇所が Func に置き換えられました。Func は<>内の最後のパラメーターを戻り値と判断し、それまでのパラメーターをすべて引数と判断します。例えば今回であれば string が引数で、bool が戻り値です。あとの書き方は Predicate などと同じですね。
実際に戻り値を bool 以外にして確認してみましょう。
コード
private static string[] GetValue(string[] data, Func<string, int> ファンクで渡されるメソッド)
{
var result = new List<string>();
foreach (var value in data)
{
if (ファンクで渡されるメソッド(value) == 2)
{
result.Add(value);
}
}
return result.ToArray();
}
public static void Main(string[] args)
{
var data = new string[] { "A", "BB", "CCC", "DDDD", "ABCDE" };
var result1 = GetValue(data, value => value.Length);
Console.WriteLine("出力結果1:" + String.Join(",", result1));
}
出力結果1:BB
応用ステップ3 Action
Funcは便利ですが、戻り値が必須という制約があります。当然戻り値がない場合のメソッドを渡したいという状況もあるはずです。そこで使えるのがFuncです。
Funcは好きな引数を受け取り、戻り値を返さないメソッドを受け取ることができるようになります。
コード
private static string[] GetValue(Action<int> アクションで渡されるメソッド)
{
var result = new List<string>();
for (int value = 0; value <= 10; value++)
{
result.Add(value.ToString());
アクションで渡されるメソッド(value);
}
return result.ToArray();
}
public static void Main(string[] args)
{
GetValue(value => Console.WriteLine(value + "回目の処理"));
}
0回目の処理
1回目の処理
2回目の処理
3回目の処理
4回目の処理
5回目の処理
6回目の処理
7回目の処理
8回目の処理
9回目の処理
10回目の処理
解説
GetValueメソッドの引数にAction宣言をしています。受け取ったintをConsoleWriteLineによって書き出す というロジックになります。
forループがある分少し分かりにくくなっているかもしれませんが、引数名を統一しているので、ワンステップずつ追っていけば必ず理解できるはずです。
まとめ
ラムダ式はいきなり書くとチンプンカンプンになりやすいですが1つずつ追っていけば案外シンプルな構文です。x => x.Lneght みたいな書き方されているとなかなかコードを追いにくくなるときもあります。そのときは引数名を統一化してあげるなど工夫をしてみましょう。
ラムダ式を理解できればLINQなどの強力な構文を扱えるようになるのでぜひ習得しましょう。