イベントの考え方

2019年12月17日

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

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

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

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 myEvent = new Player();
        // PlayerインスタンスのAttackに実行したいメソッドをセット(代入)
        myEvent.Attack += myEvent.Kick;

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

実行したいメソッドのセットに=ではなく+=演算子を使うのはイベントの特徴です。
Punckも同時に実行したい(イベントのマルチキャスト)の場合、myEnent.Attackの行に続けて

 myEvent.Attack += myEvent.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 myEvent = new Player();
        // PlayerインスタンスのAttackに実行したいメソッドをセット(代入)
        myEvent.Attack += myEvent.Kick;
        myEvent.Attack += myEvent.Punch;

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

(参考)引数のある場合

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 myEvent = new Player();
        // PlayerインスタンスのAttackに実行したいメソッドをセット(代入)
        myEvent.Attack += myEvent.Kick;
        myEvent.Attack += myEvent.Punch;

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

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

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");
    }
}

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

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

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

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

using System;

public class EventClass
{
    public event Action<string> Attack;

    public EventClass() { }
    public EventClass(Action<string> action) => Attack += action;

    public void OnAttack(string name) => Attack(name);
}

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)
    {
        var player = new Player();
        var eventClass = new EventClass(player.Kick);
        eventClass.Attack += player.Punch;
        eventClass.OnAttack("falcon");
    }
}

2019年12月17日C#

Posted by hidepon