2009年10月14日

【ラムダ式】 ThreadクラスとThreadStartデリゲート

前回の続きです。
今回は、ラムダ式が上位スコープの変数にアクセス出来ることを利用した実用的な例を示します。
Threadクラスは、別スレッドで処理するメソッドをThreadStartデリゲート型で受け取ります。
このThreadStartデリゲートにラムダ式を使用することが出来ます。
以下のコードを御覧ください。

class Program
{
    static void Main(string[] args)
    {
        string message = "スレッド内で表示する文字列:";
        // 別スレッドで処理するラムダ式
        ThreadStart action = () =>
        {
            for (int i = 0; i < 5; i++)
            {
                // 1秒スリープ
                Thread.Sleep(1000);
                // Mainメソッドのローカル変数を参照
                Console.WriteLine(message + i);
            }
            // Mainメソッドの引数を参照
            Console.WriteLine("argsの型={0}, argsのレングス={1}",
                                      args.ToString(), args.Length);
        };

        // スレッドクラスにThreadStartデリゲート(今回はラムダ式)を渡す
        Thread thread = new Thread(action);
        // スレッド開始
        thread.Start();
        // スレッド終了まで待つ
        thread.Join();

        // <結果>
        //スレッド内で表示する文字列:0
        //スレッド内で表示する文字列:1
        //スレッド内で表示する文字列:2
        //スレッド内で表示する文字列:3
        //スレッド内で表示する文字列:4
        //argsの型=System.String[], argsのレングス=0
    }
}
System.Threading名前空間のThreadStartデリゲートは、public delegate void ThreadStart ()と定義されており、これは引数なし、戻り値voidのメソッドのみ指定出来ることを意味しています。
Threadクラスには素直に引数を渡すことが出来ないので、パラメータを渡すのは多少工夫が必要です。
しかし、このThreadStartデリゲートにラムダ式を適用する事で、簡単にスレッド処理側にパラメータを渡すことが可能です。
上記のサンプルコードの例では、ラムダ式actionの中でMainメソッドのローカル変数messageとMainメソッドの引数argsを参照しています。
ラムダ式なら、引数として渡していない変数でもスレッド内で参照出来るのです。
サンプルの為に即興で考えたコードですが、今まであまり使ったことが無かったThreadクラスを一度使ってみたら面白いかなと思いました。

ラムダ式の先頭記事へ ラムダ式の次の記事へ
ラベル:ラムダ式
posted by 吾一 at 00:12| 2. ラムダ式 | このブログの読者になる | 更新情報をチェックする

2009年10月11日

【ラムダ式】 上位スコープ変数の参照

ラムダ式は、名前の無い即席メソッドというだけでなくもう一つ重要な性質があります。
なんと、本来スコープ外であるはずの上位スコープの変数にアクセス出来るのです。
まずは、以下のコードを御覧ください。

class Program
{
    static void Main(string[] args)
    {
        // Mainのローカル変数
        int count = 0;
        // intを返すラムダ式の定義
        Func<int> getCount = () =>
        {
            // Mainのローカル変数を操作している
            count++;
            // インクリメントした値を返す
            return count;
        };

        // 引数にラムダ式を渡して実行
        PrintCounter(getCount);
        // 最後にPrintCounterメソッド実行後のcount値を表示
        Console.WriteLine("ローカル変数countの値={0}", count);

        // <結果>
        // ラムダ式の戻り値=1
        // ラムダ式の戻り値=2
        // ラムダ式の戻り値=3
        // ローカル変数countの値=3
    }

    static void PrintCounter(Func<int> getCount)
    {
        // ラムダ式を3回実行して、その戻り値を表示
        Console.WriteLine("ラムダ式の戻り値={0}", getCount());
        Console.WriteLine("ラムダ式の戻り値={0}", getCount());
        Console.WriteLine("ラムダ式の戻り値={0}", getCount());
    }
}
getCountは、引数なし、戻り値int型のラムダ式として定義されています。
このラムダ式から、本来スコープ外でアクセス出来ないはずのローカル変数countを操作しています。(最後にcountの値を表示してみても、確かにローカル変数の値が変更されていることが分かります)
実際順に処理を追ってみると、明らかに今までの常識外の動作であることが分かります。
まず、PrintCounterメソッドにgetCountラムダ式を引数として渡します。
PrintCounterメソッドでは、getCountを3回呼び出しますが、このラムダ式の中でMainメソッドのローカル変数であるcountを操作しています。
countを引数で渡していないにも関わらず、上位スコープのローカル変数の値を変更しているのです。
これは、オブジェクト指向の考えを覆すほどの大きな変化です。
オブジェクト指向では、GOF本等で書かれている通り、流動的要素をカプセル化する為に想定する変更箇所の外部インターフェースを事前に定義します。
つまり、抽象クラスやインターフェース(abstract classとinterface)をあらかじめ定義して、そのインターフェースに合うように具象クラスを作っていきます。
しかし、ラムダ式を使うことによってあらかじめ定義された引数以外の値すらメソッド内で操作出来るようになるのです。
このことは、事前に定義された外部インターフェースに縛られること無く、柔軟な拡張が行える可能性を秘めています。
次回は、この性質を使った実用的な例を解説します。

