『究極のC#プログラミング』 Chapter6 ラムダ式 前編
ラムダ式はC# 3.0の花形
C# 3.0では驚くほどコーディングのストレスが存在しない。
たとえば、複数の定義を整合させるためにソースコードを往復することが少ない。
ストレスフリーであることで、C# 3.0の習熟とともに、ラムダ式を多用するようになる。
その結果、メソッドに細かく分割されたコードとなり、メンテナンス性が向上する。
ラムダ式とは何か
(引数リスト) => { 実行内容 }
デリゲート型変数や引数に入れて使う。
ラムダ式を使うと、関連するコードがひとまとまりになる。
例えば、メソッドの中にラムダ式を組み込むことで、別途メソッドを定義する必要が無くなる。
定義済みデリゲート
MethodInvoker // 引数も戻り値もないメソッドを格納するデリゲート。これだけSystem.Windows.Forms名前空間に定義されているので注意。 // 以下のデリゲートはSystem名前空間に定義されている。 Action // MethodInvokerと同じく、引数も戻り値もないデリゲート。.NET Framework 3.5からなので注意。 Action<T> // 引数1つ戻り値なし。ジェネリックデリゲート。.NET Framework 2.0から。 Action<T1, T2>, Action<T1, T2, T3>, Action<T1, T2, T3, T4> // 引数2-4個、戻り値なし。.NET Framework 3.5からなので注意。 Predicate<T> // 引数1つ、bool型戻り値。 // 定義済みデリゲートの最終兵器。高い汎用性。戻り値の無い場合にだけ対応できないのでActionを使うことになる。 Func<TResult>, Func<T1, TResult>, Func<T1, T2, TResult>, Func<T1, T2, T3, TResult>, Func<T1, T2, T3, T4, TResult>
ラムダ式は上位スコープにアクセスできる
メソッド内で何か変数とラムダ式を同一スコープ内に定義したとき、ラムダ式から変数を参照できる。
この点は、ラムダ式の本質的な特徴である。
ラムダ式ではなく、普通にメソッド呼び出しを書いた時には、そのような参照はできない。
どうしても参照したいなら、メソッドの引数に変数を渡す必要が生じる。
ラムダ式のこの性質により、メソッドの引数の肥大化が防げるメリットがある。
ラムダ式を使うと、外部変数がキャプチャされる
上位スコープへのアクセスと関連して、ラムダ式には、外部変数のキャプチャという機能もある。
これは、上位スコープの実行が終了しても、ラムダ式がデリゲートによって参照されている限りは、変数が延命される。
この1つの結果として、値型の変数であっても、参照型の変数であるかのような寿命を持つことになる。
ラムダ式ではなく通常のメソッドの場合には、このようなことは起きない。
キャプチャは予想外のふるまいをしうる
クロージャとも関連した話。
delegate int Del1(); Del1[] methods = new Del1[2]; // シンプルなforループ for(int i = 0; i < 2; i++) { methods[i] = () => { return i; }; } Console.WriteLine("{0} {1}", methods[0](), methods[1]()); // 2 2 // ForEachメソッドを使ったループ int[] array = { 0, 1 }; Array.ForEach(array, (i) => { methods[i] = () => { return i; }; }); Console.WriteLine("{0} {1}", methods[0](), methods[1]()); // 0 1 // forループの変形版 for(int i = 0; i < 2; i++) { int j = i; // ループのたびに新しく変数jが作られる methods[i] = () => { return j; }; } Console.WriteLine("{0} {1}", methods[0](), methods[1]()); // 0 1
シンプルなforループでは、外部変数であるiがキャプチャされるので、ラムダ式におけるreturn i;はこの外部変数の値である2を返す。
ForEachメソッドを使う例では、引数としてiを渡すので、ループのたびに新しく変数iが作られる。
だから、全く別の変数iが2つ作られることになる。
その結果、0 1と出力される。
forループの変形版では、forループのスコープに入るたびに変数jが新しく作られる。
だから、全く別の変数jが2つ作られることになる。
その結果、0 1と出力される。
デリゲートの共変性・反変性
共変性・・・戻り値の型が弱い型であっても受け入れる
反変性・・・引数の型が強い型であっても受け入れる
例えば、クラスA1を継承したクラスA2があったとして、
delegate A1 SampleDelegate();
のようにA1を戻り型とするデリゲートが定義されているとする。
この時、SampleDelegateに
static A2 SampleMethod { return new A2(); }
のようなA2を戻り型とするメソッドを代入することができる。
これをデリゲートの共変性という。
デリゲートインスタンスの等価性
Equalsや==演算子によってデリゲートインスタンスの等価性を調べることができる。
ただし、注意事項がいくつかある。
別個のコードで生成されたデリゲートインスタンスは、内容が同じラムダ式であっても、等価とはみなされない。
Action<int> m1 = (n) => { }; Action<int> m2 = (n) => { }; Console.WriteLine(m1 == m2); // False
同じコードで生成した場合にはどうか?次の場合はTrueになる。
Action<int>[] methods = new Action<int>[2]; for (int i = 0; i < 2; i++) { methods[i] = (n) => { }; } Console.WriteLine(methods[0] == methods[1]); // True
ただし、ラムダ式がキャプチャしている変数が同じでない場合には、Falseになることに注意である。
Action<int>[] methods = new Action<int>[2]; for (int i = 0; i < 2; i++) { int j = i; // ループを回るたびに別の変数jが作られる methods[i] = (n) => { j++; }; // ラムダ式でjが使われているので、変数jはキャプチャされる } Console.WriteLine(methods[0] == methods[1]); // False // キャプチャしている変数が同じかが関係している証拠に、j++をi++に書き換えるとTrueに変わる。
なぜこのような等価性の定義になっているかというと、
デリゲートを呼び出した時に同じ結果になるかを予測するためである。
たとえば、同じ変数をキャプチャしているならば、デリゲートを呼び出した時に同じ結果をもたらすと予測できる。