イベントの考え方

2023年2月24日

「何かが起こったら」→「どうする」式でプログラムすることをイベントドリブン方式(イベント駆動方式)と言います。

「ボタンを押されたら」→「メニューを表示する」などですね。この「ボタンを押されたら」がイベント、「メニューを表示する」がイベントハンドラになります。イベントハンドラは、イベントが発生した時に実行される一連の処理と言えます。

基本

コード

で、早速、具体例をみていきましょう。

public void Kick()
{
    Console.WriteLine("キック");
}

このシグネチャは、

  • 戻り値なし
  • 引数なし

になります。このシグネチャをデリゲート型で表すと、

delegate void Handler();

となります。Handlerはネーミングなので意味が通じればどのように名付けても構いません。
Handlerのシグネチャを見てみると、

  • 戻り値なし
  • 引数なし

で、同じパターンです。

これをもとにPlayerクラスを作成してみましょう。
Punchも追加しています。

public class Player
{
    public delegate void Handler();

    public void Kick()
    {
        Console.WriteLine("キックした");
    }

    public void Punch()
    {
        Console.WriteLine("パンチした");
    }
}

次にMainメソッドに、Playerのインスタンスを作成し、アタックしたら登録してある「Kick」「Punch」のどちらかの処理をすることを考えます。

準備として、Playerクラスに、「Kick」「Punch」のどちらを選択するかを外部から登録できるようにします。
そのために、eventキーワードが用意されています。

 public event Handler Attack;
  • Handlerは、デリゲートで設定した名前です。
  • Attackは、外部から登録されるメソッドを格納するためのフィールド(イベントフィールド)です。
  • Atttackは、外部からはフィールドのように見える。

このAttackは、メソッドを格納するものです。外部からセット(代入)されるようにします。

外部から、OnAttack()メソッドが呼び出されるとAttackが実行されるように記述します。(Attackを実行するときは、Attack();となります)

public void OnAttack()
{
    // イベントフィールドにセットされたイベントが実行される
    Attack();
}

一般的に、このメソッドの名前付けは、On+イベントハンドラ名をします。

まとめると、

public class Player
{
    public delegate void Handler();

    public event Handler Attack;

    public void OnAttack()
    {
     // イベントフィールドにセットされたイベントが実行される
        Attack();
    }

    public void Kick()
    {
        Console.WriteLine("キックした");
    }

    public void Punch()
    {
        Console.WriteLine("パンチした");
    }
}

メインメソッドからアクセスするときは、

  1. Playerのインスタンスを作成
  2. PlayerインスタンスのAttackに実行したいメソッドをセット(代入)
  3. 実行用のメソッドを呼び出す。

これをコードにすると

class Program
{
    static void Main(string[] args)
    {
        // Playerのインスタンスを作成
        Player player = new Player();
        // PlayerインスタンスのAttackに実行したいメソッドをセット(代入)
        player += player.Kick;

        // 実行用のメソッドを呼び出す。
        player.OnAttack();
    }
}

実行したいメソッドのセットに=ではなく+=演算子を使うのはイベントの特徴です。

実行結果

キックした

Punckも同時に実行したい(イベントのマルチキャスト)の場合、myEnent.Attackの行に続けて

他のメソッドも追加してみる

 player.Attack += player.Punch;

を追記します。結果は、

実行結果

キックした
パンチした

となれば、OKです。

まとめたコード

using System;

public class Player
{
    public delegate void Handler();

    public event Handler Attack;

    public void OnAttack()
    {
     // イベントフィールドにセットされたイベントが実行される
        Attack();
    }

    public void Kick()
    {
        Console.WriteLine("キックした");
    }

    public void Punch()
    {
        Console.WriteLine("パンチした");
    }

}
class Program
{
    static void Main(string[] args)
    {
        // Playerのインスタンスを作成
        Player player = new Player();
        // PlayerインスタンスのAttackに実行したいメソッドをセット(代入)
        player.Attack += player.Kick;
        player.Attack += player.Punch;

        // 実行用のメソッドを呼び出す。
        player.OnAttack();
    }
}
class Program
{
    static void Main()
    {
        // Playerのインスタンスを作成
        var player = new Player();

        // AttackイベントにKickとPunchのハンドラーを追加
        player.Attack += player.Kick;
        player.Attack += player.Punch;

        // Attackイベントを発生させるためのメソッドを呼び出す。
        player.OnAttack();
    }
}

public class Player
{
    public event Action Attack;

    // Attackイベントを発生させるメソッド
    public void OnAttack()
    {
        Attack?.Invoke();
    }

    // キックするメソッド
    public void Kick()
    {
        Console.WriteLine("キックした");
    }

