旧来のVBではイベントは言語の機能として提供されていましたが、.NET Frameworkにおいてはデリゲートの機能を使ってイベントが実現されます。 イベントを検知・受信する側は、デリゲートを使ってコールバック(イベントハンドラ)となるメソッドを指定し、イベントを発生・通知させる側は指定されたデリゲートを使ってイベントハンドラを呼び出します。 これによってイベントの発生と受信の機構が実現されます。

§1 イベントとデリゲート

デリゲートだけを使ってイベント機構を実装することも出来ますが、C#ではeventキーワード、VBではEventステートメントを使うことでイベント機構をより簡単に実装できるようになっています。 以下は、クラスにイベントを実装した簡単な例です。

using System;
using System.Threading;

class WaitAndNotify {
  // 待機を開始したときに発生させるイベント
  public event EventHandler WaitStart;

  // 待機が完了したときに発生させるイベント
  public event EventHandler WaitDone;

  public void Wait(TimeSpan timeout)
  {
    // 開始したらWaitStartイベントを発生させる
    if (WaitStart != null) WaitStart(this, EventArgs.Empty);

    // 指定された時間だけ待機
    Thread.Sleep(timeout);

    // 完了したらWaitDoneイベントを発生させる
    if (WaitDone != null) WaitDone(this, EventArgs.Empty);
  }
}

class Sample {
  static void Main()
  {
    WaitAndNotify w = new WaitAndNotify();

    // WaitStart, WaitDoneイベントのハンドラを設定
    w.WaitStart += new EventHandler(WaitStartHandler);
    w.WaitDone  += new EventHandler(WaitDoneHandler);

    // Waitメソッドを呼び出して待機を開始する
    w.Wait(TimeSpan.FromSeconds(3.0));
  }

  static void WaitStartHandler(object sender, EventArgs e)
  {
    Console.WriteLine("{0:T}: start", DateTime.Now);
  }

  static void WaitDoneHandler(object sender, EventArgs e)
  {
    Console.WriteLine("{0:T}: done", DateTime.Now);
  }
}
実行結果
13:59:34: start
13:59:37: done

まずは、イベントを発生・通知する側から見ていきます。 この例のWaitAndNotifyクラスは待機を行う機能と、待機の開始と完了の時点でイベントを発生させて通知を行う機能を持っています。 開始と完了のイベントは、WaitStartイベントおよびWaitDoneイベントとして外部に公開しています。 この例では、イベントの型としてEventHandlerデリゲートを使っています。 このデリゲートはイベントを発生させたインスタンスを表すobject型の引数と、発生したイベントの内容を表すEventArgs型の引数をとるメソッドを表します。 イベントを受信する側は、このシグネチャと一致するメソッドをイベントハンドラとして指定することで、WaitStartとWaitDoneのイベントの発生を検知することが出来るようになります。

実際にイベントを発生させているのがWaitAndNotify.Waitメソッドの部分です。 C#ではメソッド呼び出しと同様に、VBではRaiseEventステートメントを使ってイベントに割り当てられているハンドラを呼び出しています。 EventHandlerデリゲートのシグネチャに合わせて、引数に現在のインスタンス(this, Me)と空のイベントを表すEventArgs.Emptyを指定して呼び出しています。 ここで指定した引数は、イベントハンドラとなるメソッドに引き渡されることになります。

続いて、イベントを検知・受信する側について見ていきます。 C#では+=演算子、VBではAddHandlerステートメントを使うことで、WaitStartイベントとWaitDoneイベントにそれぞれのイベントハンドラとなるメソッドのデリゲートを指定しています。 イベントが発生すると、これらのメソッドが呼び出されることになります。 イベントが発生した際に呼び出されるメソッドがWaitStartHandlerとWaitDoneHandlerです。

このようにしてイベントの発生と受信の機構を実装することが出来ます。 イベントとデリゲートの関係については、以下のドキュメントでより詳しく解説されています。

なお、上記の例では.NET Frameworkのガイドラインに従いイベントのデリゲートにEventHandlerを使用していますが、独自に宣言したデリゲートなどEventHandler以外のデリゲートを使ってもイベントを実装することも出来ます。 イベントを発生・通知させる側と検知・受信する側で同じシグネチャのデリゲートが使われていれば良く、引数の数や型はイベントとして通知したい内容に合わせて自由に選択することが出来ます。 以下は、上記の例をActionデリゲートを使って書き換えたものです。

using System;
using System.Threading;

class WaitAndNotify {
  // 待機を開始したときに発生させるイベント
  public event Action WaitStart;

  // 待機が完了したときに発生させるイベント
  public event Action WaitDone;

  public void Wait(TimeSpan timeout)
  {
    // 開始したらWaitStartイベントを発生させる
    if (WaitStart != null) WaitStart();

    // 指定された時間だけ待機
    Thread.Sleep(timeout);

    // 完了したらWaitDoneイベントを発生させる
    if (WaitDone != null) WaitDone();
  }
}

