C# の delegate と event

オブジェクト指向言語では delegate という用語が使われます。 C# でも delegate と名付けられた機能があるのですが、よく使われる意味での delegate を知っていると "なんでこれが delegate" と感じるのではないでしょうか。
今回はそんな C# の delegate について解説したいと思います。 また、 ついでに delegate に似た event の機能についても説明しています。

一般的な Delegate

まずは、他のオブジェクト指向言語でよく使われている意味での delegate から見てみます。
delegate は一般的には 委譲と訳され、 次のような意味で使われることが多いです。
クラスのメソッドを実行する際に、メンバー変数のメソッドを代わりに実行する

例えば、次のような Bell クラスがあったとします。
class Bell
{
public:
    Bell() {}
    void ring()
    {
        cout << "Ring! Ring!" << endl;
    }
};      
目覚まし時計(AlarmClock)クラスでは、 ring() メソッドでメンバーの Bell のメソッドを呼び出します。
class AlarmClock
{
    Bell m_bell;
public:
    AlarmClock() {}
    void ring()
    {
        m_bell.ring();
    }
};

void main()
{
    AlarmClock aclock;
    aclock.ring();
}
~/lang/cpp $ ./delegate.exe 
Ring! Ring!
このようにメソッドの処理を実際には他のクラスのメソッドにやってもらうため、 委譲(delegate) と呼ばれています。

イベントとコールバック

ここで GUI におけるイベント駆動についても見てみます。

"ボタンを押す" などのイベントが起こった時、 イベントに割り当てられたコールバックを実行します。
cs_delgate_event.png


C や C++ における多くのツールキットでは、 コールバックとして呼ばれるのは通常の(静的な)関数であり、 クラスのメンバー関数ではありません。
通常の関数が使われるのは、それが使いやすいからです。 C, C++ では通常の関数は関数ポインターとして変数に格納でき、 変数に () をつければ格納した関数を呼び出すことができます。
void hello(const string &name)
{
    cout << "Hello " << name << endl;
}

void main()
{
    void (*cb)(const string &name) = hello;
    cb("world");
}
関数ポインターも型として typedef で別名をつけておくことができます。 これを使えば、より変数っぽく扱えます。
typedef void CallBack(const string &name);
void main()
{
    CallBack cb = hello;
    cb("world");
}

Delegate パターン

コールバックとして呼び出す関数ですが、 C++ のようなオブジェクト指向言語では、 静的な関数よりもオブジェクトのメソッドを呼び出したいことの方が多いでしょう。 しかし、 C++ では関数ポインターの変数にメソッドを格納することはできません。
    Bell bell;
    void (*cb)() = bell.ring;   // コンパイルエラー
それでも、コールバックとしてはメソッドを登録したいです。 もっと言えば、複数登録して複数のメソッドを実行したいし、 登録したメソッドを自由に着脱したいです。
これは、頑張れば C++ で実現するやり方もなくはありません。 『 GoF 本』には入っていませんが、 Delegate パターン と呼ばれているやり方があります。 Delegate パターンの中身は説明しませんが、 処理を実行する際に登録しておいた他のメソッドが実行されます。 "他のメソッドが実行する" という点において、これも Delegate と呼ばれています。

C# の delegate

C# の delegate は一般的な Delegate よりも先ほどの Delegate パターンに近いです。 その C++ の Delegate パターンですが、実はかなり実装が面倒くさいです。 これを言語の機能として簡単に使えるようにしたものが C# の delegate です。


C# の delegate は C, C++ の関数ポインター変数のようなものですが、 次のような点が拡張されています。
  • 通常の関数だけでなく、クラスのメソッドも格納(アタッチ)できる
  • 複数の関数を格納可能
    • 呼び出し時には格納した順に逐次実行される
  • 関数の着脱(アタッチ、デタッチ)も自由
namespace DelegateSample
{
    // Delegate の型を宣言
    delegate void HelloDelegate(string name);
    
    class Foo
    {
        // メソッド
        public void Hello(string name)
        {
            Console.WriteLine("(method) : Hello {0}!", name);
        }
    }
    
    class Program
    {
        // static な関数
        static void StaticHello(string name)
        {
            Console.WriteLine("(static) : Hello {0}!", name);
        }

        static void Main()
        {
            Foo foo = new Foo();

            // delegate の変数
            HelloDelegate funcs;
            
            funcs = foo.Hello;          // メソッドの格納
            funcs += StaticHello;       // static 関数の格納
            // 無名関数の格納
            funcs += (string name) => {
                Console.WriteLine("(lambda) : Hello {0}!", name);
            };

            // 格納した関数の呼び出し
            funcs("world");
        }
    }    
}
~/cs/delegate $ ./DelegateSample.exe 
(method) : Hello world!
(static) : Hello world!
(lambda) : Hello world!
このように C# の delegate は通常の関数だけでなく、メソッド、無名関数なども格納できます。

無名関数の delegate