    // パンチするメソッド
    public void Punch()
    {
        Console.WriteLine("パンチした");
    }
}

(参考)引数のある場合

using System;


public class Player
{
    public delegate void Handler(string name);

    public event Handler Attack;

    public void OnAttack(string name)
    {
     // イベントフィールドにセットされたイベントが実行される
        Attack(name);
    }

    public void Kick(string str)
    {
        Console.WriteLine($"{str}のキック");
    }

    public void Punch(string str)
    {
        Console.WriteLine($"{str}のパンチ");
    }

}
class Program
{
    static void Main(string[] args)
    {
        // Playerのインスタンスを作成
        Player player = new Player();
        // PlayerインスタンスのAttackに実行したいメソッドをセット(代入)
        player.Attack += player.Kick;
        player.Attack += player.Punch;

        // 実行用のメソッドを呼び出す。
        player.OnAttack("falcon");
    }
}
// プレイヤークラスの定義
public class Player
{
    // 攻撃イベントの定義
    public event Action<string> Attack;
    // 攻撃イベントを呼び出すメソッド
    public void OnAttack(string name)
    {
        Attack?.Invoke(name);
    }

    // キックをするメソッド
    public void Kick(string name)
    {
        Console.WriteLine($"{name}のキック");
    }

    // パンチをするメソッド
    public void Punch(string name)
    {
        Console.WriteLine($"{name}のパンチ");
    }
}

// プログラムのエントリーポイント
class Program
{
    static void Main(string[] args)
    {
        // プレイヤーのインスタンスを作成
        Player player = new Player();
        // プレイヤーの攻撃イベントにキックとパンチのメソッドを登録
        player.Attack += player.Kick;
        player.Attack += player.Punch;

        // 攻撃イベントを発火させる
        player.OnAttack("falcon");
    }
}

変更点:

  • delegate の代わりに Action<T> を使用して Player クラスのイベントを定義しました。
  • OnAttack メソッドの Attack(name) の部分を Attack?.Invoke(name) に変更しました。これにより、Attack イベントが null である場合に例外が発生するのを回避できます。
  • Kick メソッドと Punch メソッドの引数名を str から name に変更しました。
  • Main メソッドで使用する Player インスタンスの変数名を myEvent から player に変更しました。
  • Program クラスのコンストラクタが不要であるため、削除しました。

イベント関連の処理を別クラスに分けます。

using System;

public class EventClass
{
    public delegate void Handler(string name);

    public event Handler Attack;

    // 上2行をまとめて次のようにすることもできます。
    //  public event Action<string> Attack;

    public void OnAttack(string name)
    {
     // イベントフィールドにセットされたイベントが実行される
        Attack(name);
    }
}

public class Player
{
    public void Kick(string str)
    {
        Console.WriteLine($"{str}のキック");
    }

    public void Punch(string str)
    {
        Console.WriteLine($"{str}のパンチ");
    }

}
class Program
{
    static void Main(string[] args)
    {
        // イベントをコントロールするクラスを独立させます
        EventClass eventClass = new EventClass();
        // Playerのインスタンスを作成
        Player player = new Player();
        // EventClassのインスタンスに実行したいメソッドをセット(代入)
        eventClass.Attack += player.Kick;
        eventClass.Attack += player.Punch;

        // 実行用のメソッドを呼び出す。
        eventClass.OnAttack("falcon");
    }
}
// イベントクラスの定義
public class EventClass
{
    // Action型のジェネリックイベントを定義
    public event Action<string> Attack;
    // Attackイベントを発生させるメソッド
    public void OnAttack(string name)
    {
        Attack?.Invoke(name);
    }
}

// プレイヤークラスの定義
public class Player
{
    // キックを行うメソッド
    public void Kick(string name)
    {
        Console.WriteLine($"{name}のキック");
    }
    // パンチを行うメソッド
    public void Punch(string name)
    {
        Console.WriteLine($"{name}のパンチ");
    }
}

// プログラムの実行クラス
class Program
{
    static void Main(string[] args)
    {
        // EventClassのインスタンスを作成
        EventClass eventClass = new EventClass();
        // Playerのインスタンスを作成
        Player player = new Player();
        // AttackイベントにKickメソッドとPunchメソッドを登録
        eventClass.Attack += player.Kick;
        eventClass.Attack += player.Punch;
        // OnAttackメソッドを実行し、Attackイベントを発生させる
        eventClass.OnAttack("falcon");
    }
}

変更点:

  • イベントハンドラの型を Action に変更しました。これにより、EventClass に定義された delegate は不要になります。
  • Attack イベントのインスタンスが null でないことを確認するために、Null条件演算子 (?.) を使用しました。
  • Console.WriteLine メソッドで文字列を出力する際に、引数の名前を “str" から “name" に変更しました。また、文字列のフォーマットにも微調整を加えました。