class Sample {
  static void Main()
  {
    WaitAndNotify w = new WaitAndNotify();

    // WaitStart, WaitDoneイベントのハンドラを設定
    w.WaitStart += new Action(WaitStartHandler);
    w.WaitDone  += new Action(WaitDoneHandler);

    // Waitメソッドを呼び出して待機を開始する
    w.Wait(TimeSpan.FromSeconds(3.0));
  }

  static void WaitStartHandler()
  {
    Console.WriteLine("{0:T}: start", DateTime.Now);
  }

  static void WaitDoneHandler()
  {
    Console.WriteLine("{0:T}: done", DateTime.Now);
  }
}


§2 イベント引数

.NET Frameworkのガイドラインに従ったイベントの場合、二つ目の引数にEventArgsクラスから派生した任意のクラスをイベント引数として指定します。 イベントが発生したときの状況や、追加の情報などはイベント引数としてイベントを受信する側に伝える事が出来ます。 先の例を少し変えて、イベント引数を使ってイベントが発生した時刻を取得できるようにしてみます。

using System;
using System.Threading;

// イベントが発生した時刻を保持するフィールドを持つイベント引数
class WaitEventArgs : EventArgs {
  public readonly DateTime DateTime;

  public WaitEventArgs(DateTime dateTime)
  {
    this.DateTime = dateTime;
  }
}

class WaitAndNotify {
  // WaitEventArgsをイベント引数として使用するデリゲートを使ってイベントを定義
  public event EventHandler<WaitEventArgs> WaitStart;
  public event EventHandler<WaitEventArgs> WaitDone;

  public void Wait(TimeSpan timeout)
  {
    if (WaitStart != null) WaitStart(this, new WaitEventArgs(DateTime.Now));

    Thread.Sleep(timeout);

    if (WaitDone != null) WaitDone(this, new WaitEventArgs(DateTime.Now));
  }
}

class Sample {
  static void Main()
  {
    WaitAndNotify w = new WaitAndNotify();

    w.WaitStart += new EventHandler<WaitEventArgs>(WaitStartHandler);
    w.WaitDone  += new EventHandler<WaitEventArgs>(WaitDoneHandler);

    w.Wait(TimeSpan.FromSeconds(3.0));
  }

  static void WaitStartHandler(object sender, WaitEventArgs e)
  {
    Console.WriteLine("{0:T}: start", e.DateTime);
  }

  static void WaitDoneHandler(object sender, WaitEventArgs e)
  {
    Console.WriteLine("{0:T}: done", e.DateTime);
  }
}
実行結果
3:26:55: start
3:26:58: done

この例では、EventArgsクラスを継承したWaitEventArgsクラスを作成し、DateTime型の読み取り専用フィールドを追加しています。 読み取り専用にしているのは、イベントハンドラの側で値を変更できないようにするためです。 また、WaitEventArgsクラスを使ったイベントを定義するために、EventHandler<TEventArgs>デリゲートを使っています。 このデリゲートの型パラメータTEventArgsには、EventArgsから派生した任意の型を指定することが出来ます。 この例ではEventHandler<WaitEventArgs>として使用しています。

このようにして、独自に作成したWaitEventArgsを使ってイベントを発生させることが出来るようになりました。 これに合わせて、イベントハンドラの引数もWaitEventArgsにすることで発生したイベントを受信できるようになります。

EventHandler<TEventArgs>の代わりに、独自にデリゲートを宣言することも出来ます。 イベントハンドラとなるデリゲートの場合でもデリゲートには任意の名前を指定することは出来ますが、ガイドラインではイベントハンドラとなるデリゲートであることが分かるよう、デリゲートの名前はEventHandlerで終わるようにすることが推奨されています。 以下の例では、WaitEventArgsをイベント引数とするデリゲートWaitEventHandlerを使ってイベントを定義しています。

using System;
using System.Threading;

// イベントが発生した時刻を保持するフィールドを持つイベント引数
class WaitEventArgs : EventArgs {
  public readonly DateTime DateTime;

  public WaitEventArgs(DateTime dateTime)
  {
    this.DateTime = dateTime;
  }
}

// WaitEventArgsをイベント引数として使用するデリゲート
delegate void WaitEventHandler(object sender, WaitEventArgs e);

class WaitAndNotify {
  public event WaitEventHandler WaitStart;
  public event WaitEventHandler WaitDone;

  public void Wait(TimeSpan timeout)
  {
    if (WaitStart != null) WaitStart(this, new WaitEventArgs(DateTime.Now));

    Thread.Sleep(timeout);

    if (WaitDone != null) WaitDone(this, new WaitEventArgs(DateTime.Now));
  }
}

class Sample {
  static void Main()
  {
    WaitAndNotify w = new WaitAndNotify();

    w.WaitStart += new WaitEventHandler(WaitStartHandler);
    w.WaitDone  += new WaitEventHandler(WaitDoneHandler);

    w.Wait(TimeSpan.FromSeconds(3.0));
  }

