最初に
一体何番煎じの記事なんだ…
でも、C#の初心者が順番に文法を学んでいって、最初に詰まるところはラムダ式じゃないかな、と思います。理由ははっきりしていて、ラムダ式だけ考え方、つまり頭の使い方が違うんですよね。この記事が理解の足しになればな、と思います。
またいつものループ処理か…
List<T>
の各要素に対して XX 処理をする、というのは、C#をやっていると頻繁に出くわすのではないでしょうか? 例えば List<string>
の各要素の文字列に対して、".txt"
を付ける、みたいな処理です。まぁ、foreach
でいいですよね。
var list = new List<string>(); // このListの各要素に".txt"をつける
var result = new List<string>();
foreach (var s in list)
{
result.Add(s + ".txt");
}
何が悪いかって、面倒なんですよね、毎回毎回foreach
書くのが。初心者の人はまだ100回くらいかもしれませんが、私はもう10000回くらい書いてるよ。いやそんなことよりもっと大きな問題がある。List<string>
の各要素に対しての処理なら、List<string>
の処理として、つまりメソッドを用意して欲しい、ということだ。
つまり、こう書きたいのです。
var list = new List<string>(); // このListの各要素に".txt"をつける
var result = list.Select(文字列に".txt"を付けたす処理);
ここで問題になるのは、文字列に".txt"を付けたす処理の部分で、Selectメソッドの引数に何の値を渡せばいいか、ということです。渡せるモノは値なんですよ、メソッドの引数ですから。でも今渡したいのは、文字列に".txt"を付けたす処理で、プログラムで ~処理 、と見たら、ほぼそれは機械的にメソッド/関数と脳内置換していいです。つまり今Select
メソッドに渡したいモノは値ではなくメソッドなのです。
じゃあ、メソッドをメソッドに渡しちゃおう
そこでMicrosoftは考えたのですよ、別にメソッドを渡せてもいいんじゃないかな、と。メソッドに値を渡す場合、変数定義(宣言)して、それを渡しますよね、こんな風に。
var list = new List<string>();
var s = "hoge"; // ← Addメソッドに渡す変数を定義(宣言)
list.Add(s);
だからメソッドを渡す場合も、メソッドを定義しておく必要があります。
public static void Main(string[] args)
{
var list = new List<string>(); // このListの各要素に".txt"をつける
var result = list.Select(Append);
}
// ↓ Selectメソッドに渡すメソッドを定義
private static string Append(string s)
{
return s + ".txt";
}
これはこれでいいのですが、こんな中身がちっちゃいメソッドをわざわざ作りたくないんですよね、メソッド名を考えるのが嫌になってくるから。でもメソッドに値を渡す場合は、変数なんて定義(宣言)しなくても、値を直接(リテラル)書けますよね。
var list = new List<string>();
list.Add("hoge"); // ← 直接リテラルを書く
メソッドもリテラルが書けてもいいともいませんか? こういう風に。
public static void Main(string[] args)
{
var list = new List<string>(); // このListの各要素に".txt"をつける
var result = list.Select((string s) => { return s + ".txt"; }); // ← 直接リテラルを書く
}
list.Select(Append)
のAppend
メソッドの中身を強引に展開しました。それによって、Appendというメソッド名も必要なくなりました。はい、これがラムダ式です。簡単ですね。
なんか、いつもよく見かけるラムダ式と少しちがうような
(string s) => { return s + ".txt"; }
の部分がラムダ式なのですが、なにか微妙に記述が違います。いつも見かけるのは、このラムダ式の省略記法で、純粋にC#の文法・記述のだけの話です。
C#のラムダ式の記法は、次の場合省略できます。
- 引数の型が自明(型推論が出来る)の場合、省略できる。
(string s) => { return s + ".txt"; }
↓
(s) => { return s + ".txt"; }
- メソッド本体が1文しかない(正確には式しかない)場合は、
{ }
とreturn
を省略できる。
(s) => { return s + ".txt"; }
↓
(s) => s + ".txt"
- 引数が1つの場合は
( )
を省略できる(引数0はダメ)。
(s) => s + ".txt"
↓
s => s + ".txt"
これでよく見かけるラムダ式になりました。最初、慣れない間は()
や{}
を補ってあげると良いです。慣れれば自然と分かるようになります。
var result = list.Select(s => s + ".txt");
↓
var result = list.Select((string s) => { return s + ".txt"});
便利なので、自分でもメソッドを受け取るメソッドを作りたい
今までは使う側だったのですが、こんどは作る側(提供する側)です。ぱっといい例が思いつかなかったのでが、ファイルを読んで、各行をなんらかの変換してList
を返すメソッドを作るとしましょう。なんらかの変換は、呼び出し側にメソッドを指定してもらって、trimするとか".txt"付け足すとかできるようにしましょう。ラムダ式の出番です。メソッドを書いてみましょう。こんな感じでしょうか。
public IEnumerable<string> Read(string path, 文字列を引数にとって文字列を返すメソッド fx)
{
var result = new List<string>();
using (var reader = new StreamReader(path))
{
while (reader.Peek() >= 0)
{
var line = reader.ReadLine();
result.Add(fx(line));
}
}
return result;
}
1つ困ったことが発生しました。C#の文法だと、メソッドの定義は型+(仮)変数名
なのですが、メソッドを受け取る引数は、いったいどうやって書けばいいのでしょうか? 文字列を引数にとって文字列を返すメソッドの部分です。
当然、メソッドを表す型です。C#にメソッドを受け取る書き方ができる文法が用意されていればよかったのですが、そんなのはありません、というかMicrosoftはそんなの不要、と判断したのでしょう。だからメソッドを表す型があるのです。
それが、Func
とAction
です。この2つは本質的に同じもので、使い分けは、
戻り値がある(voidじゃない) → Func
戻り値がない(void) → Action
だけです。これはC#のジェネリックス型引数にvoid
が指定できないから、2種類あるだけです。voidが指定できたら2種類いらなかった。
今回の例は、戻り値があるのでFunc
を使います。引数:文字列、戻り値:文字列、なので、型はFunc<string, string>
になります。
public IEnumerable<string> Read(string path, Func<string, string> fx)
{
...
これで、無事呼び出し側から、ラムダ式を受け取れます。
Read("hoge.txt", s => s + ".txt");
まとめ
ラムダ式の説明としては、まだ遅延評価と副作用の話がありますが、とりあえずこれだけ理解しておけば当面困らないと思います。ラムダ式の理解がつまづくのは、今まではメソッドに値を渡すという、値ベースの考え方なのですが、ラムダ式で急にメソッド(処理)を渡すという、関数型指向の考え方が出てくるからです。
最後に1つだけ言っておきたいことがある
delegate
なんてなかった、いいね?