イベント(event)とデリゲート(delegate)を理解しよう!

[C#] イベント(event)とデリゲート(delegate)を理解しよう!

※ 当サイトは広告を含みます。

ここではC#のイベントを学習します。その中で登場するデリゲートはイベントで必要になる仕組みです。

このイベントですが、自分で作るよりも.NETに定義済みのイベントを利用して覚えるほうが簡単です。
そのため、今回は利用者としてイベントを理解しましょう。別枠で自作方法もやります。

イベント駆動型(event driven)

多くのプログラミング言語で、何か起きたら何かするような仕組みをイベント駆動型と言います。
もしくは普通に英語読みでイベントドリブン(event driven)と言います。

1番分かりやすいのがGUI系で、画面上のボタンを押すとかキーボード入力をすると処理が走るパターンです。
メモ帳を例にすると、キーボード入力で画面に文字が表示され、メニューから保存を選べばファイルが保存されます。

それ以外にもOS系の機能を利用する場合にイベントを使うことがあります。
例えば特定のファイルを監視して、そのファイルに変化があった場合に処理を走らせるとかです。
また、発生した事象をイベント、発生条件をトリガー、結果的に呼び出される処理をイベントハンドラと言います。

管理人
管理人

これらの名前は命名規則で使われることが多いので覚えたほうがいいです。

イベント(event)

イベントを理解するために、.NETに定義されたファイル監視のイベントを使ってみましょう。

次のサンプルコードは指定フォルダ、今回はC:\Tempのフォルダを監視するプログラムです。
対象のフォルダに、フォルダやファイルが作成または削除されると、そのパスが表示されます。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      // 指定したディレクトリを監視するクラス
      using var watcher = new System.IO.FileSystemWatcher(@"C:\Temp");

      // ディレクトリまたはファイルが作成された場合のイベント
      watcher.Created += Watcher_OnCreated;

      // ディレクトリまたはファイルが削除された場合のイベント
      watcher.Deleted += Watcher_OnDeleted;

      // イベントの有効化
      watcher.EnableRaisingEvents = true; // ※ FileSystemWatcherを使う場合は必要

      // Enterが入力されるまでプログラムを続行
      Console.WriteLine("Enterを入力すると終了します。");
      Console.ReadLine();
    }

    // ディレクトリまたはファイルが作成された場合に呼び出されるメソッド
    private static void Watcher_OnCreated(object sender, FileSystemEventArgs e)
    {
      Console.WriteLine($"ファイルが作成されました。FullPath={e?.FullPath}");
    }

    // ディレクトリまたはファイルが削除された場合に呼び出されるメソッド
    private static void Watcher_OnDeleted(object sender, FileSystemEventArgs e)
    {
      Console.WriteLine($"ファイルが削除されました。FullPath={e?.FullPath}");
    }
  }
}

補足using varに関しては本題ではないので説明を避けます。これはイベントとは関係ありません。

このように何かのイベントに対して、処理を実行するパターンがイベント駆動型です。
ここからは先程のコードと.NET定義済みの部分を理解しながら、イベントの理解を深めます。

トリガー(trigger)

最初に大切なのがトリガー(trigger)です。これはそのままの意味で発生条件です。
サンプルコードでは、ファイルまたはフォルダの作成と削除がトリガーになります。

今回の場合、.NETに定義された機能を利用するため、自分でトリガーに関する処理を作る必要はありません。
トリガーが条件を満たすと、後述するイベントハンドラが呼び出されることを理解すれば大丈夫です。

当たり前ですが、自分でイベントをゼロから作る場合はトリガーに関する処理も書きます。
と言っても、特に難しくはなく、決められたコードを利用してイベントハンドラを呼び出すだけです。

イベントハンドラ(event handler)

トリガーの条件を満たすと呼び出されるのがイベントハンドラです。
サンプルコードの場合、次の2個がイベントハンドラに該当します。

  1. Watcher_OnCreated()
  2. Watcher_OnDeleted()
りさ
りさ

これメソッドと何が違うんですか?

管理人
管理人

別に違いません。単なるメソッドです。

りさ
りさ

どういうこと?

イベントハンドラとは、イベント発生時に呼び出される処理を意味する固有名称(たぶん)です。

