イベントの考え方
「何かが起こったら」→「どうする」式でプログラムすることをイベントドリブン方式(イベント駆動方式)と言います。
「ボタンを押されたら」→「メニューを表示する」などですね。この「ボタンを押されたら」がイベント、「メニューを表示する」がイベントハンドラになります。イベントハンドラは、イベントが発生した時に実行される一連の処理と言えます。
基本
コード
で、早速、具体例をみていきましょう。
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("パンチした");
}
}
メインメソッドからアクセスするときは、
- Playerのインスタンスを作成
- PlayerインスタンスのAttackに実行したいメソッドをセット(代入)
- 実行用のメソッドを呼び出す。
これをコードにすると
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 メソッドを呼び出しています。
ディスカッション
コメント一覧
まだ、コメントがありません