  static void WaitStartHandler(object sender, WaitEventArgs e)
  {
    Console.WriteLine("{0:T}: start", e.DateTime);
  }

  static void WaitDoneHandler(object sender, WaitEventArgs e)
  {
    Console.WriteLine("{0:T}: done", e.DateTime);
  }
}

§3 マルチキャスト

イベントハンドラに指定するメソッドは、一つだけではなく複数指定することが出来ます。 これはデリゲートの持つマルチキャストの機能により実現されるものです。 既に解説した一つのイベントハンドラを指定する場合と同様、C#では+=演算子、VBではAddHandlerステートメントを使って一つのイベントに複数のイベントハンドラを指定する事が出来ます。

using System;
using System.Threading;

class WaitAndNotify {
  public event EventHandler WaitStart;
  public event EventHandler WaitDone;

  public void Wait(TimeSpan timeout)
  {
    if (WaitStart != null) WaitStart(this, EventArgs.Empty);

    Thread.Sleep(timeout);

    if (WaitDone != null) WaitDone(this, EventArgs.Empty);
  }
}

class Sample {
  static void Main()
  {
    WaitAndNotify w = new WaitAndNotify();

    // WaitStart, WaitDoneイベントに複数のハンドラを指定
    w.WaitStart += WaitCommonHandler;
    w.WaitStart += WaitStartHandler1;
    w.WaitStart += WaitStartHandler2;

    w.WaitDone  += WaitDoneHandler1;
    w.WaitDone  += WaitCommonHandler;
    w.WaitDone  += WaitDoneHandler2;

    w.Wait(TimeSpan.FromSeconds(3.0));
  }

  static void WaitCommonHandler(object sender, EventArgs e)
  {
    Console.WriteLine("WaitCommonHandler");
  }

  static void WaitStartHandler1(object sender, EventArgs e)
  {
    Console.WriteLine("WaitStartHandler1");
  }

  static void WaitStartHandler2(object sender, EventArgs e)
  {
    Console.WriteLine("WaitStartHandler2");
  }

  static void WaitDoneHandler1(object sender, EventArgs e)
  {
    Console.WriteLine("WaitStartHandler1");
  }

  static void WaitDoneHandler2(object sender, EventArgs e)
  {
    Console.WriteLine("WaitStartHandler2");
  }
}
実行結果
WaitCommonHandler
WaitStartHandler1
WaitStartHandler2
WaitDoneHandler1
WaitCommonHandler
WaitDoneHandler2

結果から分かるとおり、指定したすべてのイベントハンドラが呼び出されています。 なお、複数のイベントハンドラを指定した場合は、指定した順(イベントに追加した順)でイベントハンドラが呼び出されるようになります。

この様にして追加した複数のイベントハンドラから特定のハンドラを削除したい場合は、C#では-=演算子、VBではRemoveHandlerステートメントを使います。 当然、削除したイベントハンドラはイベントが発生しても呼び出されなくなります。

using System;
using System.Threading;

class WaitAndNotify {
  public event EventHandler WaitStart;
  public event EventHandler WaitDone;

  public void Wait(TimeSpan timeout)
  {
    if (WaitStart != null) WaitStart(this, EventArgs.Empty);

    Thread.Sleep(timeout);

    if (WaitDone != null) WaitDone(this, EventArgs.Empty);
  }
}

class Sample {
  static void Main()
  {
    WaitAndNotify w = new WaitAndNotify();

    // WaitStart, WaitDoneイベントに複数のハンドラを指定
    w.WaitStart += WaitCommonHandler;
    w.WaitStart += WaitStartHandler1;
    w.WaitStart += WaitStartHandler2;

    w.WaitDone  += WaitDoneHandler1;
    w.WaitDone  += WaitCommonHandler;
    w.WaitDone  += WaitDoneHandler2;

    // 指定したイベントハンドラを削除
    w.WaitStart -= WaitStartHandler1;

    w.WaitDone  -= WaitDoneHandler2;

    w.Wait(TimeSpan.FromSeconds(3.0));
  }

  static void WaitCommonHandler(object sender, EventArgs e)
  {
    Console.WriteLine("WaitCommonHandler");
  }

  static void WaitStartHandler1(object sender, EventArgs e)
  {
    Console.WriteLine("WaitStartHandler1");
  }

  static void WaitStartHandler2(object sender, EventArgs e)
  {
    Console.WriteLine("WaitStartHandler2");
  }

  static void WaitDoneHandler1(object sender, EventArgs e)
  {
    Console.WriteLine("WaitDoneHandler1");
  }

  static void WaitDoneHandler2(object sender, EventArgs e)
  {
    Console.WriteLine("WaitDoneHandler2");
  }
}
実行結果
WaitCommonHandler
WaitStartHandler2
WaitDoneHandler1
WaitCommonHandler

なお、一つのイベントに同じメソッドのイベントハンドラが複数追加されている場合、削除の際に同じメソッドのイベントハンドラが一度に削除されます。