ラムダ式の先頭記事へ ラムダ式の次の記事へ
ラベル:ラムダ式
posted by 吾一 at 23:55| 2. ラムダ式 | このブログの読者になる | 更新情報をチェックする

2009年10月10日

【ラムダ式】 定義済みデリゲート一覧

今回は定義済みデリゲートのお話です。
定義済みデリゲートとは、クラスライブラリで定義されているラムダ式を格納出来る型のことです。
前回までのサンプルコードでは、Actionデリゲート型のみを使用しましたが、
.NET FrameworkにはAction以外にもラムダ式に使用できる型が定義されています。
以下のコードを御覧ください。
class Program
{
    static void Main(string[] args)
    {
        // string型の引数1つ、戻り値voidのラムダ式
        Action<string> lambda1 = (string message) => /* 括弧の中に型を明示しても良い */
        {
            // 引数で受け取った文字列を表示
            Console.WriteLine(message);
        };

        // string型の引数1つを受け取り、int型の戻り値を返すラムダ式
        Func<string, int> lambda2 = (message) =>
        {
            Console.WriteLine(message);
            // int型を返す
            return 0;
        };

        // string型の引数1つを受け取り、bool型の戻り値を返すラムダ式
        Predicate<string> lambda3 = (message) =>
        {
            Console.WriteLine(message);
            // bool型を返す
            return true;
        };

        // 引数なし、戻り値voidのラムダ式を呼び出し( void lambda1(string)の呼び出しと同じ )
        lambda1("ラムダ式に引数を渡して実行!");

        // 引数1つ、戻り値intのラムダ式を呼び出し( int lambda2(string)の呼び出しと同じ )
        int retInt = lambda2("ラムダ式に引数を渡し、int型の戻り値を受け取る!");
        Console.WriteLine("lambda2の戻り値=" + retInt);

        // 引数1つ、戻り値boolのラムダ式を呼び出し( bool lambda3(string)の呼び出しと同じ )
        bool retBool = lambda3("ラムダ式に引数を渡し、bool型の戻り値を受け取る!");
        Console.WriteLine("lambda3の戻り値=" + retBool);

        // <結果>
        // ラムダ式に引数を渡して実行!
        // ラムダ式に引数を渡し、int型の戻り値を受け取る!
        // lambda2の戻り値=0
        // ラムダ式に引数を渡し、bool型の戻り値を受け取る!
        // lambda3の戻り値=True
    }
}
Action<string>型は、引数がstring、戻り値がvoidのラムダ式を代入可能です。
Func<string, int>型は、引数がstring、戻り値がintのラムダ式を代入可能です。
Predicate<string>型は、引数がstring、戻り値がboolになるラムダ式を代入可能です。
Action<T>、Func<T, TResult>、Predicate<T>は、それぞれTの部分に任意の型が指定出来るジェネリックデリゲートと呼ばれるものです。
デリゲート型の変数には、型が一致するラムダ式を代入することが出来ます。(=引数と戻り値の型が全く同じメソッドのみ代入出来るということ)
これらは、.NET Frameworkにて標準で用意されているものなのでいつでも自由に使うことが出来ます。

以下、定義済みデリゲートの一覧です。
.NETバージョン 備考
MethodInvoker 引数:なし
戻り値:void
1.0 プロジェクトの参照設定でSystem.Windows.Forms.dllを追加する必要がある。
Action 引数:なし
戻り値:void
3.5 MethodInvokerよりはこちらを使うほうが良い
Action<T> 引数:任意の型1個
戻り値:void
2.0 Tには引数の型を指定する
Action<T1, T2>
Action<T1, T2, T3>
Action<T1, T2, T3, T4>
引数:任意の型2〜4個
戻り値:void
3.5 T1〜T4には引数の型を指定する
Func<TResult>
Func<T, TResult>
Func<T1, T2, TResult>
Func<T1, T2, T3, TResult>
Func<T1, T2, T3, T4, TResult>
引数:任意の型0〜4個
戻り値:任意の型
3.5 TResultは戻り値の型、T1〜T4には引数の型を指定する
Predicate<T> 引数:任意の型1個
戻り値:bool
2.0 戻り値は暗黙的にbool
Comparison<T> 引数:任意の型2個
戻り値:int
2.0 Sortなどでの比較用デリゲート
Converter<TInput, TOutput> 引数:任意の型1個
戻り値:TOutput
2.0 特定の型のオブジェクトを別の型のオブジェクトに変換

上記の定義済みデリゲート以外にも、自分で任意のデリゲート型を定義してラムダ式を代入することも出来ます。
ただ、これらの汎用的なデリゲートを使えば定義を省けるので楽出来るというわけですね。

ラムダ式の先頭記事へ ラムダ式の次の記事へ
ラベル:ラムダ式
posted by 吾一 at 23:13| 2. ラムダ式 | このブログの読者になる | 更新情報をチェックする

2009年10月09日

【ラムダ式】 ラムダ式のメリット