C#は、ほぼ全ての処理をメソッドで記述しますが、それをイベントハンドラに登録することでイベントとして機能させます。
つまり、イベントハンドラとは単なるメソッドです。これを証明するのが次のサンプルコードです。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      // 指定したディレクトリを監視するクラス
      using var watcher = new System.IO.FileSystemWatcher(@"C:\Temp");

      // ディレクトリまたはファイルが作成された場合のイベント
      watcher.Created += Watcher_OnCreated;

      // ディレクトリまたはファイルが削除された場合のイベント
      watcher.Deleted += Watcher_OnDeleted;

      // イベントの有効化
      watcher.EnableRaisingEvents = true; // ※ FileSystemWatcherを使う場合は必要

      // イベントハンドラに設定したメソッドを直接呼び出す
      Watcher_OnCreated(null, null);
      Watcher_OnDeleted(null, null);

      // Enterが入力されるまでプログラムを続行
      Console.WriteLine("Enterを入力すると終了します。");
      Console.ReadLine();
    }

    // ディレクトリまたはファイルが作成された場合に呼び出されるメソッド
    private static void Watcher_OnCreated(object sender, FileSystemEventArgs e)
    {
      Console.WriteLine($"ファイルが作成されました。FullPath={e?.FullPath}");
    }

    // ディレクトリまたはファイルが削除された場合に呼び出されるメソッド
    private static void Watcher_OnDeleted(object sender, FileSystemEventArgs e)
    {
      Console.WriteLine($"ファイルが削除されました。FullPath={e?.FullPath}");
    }
  }
}

このように普通にメソッドの呼び出しを記述してもエラーにはなりません。
ただし、現実はまともな引数を与える必要があるので、必ずしも動くとは限りません。

管理人
管理人

ちなみに登録される側のメソッドはOn〇〇と名付けることが多いです。

さて、このイベントハンドラですが、どんなメソッドでも登録できるのでしょうか?
答えはNoです。予め決められた形式のメソッドしか登録できません。

それを理解するために.NET側のイベントハンドラの定義を確認しましょう。
まず、前提としてFileSystemEventHandlerデリゲートが定義されてます。


public delegate void FileSystemEventHandler(object sender, FileSystemEventArgs e);

そして、このデリゲートはCreatedイベントまたはDeletedイベントと結びつきます。


public event System.IO.FileSystemEventHandler? Created;

public event System.IO.FileSystemEventHandler? Deleted;
管理人
管理人

ここで登場するのがデリゲートです。日本語訳で委任するだそうです。

りさ
りさ

委任?

管理人
管理人

たぶん、代理するって意味(適当)。大切なのはイベントとデリゲートの関係です。

デリゲート(delegate)

そんなに深く考えなくていいです。結論としてデリゲートは型です。int型とかclass型と同じです。
デリゲートはdelegateキーワードを利用して定義します。これは先程の.NET定義済みデリゲートです。


public delegate void FileSystemEventHandler(object sender, FileSystemEventArgs e);

では、次にイベントハンドラに登録した2つのメソッドを見てみましょう。
丸ごとだと分かりづらいので、必要な部分だけ抜粋します。


private static void Watcher_OnCreated(object sender, FileSystemEventArgs e)
{
}

private static void Watcher_OnDeleted(object sender, FileSystemEventArgs e)
{
}
管理人
管理人

おわかりいただけただろうか。

りさ
りさ

戻り値とか引数の形が同じってこと?

管理人
管理人

そうだよ。

このようにイベントハンドラには定義されたデリゲートと同じ形式のメソッドしか登録できません。
先程のパターンであれば、登録するメソッドの戻り値や引数の形を変えるとビルドエラーになります。

補足1つ1つの引数の名前は変更してもOKです。ただ、普通は同じ名前を使います。

デリゲート(delegate)とイベント(event)の関係

デリゲートはイベントと一緒に説明されることが多いのですが、別にイベント限定の型ではありません。
デリゲート型とは、単にメソッドの参照を保存できる型です。よって、こんな感じの記述ができます。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      FileSystemEventHandler fileSystemEventHandler = new FileSystemEventHandler(CalledMethod);

      // メソッドのように呼べる
      fileSystemEventHandler(null, null);
    }

    // 呼び出されるメソッド
    private static void CalledMethod(object sender, FileSystemEventArgs e)
    {
      Console.WriteLine("CalledMethod()が呼ばれました。");
    }
  }
}
管理人
管理人

このようにデリゲートに保存したメソッドを、まるで普通のメソッドみたいに呼び出せます。

補足C言語の関数ポインタが1番近い機能だと思います。

次にイベントですが、これはプロパティが1番似てます。
普通は簡易的な記述を使いますが、本当はこんな感じの実装になってます。


public event FileSystemEventHandler? Created
{
  add
  {
    _onCreatedHandler += value;
  }
  remove
  {
    _onCreatedHandler -= value;
  }
}
管理人
管理人

プロパティのget & setadd & removeな感じ。

