『究極の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に変わる。


なぜこのような等価性の定義になっているかというと、
デリゲートを呼び出した時に同じ結果になるかを予測するためである。
たとえば、同じ変数をキャプチャしているならば、デリゲートを呼び出した時に同じ結果をもたらすと予測できる。

継承をラムダ式で置き換えられることがある

抽象メソッドはデリゲートで置き換えられることがある


コンストラクタにラムダ式を渡すようにする


継承クラスを複数作る代わりに、コンストラクタに相当する機能を持つメソッド(親クラスをnewしたものを返す)を複数定義する
そのメソッド内でコンストラクタを呼び出すようにする。呼び出す時に渡されるラムダ式から、メソッドの引数をキャプチャする。


文字数が減り、楽になり、間違いが減るメリットがある。
デメリットとして、thisが使えない。


全ての継承をデリゲートに置き換えられるわけではない。

C# 2.0の匿名メソッド

ラムダ式があれば匿名メソッドを使う機会はまずない。

delegate (引数リスト) { 内容 }

(引数リスト) => { 内容 }

に置き換えて読めば、ほぼ間違いない。