Visual Studio 2008 C#で開発効率アップ!

第2回コード比較で理解するC#3.0の新機能(2)

はじめに

前回は、C#3.0とC#2.0のコードを比較をすることで、比較的容易に理解できる新機能についてを中心にご説明しました。 今回は、ラムダ式についてご説明して行きたいと思います。

前回に比べると、少々難解に感じるかも知れません。 もし、そのように感じてしまった方は、是非サンプルコードを実際に動作させてみてください。 思ったよりも簡単に理解できると思います。

ラムダ式(Lambda expressions)

C#3.0
okButton.Click += (sender, e) => this.Method();
C#2.0
okButton.Click += delegate(object sender, EventArgs e) {
    this.Method();
};

上記はokButtonオブジェクトのClickイベントハンドラを、C#3.0ではラムダ式で、C#2.0では匿名メソッドで表現したものです。 このように、匿名メソッドで書くことができれば、ラムダ式で書くこともできます。

上記の2つコードを見比べると、ラムダ式の引数(sender, e)には型が明記されていないことに気が付くでしょう。 つまり、ここでも型推論が行われていることになります。

上記はイベントハンドラにラムダ式を用いた例で比較をしましたが、ラムダ式を実際に理解するには、デリゲートから順に理解していく方法がよいと思います。 以下に、例をあげてご説明します。

サンプルコード

次のコードは、加算・減算・乗算・除算用の4つのメソッドです。

それぞれが、あるコレクションに格納されている全オブジェクトのValue1、Value2のプロパティの値を計算し、その結果をResultプロパティに格納した後に、コンソールに出力しています。