今回は、ラムダ式を使うことによって得られるメリットについて考察します。
ラムダ式について最初から学びたい方は、前回の記事を御覧ください。
前回のコードでは、引数なし、戻り値なしのラムダ式を使用しましたが、今回のサンプルコードでは、引数のあるラムダ式を使用しています。
Action<int>型の変数には、引数int型、戻り値なしのラムダ式を代入出来ます。
(ちなみに、Action<int, int>ならint型2つ、Action<int, int, string>ならint型2つ、string型1つを引数に持つメソッドが代入可能)
では、以下のコードを御覧ください。

class Program
{
    static void HeavyJob(Action<int> report)
    {
        for (int i = 1; i <= 100; i++)
        {
            // 時間のかかる処理を実行していると仮定(50msecスリープ)
            System.Threading.Thread.Sleep(50);

            // 進捗状況を引数のラムダ式に通知する
            report(i);
        }
    }

    static void Main(string[] args)
    {
        // 進捗をパーセント表示するラムダ式
        Action<int> percentage = (int progress) =>
        {
            // コンソール画面のクリア
            Console.Clear();
            // 進捗率をパーセント表示
            Console.WriteLine("進捗率={0}%", progress);
        };

        // 進捗をアスタリスクの数で表示するラムダ式
        Action<int> asterisk = (int progress) =>
        {
            // 呼ばれるたびにアスタリスクを1つ増やす
            Console.Write("*");
        };

        // 引数に渡すラムダ式を変えて2回実行
        HeavyJob(percentage);
        HeavyJob(asterisk);

        // <結果>
        // 進捗率=100%
        // ****************************************************************************************************
    }
}
まず、HeavyJobメソッドは時間がかかる処理を行うメソッドを想定しています。
今回のサンプルコードでは、Sleepで50msec処理を停止させることで重い処理を表現しています。
これをfor文で100回ループさせ、その進捗状況をreportメソッド経由で通知するようになっています。
HeavyJobメソッドの引数はAction<int>型になっており、引数としてラムダ式を渡しています。
ラムダ式を渡すことで、メソッドを呼ぶ側でreportの挙動をカスタマイズするというのが今回の最大の目的です。

Mainメソッド側では、同じ型のラムダ式を2つ定義しています。
percentageとasteriskですね。
percentageラムダ式の方は引数で受け取った進捗率をパーセント表示で出力します。(進捗率=??%)
asteriskラムダ式の方では引数で受け取った進捗率をプログレスバー形式で出力します。(*****....)
HeavyJobメソッドに渡すラムダ式を変えることで、HeavyJobメソッドの内容を全く変更することなく、進捗率の表示方法を変えることが出来ました。

これが、最も基本的なラムダ式の使い方になります。
ラムダ式を渡すことによって、外部から処理をカスタマイズできる事を覚えておいてください。

ラムダ式 はじめの一歩へ戻る ラムダ式の次の記事へ
ラベル:ラムダ式
posted by 吾一 at 21:36| 2. ラムダ式 | このブログの読者になる | 更新情報をチェックする

2009年10月08日

【ラムダ式】 はじめの一歩

まずは最も簡単なラムダ式のサンプルコードを見てみましょう。
以下のコードの4行目「() => Console.WriteLine("Hello, World!!");」がラムダ式と呼ばれるものです。
ラムダ式は4行目で定義され、7行目で実行されています。
つまり、action()の行を実行した時に、「Hello, World!!」と出力されるのです。
なぜ、ラムダ式はメソッドのように呼び出して実行できるのでしょうか?

static void Main(string[] args)
{
    // ラムダ式の定義
    Action action = () => Console.WriteLine("Hello, World!!");

    // ラムダ式を実行
    action();

    // <結果>
    // Hello, World!!
}

ラムダ式とは、簡単に言うとメソッド内で定義された即席のメソッドのことです。
いちいちメソッド名を付けて定義するまでも無い、簡易的なメソッドをその場で作って使用します。
上記のサンプルコードでは、4行目でactionという変数に即席メソッドを格納し、7行目でそのメソッドを呼び出しているのです。(C言語で言う関数ポインタに格納された関数を呼び出すのに似ています)
C# 2.0までは匿名メソッド(delegateキーワードで定義)と呼ばれる機能で同じことが出来たのですが、 ラムダ式では同じ機能をより簡潔に書けるようになっています。
匿名メソッドは定義にも手間がかかるし、ソースコードが長くなりがちなので今ひとつ使い辛いものでしたが、 ラムダ式は記述も短くなり、非常に使いやすいものになっています。記述に見慣れてしまえばですが。
C/C++やJavaからC#へ移行してきた人にとってラムダ式の、
() => 」このような記述は、奇妙以外の何者でもないでしょう。(私もそうでした)
このような分かりにくい機能をわざわざ追加したのは、それ相応のメリットがあると考えたからでしょう。
ラムダ式のメリットとは何なのか?次項で、もう少し深く掘り下げます。

ラムダ式の次の記事へ
ラベル:ラムダ式
posted by 吾一 at 19:28| 2. ラムダ式 | このブログの読者になる | 更新情報をチェックする
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。