イベントとデリゲートとの違い

実は、eventキーワードがなくても上記プログラムは動作します。
では、なぜイベントの機能が必要なのでしょうか?

  • デリゲートはどのクラスにも記述できますが、イベントは、イベントフィールドが宣言されているクラス内にしか記述することができません。イベントの宣言とハンドラは同一が望ましいとの考えからです。
  • デリゲートは直接実行可能ですが、イベントは許されていません。(間接的な実行になります)この制限により、“イベントに関連付けたコールバック(関数)はイベントが発生した時に呼び出される" という形式になります。 
  • デリゲートは、登録に=演算子と+=演算子を使えますが、イベントは、+=演算子しか使えますせん。

コードを省略化したパターンを次に示します。(引数ありのコンストラクタを追記しています)

public class EventClass
{
    // Action<string>型のAttackという名前のイベントを宣言
    public event Action<string> Attack;
    // Attackイベントがnullでない場合にnameを引数として呼び出す
    public void OnAttack(string name) => Attack?.Invoke(name);

}

public class Player
{
    // nameを引数にして、キックのメッセージを表示するメソッド
    public void Kick(string name) => Console.WriteLine($"{name}のキック");
    // nameを引数にして、パンチのメッセージを表示するメソッド
    public void Punch(string name) => Console.WriteLine($"{name}のパンチ");
}

class Program
{
    static void Main(string[] args)
    {
        // EventClassクラスのインスタンスを生成
        var eventClass = new EventClass();
        // Playerクラスのインスタンスを生成
        var player = new Player();
        // AttackイベントにKickメソッドとPunchメソッドを追加
        eventClass.Attack += player.Kick;
        eventClass.Attack += player.Punch;
        // OnAttackメソッドを呼び出し、"falcon"を引数として渡す
        eventClass.OnAttack("falcon");
    }
}

ここでは、ラムダ式を使って OnAttack メソッドを簡潔に表現しています。また、var キーワードを使って変数の型を推論させています。

おまけ(難易度高)

オブザーバーパターンにしてみる

そもそもイベントがおブザーパターンであるため、これに変更する必要はありません
ベースとなっているデザインパターンの1つのオブザーバーパターンで実装して、インターフェースがその仕組みを使っていることを学びましょう

// IAttackObserver インターフェースを定義
public interface IAttackObserver
{
    // Updateメソッドを定義し、引数としてnameを受け取る
    void Update(string name);
}

// EventClassクラスを定義
public class EventClass
{
    // IAttackObserverを格納するListを定義
    private List<IAttackObserver> _observers = new List<IAttackObserver>();
    // IAttackObserverを追加するAddObserverメソッドを定義
    public void AddObserver(IAttackObserver observer) => _observers.Add(observer);

    // IAttackObserverを削除するRemoveObserverメソッドを定義
    public void RemoveObserver(IAttackObserver observer) => _observers.Remove(observer);

    // 攻撃が発生した際に、登録されたIAttackObserver全員のUpdateメソッドを呼び出すOnAttackメソッドを定義
    public void OnAttack(string name)
    {
        foreach (var observer in _observers)
        {
            observer.Update(name);
        }
    }
}

// Playerクラスを定義し、IAttackObserverインターフェースを実装
public class Player : IAttackObserver
{
    // Kickメソッドを定義し、nameを受け取る
    public void Kick(string name) => Console.WriteLine($"{name}のキック");
    // Punchメソッドを定義し、nameを受け取る
    public void Punch(string name) => Console.WriteLine($"{name}のパンチ");

    // Updateメソッドを実装し、KickとPunchを呼び出す
    public void Update(string name)
    {
        Kick(name);
        Punch(name);
    }
}

class Program
{
    static void Main(string[] args)
    {
        // EventClassとPlayerのインスタンスを生成
        var eventClass = new EventClass();
        var player = new Player();
        // PlayerをIAttackObserverとしてEventClassに登録し、攻撃を発生させる
        eventClass.AddObserver(player);
        eventClass.OnAttack("falcon");

        // PlayerをIAttackObserverから削除する
        eventClass.RemoveObserver(player);
    }
}

ここでは、IAttackObserver インターフェースを定義し、Player クラスがそれを実装することで、EventClass クラスからの通知を受け取るようにしています。EventClass クラスでは、AddObserver メソッドと RemoveObserver メソッドでオブザーバーを追加・削除し、OnAttack メソッドでオブザーバーに通知を送ります。Player クラスでは、Update メソッドで通知を受け取り、Kick メソッドと Punch メソッドを呼び出しています。

C#

Posted by hidepon