private void Plus() {
foreach(var x in this.Collection) {
    x.Result = x.Value1 + x.Value2; // 足し算
    Console.WriteLine(x.Result);
}

private void Subtract() {
foreach(var x in this.Collection) {
    x.Result = x.Value1 - x.Value2; // 引き算
        Console.WriteLine(x.Result);
    }
}

private void Multiply() {
    foreach(var x in this.Collection) {
        x.Result = x.Value1 * x.Value2; // 掛け算
        Console.WriteLine(x.Result);
    }
}

private void Divide() {
    foreach(var x in this.Collection) {
        x.Result = x.Value1 / x.Value2; // 割り算
        Console.WriteLine(x.Result);
    }
}

4つのメソッドは、計算方法が異なることを除いてまったく同じ処理です。 従って、計算式のみ差し替えることができれば共通のロジックが使えます。

これを実現する方法として、デリゲート・匿名メソッド・ラムダ式が便利です。

デリゲートによる処理

以下のコードは、計算処理の部分をほかのメソッドに委譲することでロジックを共通しています。

なお、減算・乗算・除算メソッドも同様に記述できるため、以下のサンプルコードからは加算メソッドの例のみを示しています。

private void Plus() {
    this.Calculate(this.CalcAdd); // 計算を委譲するメソッド
}

// 共通計算メソッド
private void Calculate(Func<int, int, int> func) {
    foreach(var x in this.Collection) {
        x.Result = func(x.Value1, x.Value2); // 計算を委譲
        Console.WriteLine(x.Result);
    }
}

// 委譲されたメソッド
private int CalcAdd(int a, int b) {
    return a + b; // 足し算
}

Plusメソッドでは、共通計算メソッドに対して委譲するメソッドを引数として渡します。

共通計算メソッドのFunc<int, int, int>デリゲート型のfuncが、委譲するメソッドを受け取ります。 これによって、計算処理部分はfuncを呼び出し、その結果を受け取ることで共通化が実現します。

委譲するメソッドは、引数や戻り値がFunc<int, int, int>デリゲートと同じ数や型でなければいけません。

Func<>は、.NET Frameworkで定義されているジェネリックを利用したデリゲートです。 委譲されるメソッドはすべて以下のようになります。

private int Calc(int a, int b) {}

つまりFunc<>には、左からvalue1の型、value2の型、戻り値の型を表すことになるため、intを3つ指定することになります。

なお、戻り値を使わない場合はAction<>デリゲートを、さらに引数さえも使わない場合はActionデリゲートを使います。

匿名メソッドを用いた処理

次に、委譲すべきメソッド名を匿名にしてみます。 なお、共通計算(Calculate)メソッドは、上記と同じなので省略します。

public void Plus() {
    this.Calculate(
        delegate(int a, int b) { // 匿名メソッド
            return a + b;
        }
    );
}

Calculateメソッドの引数に、委譲すべきメソッド名を記述する代わりに、メソッドの処理を記述しています。 従って、先ほどのサンプルコードのように、委譲先となるCalcAddメソッドを別途用意する必要はありません。

ここでは、value1、value2引数の型は明記されていますが、戻り値の型がintであることは、Calculateメソッドのデリゲートに指定された型から推論されています。

ラムダ式

次に、匿名メソッドに可能な限り近い形で、ラムダ式を書いてみます。

public void Plus() {
    this.Calculate(
        (int a, int b) => {
            return a + b;
        }
    );
}

ラムダ式では、引数に対してもデリゲートに基づいて型推論されるため、以下のように型を省略できます。

記述が短くなるため1行にまとめてみます。

this.Calculate(
    (a, b) => { return a + b; }
);

さらに、1行で書く場合は{}やreturnの表記を省略できます。

Calculate((a, b) => a + b);

ラムダ式に置き換えたサンプルコード

結果的に、最初にご紹介したサンプルコードは、ラムダ式を使うことで以下のように簡潔に書くことができます。

private void Plus()     { this.Calculate((a, b) => a + b); }
private void Subtract() { this.Calculate((a, b) => a - b); }
private void Multiply() { this.Calculate((a, b) => a * b); }
private void Divide()   { this.Calculate((a, b) => a / b); }

// 共通計算メソッド
private void Calculate(Func<int, int, int> func) {
    foreach(var x in this.Collection) {
        x.Result = func(x.Value1, x.Value2); // 計算を委譲
        Console.WriteLine(x.Result);
    }
}

その他

以下は、引数を使わず、処理も行わないラムダ式の例です。

private void Method1() {
    this.Method2(() => {});
}

これはルールとして捉えずに、ラムダ式が匿名なメソッドのようなものだと理解すれば、引数のない場合に利用する () や、空のコードブロックを示す {} になることが容易に理解できます。

式ツリー(Expression Tree)

ラムダ式を、式ツリーデータとして扱うことができます。 これによって、ラムダ式を動的に作成したり、ラムダ式で記述した式を解析して、DBMSに投げるためのクエリーを動的に作成するなどが可能になります。

System.Linq.Expressions名前空間に、式ツリーを手作業で作成できるAPIが揃っています。 従って、この項のサンプルコードを実行するには、上記の名前空間をusingしてください。

以下は、先ほどご紹介したラムダ式を使ったサンプルコードです。 このコードでは、ラムダ式をデリゲートに渡しています。

private void Plus() {
    this.Calculate((a, b) => a + b);
}

驚くべきことに、以下のコードは、上記のラムダ式を式ツリーデータとして受け取り、動的にコンパイルした結果をCalculateメソッドに渡しています。

private void Plus() {
    Expression<Func<int, int, int>> ex = (a, b) => a + b;
    this.Calculate(ex.Compile());
}

では、exに本当に式ツリーデータが格納されているのでしょうか?  以下のサンプルコードによって確認してみます。

public void Plus() {
    Expression<Func<int, int, int>> ex = (a, b) => a + b;

    var bin = (BinaryExpression) ex.Body;
    var v1 = (ParameterExpression) bin.Left;
    var v2 = (ParameterExpression) bin.Right;

    Console.WriteLine(ex);
    Console.WriteLine(v1);
    Console.WriteLine(v2);

    this.Calculate(ex.Compile());
}

実行すると、以下のように表示されます。

(a + b)
a
b

以上からExpression Treeを上手に使いこなすことで、本来コードとして記述されていたはずのラムダ式を動的に操作できることがご理解いただけたかと思います。

しかし、注意点しなければならないことがあります。 式ツリーにできるラムダ式は、何らかの演算をして結果を返すという程度に簡単でなければいけません。

何故なら、例えば複数のコードが記述されたブロックをラムダ式で記述することはできても、これを式ツリーとして表現することは論理的に不可能だからです。

次回の予定

今回は、ラムダ式についてだけを集中してご説明しました。

次回は、いよいよC#3.0の新機能としては代表的ともいえるLINQについてをご説明したいと思います。

おすすめ記事

記事・ニュース一覧