ここで重要なポイントがあります。それはイベントに指定できる型がデリゲート型のみになることです。
つまり、デリゲートはメソッドへの参照を保持する型であり、デリゲート型を所有するのがイベントです。

りさ
りさ

わざわざイベントを経由する意味が分かりません。

管理人
管理人

超大雑把に言うと、デリゲートを直接扱うのは辛いからイベントを経由しよう的なやつ。

メソッドの登録(add)と削除(remove)

イベントおよびデリゲートは、=演算子でメソッドを代入するよりも、+演算子または-演算子で指定するほうが普通です。
演算子の意味はそのままで、メソッドを登録するのが+演算子、削除するのが-演算子です。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      using var watcher = new System.IO.FileSystemWatcher(@"C:\Temp");

      // イベントハンドラにメソッドを登録する
      watcher.Created += Watcher_OnCreated;
      watcher.Deleted += Watcher_OnDeleted;

      // イベントハンドラからメソッドを削除する
      watcher.Deleted -= Watcher_OnDeleted;

      watcher.EnableRaisingEvents = true;

      Console.WriteLine("Enterを入力すると終了します。");
      Console.ReadLine();
    }

    // ディレクトリまたはファイルが作成された場合に呼び出されるメソッド
    private static void Watcher_OnCreated(object sender, FileSystemEventArgs e)
    {
      Console.WriteLine($"ファイルが作成されました。FullPath={e?.FullPath}");
    }

    // ディレクトリまたはファイルが削除された場合に呼び出されるメソッド
    private static void Watcher_OnDeleted(object sender, FileSystemEventArgs e)
    {
      Console.WriteLine($"ファイルが削除されました。FullPath={e?.FullPath}");
    }
  }
}

さらに、この機能は複数個のメソッドを登録することができます。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      using var watcher = new System.IO.FileSystemWatcher(@"C:\Temp");

      // イベントハンドラに複数のメソッドを登録する
      watcher.Created += Watcher_OnCreated1;
      watcher.Created += Watcher_OnCreated2;
      watcher.Created += Watcher_OnCreated3;

      watcher.EnableRaisingEvents = true;

      Console.WriteLine("Enterを入力すると終了します。");
      Console.ReadLine();
    }

    private static void Watcher_OnCreated1(object sender, FileSystemEventArgs e)
    {
      Console.WriteLine($"Watcher_OnCreated1({e?.FullPath})");
    }

    private static void Watcher_OnCreated2(object sender, FileSystemEventArgs e)
    {
      Console.WriteLine($"Watcher_OnCreated2({e?.FullPath})");
    }

    private static void Watcher_OnCreated3(object sender, FileSystemEventArgs e)
    {
      Console.WriteLine($"Watcher_OnCreated3({e?.FullPath})");
    }
  }
}
管理人
管理人

ちなみに複数のメソッドは同時ではなく登録順に実行されます。

System.EventArgs

.NETには色々なパターンのイベントハンドラが定義されてますが、1番多いパターンがSystem.EventArgsを使った形式です。
これはイベントハンドラの引数をEventArgs型またはEventArgs型を継承した型に統一したパターンです。

管理人
管理人

なお、本当に1番多いかは知りません。僕の所感です。

りさ
りさ

経験的なやつですか?

管理人
管理人

はい。と言うかGUI系のイベントハンドラが殆どこれ。

最初にパターンの形を確認しましょう。こういうメソッドに対応するデリゲートです。


void EventRaised(object sender, EventArgs args);

1つ目のobject senderはイベントの送信元です。イベントは必ず別の誰か(object)が発生させます。
その発生者、つまりは送信元になったオブジェクトが入ってます。object型なのでis/as演算子とかでキャストして使います。

2つ目のEventArgs argsはイベントに関する情報が入ってます。サンプルコードを例にするならPathの情報が入ってました。
このEventArgs型は付加情報が存在しないイベントで利用され、大抵はEventArgs型を継承したクラスが利用されます。

サンプルコードで利用されたFileSystemEventArgs型EventArgs型を継承したクラスです。
https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemeventargs

また、自作イベントハンドラでこのパターンを利用する場合、EventArgs型を継承したクラスは名前を〇〇EventArgsとします。
もちろん言語的なルールではないので、違う名前でもビルドは通りますが、常識としてそういう名前にします。

TipsEventArgs型を継承した場合のクラス名は〇〇EventArgsとする。

あとがき

イベントにしろデリゲートにしろ、これ必要だよねって王道パターンは.NETに用意されてます。
それもあって自分で作ることは少なく、殆ど利用者として使うパターンが主な気がします。

◆ C#に関する学習コンテンツ

この記事は参考になりましたか?

関連記事

コメント

この記事へのコメントはありません。