C#のデリゲート (delegate) って何?

デリゲートを簡単に説明すると、関数変数のように扱う為のものです。

C言語に慣れている人には関数ポインタのようなものと言った方が分かるかもしれません。

 

変数のように扱えれば、関数の引数に関数を渡す事ができます。

引数に関数が渡せれば、計算式の一部だけを簡単に差し替える事ができます。


デリゲートはどのように使われる?

デリゲートはイベントハンドラコールバックで利用されます。

 

イベントハンドラは「クリックされた」とか「値が変わった」のようなイベントが起こった時に、それに対応する処理を行う仕組みです。Visual Studioなどの統合環境を使っているとコードが自動生成されるのであまり意識しないかもしれませんがデリゲートの仕組みが使われています。

 

コールバックは、ある関数が実行している途中で別の関数を実行させる仕組みです。

コールバックを理解する事でより汎用的なクラスやメソッドが作れるようになります。


デリゲート(delegate)を定義する

変数に int や double といった型がありますね。

関数を変数として扱う為には、関数の型を決める必要があります。

 

関数の型を定義するのが delegate キーワードです。

delegateの宣言は↓このようになります。



public delegate bool TestDelegate(object obj);


クラスのメソッドを作るのと似ていますが、先頭に delegate キーワードが付いていてメソッドの中身がありません。こんな形の関数を作りますよと宣言しているだけです。

 

上の例では「TestDelegateという名前の型は、 object 型の引数を1つ必要として bool の戻り値を返す関数です」と宣言しているわけです。


イベントハンドラ的な使い方

delegateを使ってイベントハンドラのような動きをするサンプルを作ってみました。

以下の例では、ループする処理の中で進捗の%が進む度にイベントを発生させています。

    public delegate void ProgressHandler(int percentage);

    public class TestProcess
    {
        public event ProgressHandler Progress;


        public void Run()
        {
            int percentage = -1;
            int count = 100000;

            for (int i = 0; i < count; ++i)
            {
                int work = (int)((double)i / (double)count * 100.0);
                if (percentage != work)
                {
                    percentage = work;
                    Progress(percentage);
                }
                //
                // なにかしらの処理
                //
            }
            Progress(100);
        }
    }

1行目で delegate が宣言されています。

5行目で、そのdelegateを使って TestProcessクラスの Progressプロパティを作ります。

15~20行目で処理の進捗率を計算し、パーセンテージが進んだら Progress プロパティにセットされたメソッドを呼び出しています。

 

※5行目のProgressプロパティには event キーワードが使われています。

※event キーワードは書かなくても同じように動作しますが、これを付ける事で delegate の一部の機能が制限されます。

※使い方をイベントハンドラとしてのものに制限する事でバグの発生を抑制しようという狙いのようです。

    public partial class TestWindow : Window
    {
        public TestWindow()
        {
            InitializeComponent();

            var test = new TestProcess();
            test.Progress += TestProgressHandler1;
            test.Progress += TestProgressHandler2;
            test.Run();
        }

        public void TestProgressHandler1(int percentage)
        {
            Console.Write(percentage);
        }

        public void TestProgressHandler2(int percentage)
        {
            Console.WriteLine("%..");
        }
    }

デリゲートにメソッドをセットするには「+=」演算子を使います。

セットしたメソッドを外すのは「-=」演算子です。

※eventキーワードが使われた場合「=」演算子でセットする事は出来なくなります。

 

デリゲートには複数のメソッドをセットする事が出来ます。


コールバック的な使い方

delegateを使ってコールバックをするサンプルを作ってみました。

以下の例は、List<int>を継承したクラスを作り、条件に合う項目だけを抜き出すWhereメソッドを作っています。この「条件に合う」という部分にdelegateを使って外に出す事で、様々な条件に対応できる汎用的なメソッドになります。

    public delegate bool TestDelegate(int obj);

    public class TestList : List<int>
    {
        public TestList Where(TestDelegate func)
        {
            var list = new TestList();
            foreach (var obj in this)
            {
                if (func(obj))
                {
                    list.Add(obj);
                }
            }
            return list;
        }
    }

1行目で delegate が宣言されています。

6行目で、その delegate を引数にしたWhereメソッドを定義しています。

引数で渡された func メソッドが true を返すものだけを抽出しています。

    public partial class TestWindow : Window
    {
        public TestWindow()
        {
            InitializeComponent();

            var list = new TestList();
            list.Add(0);
            list.Add(1);
            list.Add(2);

            var result = list.Where(TestFunc);
        }

        public bool TestFunc(int n)
        {
            if (0 < n)
                return true;
            else
                return false;
        }
    }

C#にはコールバック用のメソッドをもう少し簡素化できる匿名メソッドという仕組みがあります。 

 

※匿名メソッドを更に詳しく知るにはC#のデリゲートお手軽にする匿名メソッドをご覧ください


定義済みdelegate(FuncとAction)

ここまでの例では自分で delegate を定義して使っていました。

引数の数が変わる度にいちいち定義するのはめんどくさい気がします。

 

そんな訳で、Microsoftは予め Func と Action という delegate を定義してくれています。

戻り値を持つ delegate が Func

戻り値を持たない delagete が Actionです。

 

定義を見てみるとジェネリックという機構をつかって様々な型に対応できるようになっています。

引数の数ごとにたくさん定義されているので、自分でdelegateを定義する必要はもはや無さそうです。

Func

    public delegate TResult Func<out TResult>();
    public delegate TResult Func<in T1, out TResult>(T1 arg1);
    public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
    public delegate TResult Func<in T1, in T2, in T3, out TResult>(T1 arg1, T2 arg2, T3 arg3);
          :

Action

    public delegate void Action();
    public delegate void Action<in T1>(T1 arg1);
    public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
    public delegate void Action<in T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3);
          :

 

関連記事