先ほどの例では無名関数に => を使う方法(ラムダ式)を取りましたが、 delegate を使った無名関数の定義(匿名メソッド)の記述もあります。
            // 無名関数の格納 (delegete キーワード版)
            funcs += delegate(string name) {
                Console.WriteLine("(delegate) : Hello {0}!", name);
            };
これは他の言語では function , lambda などのキーワードが使われるのですが、 C# では delegate と記述します。
ここでの delegate キーワードは先ほどの関数格納のための delegete とは別物と考えてください。

delegate の注意点

実はサンプルでの delegate の使用方法は少し手を抜いています。

delegate の変数も初期化されていない場合は null です。 null の入った変数に対して呼び出しを行うと例外が発生します。
これを避けるために次の 2 つの方法のどちらかを使うことが多いです。

使用前に null のチェックを行う :
if (funcs != null)
{
    funcs("world");
}
何もしない関数で初期化する :
HelloDelegate funcs = delegate(string name){};

funcs += foo.Hello;
初期化した後に実際に呼び出したい関数を格納します。 呼び出し時には何もしない関数 delegate(string name){} も実行されますが、 何もしないので問題にはなりません。

サンプルコード

説明で使用したサンプルのコードは以下のリンクからダウンロード(リンク先を保存)できます。 コンパイルする場合は以下のコマンドを実行します。
 > csc DelegateSample.cs
csc.exe を使用したコンパイル方法については以前の記事を見て下さい。

C# のイベント

C# の event は一般的な "イベント-コールバック" を C# の delegate の機能を使って実現しているようなものです。
event は当然 GUI で使われることが多いのですが、 ここではわかりやすいようにシンプルにしたサンプルで説明したいと思います。


C# の event は delegate のようにメソッドを登録することができます。
ただし、以下の点が違います。
  • ローカルな変数には使えず、必ずクラスのメンバー
  • 同じクラスのメソッドからのみ呼び出し可能
EventSample.cs :
namespace EventSample
{
    delegate void HelloDelegate(string name);
    
    class Button
    {
        // イベント
        // (クラス定義内でしか event は使えない)
        public event HelloDelegate SomeEvent = delegate(string name){};

        // イベントの発生
        public void Clicked(string name)
        {
            Console.WriteLine("<Button Clicked>");
            // 登録した関数を呼び出す
            // (直接には Button クラス内でしか実行できない)
            SomeEvent(name);
        }
    }

    class Foo
    {
        public void Hello(string name)
        {
            Console.WriteLine("(method) : Hello {0}!", name);
        }
    }

    class Program
    {
        static void StaticHello(string name)
        {
            Console.WriteLine("(static) : Hello {0}!", name);
        }

        static void Main(string[] args)
        {
            Foo foo = new Foo();

            Button btn = new Button();

            // 関数の登録(アタッチ)
            btn.SomeEvent += foo.Hello;
            btn.SomeEvent += StaticHello;
            btn.SomeEvent += (string name) => {
                Console.WriteLine("(lambda) : Hello {0}!", name);
            };

            // イベントの発生
            btn.Clicked("world");
        }
    }
}
~/cs/delegate $ ./EventSample.exe 
<Button Clicked>
(method) : Hello world!
(static) : Hello world!
(lambda) : Hello world!
event は delegate の変数のように関数を登録できますが、 直接呼び出すことはできません。 この制限により、"イベントに関連付けたコールバック(関数)はイベントが発生した時に呼び出される" という形式になります。
実際、サンプルは次のように解釈できます。
  1. Button のオブジェクト btn に Hello 関数群をコールバックとして関連付け
  2. btn に Clicked イベントが発生 (btn に Clicked メッセージを送る)
  3. 関連付けられたコールバック群が実行される

delegate と event の使い処

たまに "delegate を使うべきか、 event を使うべきか" という話を聞きます。

私はそれぞれ次のようなものだと考えています。
  • delegate: 関数ポインターの拡張
  • event: イベント-コールバック というイベント駆動のための機能
個人的には、 単に関数(メソッド)を格納して呼び出したい場合など基本は delegate を使い、 GUI にかぎらずイベント駆動的な処理がしたいときに event を使えばいいのではないかと思います。


関連記事
スポンサーサイト
Prev.    Category    Next 

Facebook コメント


コメント

コメントの投稿

Font & Icon
非公開コメント

このページをシェア
アクセスカウンター
アクセスランキング
[ジャンルランキング]
コンピュータ
34位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
3位
アクセスランキングを見る>>
カレンダー(アーカイブ)
プルダウン 降順 昇順 年別

02月 | 2017年03月 | 03月
- - - 1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31 -


はてな新着記事
はてな人気記事
ブロとも申請フォーム
プロフィール

yohshiy

Author:yohshiy
職業プログラマー。
仕事は主に C++ ですが、軽い言語マニアなので、色々使っています。

はてブ:yohshiy のブックマーク
Twitter:@yohshiy

サイト紹介
プログラミング好きのブログです。プログラミング関連の話題や公開ソフトの開発記などを雑多に書いてます。ただ、たまに英語やネット系の話になることも。