◆ JavaScript の EventEmitter をブラウザ用に作ったのを思い出した

C# のイベントハンドラ周りは デザイナ・XAML で自動生成されたところにコード書くくらいな使い方で いまいち仕組みがわかってなかったので少し調べてみました

基本使うだけなら Famework に用意されたハンドラに関数をセットしておけば 各ハンドラのイベントが起きたときに関数が実行されます

作るなら

自分でイベントが起きたときに何かするようなハンドラを作ってみます
public class Foo
{
    public class EventFooArgs : EventArgs
    {
        public object data1 { get; set; }
        public string data2 { get; set; }
        public int data3 { get; set; }
    }

    public delegate void EventFooHandler(object sender, EventFooArgs e);
    public event EventFooHandler handler;

    public void trigger(EventFooArgs e)
    {
        this.handler?.Invoke(this, e);
    }
}

イベントのときに渡されるオブジェクトを EventFooArgs と言う名前で作りました
用意されてるのでもいいのですが イベント時に渡したい値に応じて作るほうがいいと思います

次にデリゲートを作って event というキーワードに作ったデリゲートとハンドラ名を指定します

デリゲートは Action や Func みたいなものでラムダ式の引数と返り値の型に名前をつけてるようなものです

上の
public delegate void EventFooHandler(object sender, EventFooArgs e);
だと 引数が object 型の sender と EventFooArgs 型の e の 2 つで返り値はなし (void) の関数を EventFooHandler と言う名前にしてます

「Action<object, EventFooArgs>」 って長く書かなくて済みます
基本ラムダ式だけでいいのですが こういう長い型のラムダ式にデリゲートで名前定義しておいて Action や Function の代わりにそっち使うとちょっと楽で見やすいです

C# の経緯的には 先にデリゲートで後からラムダ式だそうです


event なんて普段見ないキーワードもありますし event で定義されたものは += と -= でハンドラのつけはずしができたりとちょっと特殊なものです

しかもめったに使わないので覚えれる気がしません


ところで Event 引数の型の EventFooArgs は EventArgs を継承しています
マナーみたいなものらしいですが 継承しないからってエラーにはなりません
Event 引数共通のベースクラスがあったほうが扱いやすいから ってところでしょうか

event 使わない

覚えれる気もしないのと 中では別に特殊なことしてるわけでもなさそうなので 簡単に動きを再現できそうです

シンプルに

使い方を合わせることは気にせずできることを合わせる感じで作ります
public class Bar
{
    public class EventBarArgs : EventArgs
    {
        public object data { get; set; }
    }

    private List<Action<EventBarArgs>> handlers { get; } = new List<Action<EventBarArgs>>();

    public void addHandler(Action<EventBarArgs> handler) => this.handlers.Add(handler);
    public void removeHandler(Action<EventBarArgs> handler) => this.handlers.Remove(handler);

    public void trigger(EventBarArgs e)
    {
        foreach(var handler in this.handlers)
        {
            handler(e);
        }
    }
}

これだけです
addHandler メソッドと removeHandler メソッドでつけはずしします
やってることは List に追加と削除です

シンプルですが ちょっと長くなりました

使い方も合わせてみる

使い方も似せてみます
public class Baz
{
    public class EventBazArgs : EventArgs
    {
        public object data { get; set; }
    }

    public class BazHandler
    {
        private List<Action<EventBazArgs>> handlers { get; } = new List<Action<EventBazArgs>>();

        public static BazHandler operator +(BazHandler a, Action<EventBazArgs> b)
        {
            a.handlers.Add(b);
            return a;
        }
        public static BazHandler operator -(BazHandler a, Action<EventBazArgs> b)
        {
            a.handlers.Remove(b);
            return a;
        }
        public void call(EventBazArgs e)
        {
            foreach (var handler in this.handlers)
            {
                handler(e);
            }
        }
    }

    public BazHandler handler { get; set; } = new BazHandler();

    public void trigger(EventBazArgs e)
    {
        this.handler.call(e);
    }
}

ちょっと長くなりましたが演算子オーバーライドして += と -= できるようにしてます
= はなしで + と - だけでもいいのですが見た目を合わせるために += としてる都合で handler は get; のみじゃなくて set; も許可しています
別のインスタンスに差し替え防ぐために set; はなにもしないようにもできますが 長くなるのでここでは set 可能にしてます

あと 似せるとはいいましたが sender は別にいらなかったのでつけていません

BazHandler クラスは Generics で EventBazArgs を置き換えれるようにすれば使いまわせるので 外に出せばもっとすっきりします

共通部分で使える部分を外に出すと

public class Handler<T>
{
    private List<Action<object, T>> handlers { get; } = new List<Action<object, T>>();

    public static Handler<T> operator +(Handler<T> a, Action<object, T> b)
    {
        a.handlers.Add(b);
        return a;
    }
    public static Handler<T> operator -(Handler<T> a, Action<object, T> b)
    {
        a.handlers.Remove(b);
        return a;
    }
    public void call(object sender, T e)
    {
        foreach (var handler in this.handlers)
        {
            handler(sender, e);
        }
    }
}

public class Qux
{
    public class EventQuxArgs : EventArgs
    {
        public object data { get; set; }
    }

    public Handler<EventQuxArgs> handler { get; set; } = new Handler<EventQuxArgs>();

    public void trigger(EventQuxArgs e)
    {
        this.handler.call(this, e);
    }
}

たったこれだけ
こっちは一応比較のために sender もいれてます
event のときとほぼ変わらない長さになりましたね

使う

それぞれのハンドラ追加して呼び出すまで
public static void exec()
{
    var foo = new Foo();
    foo.handler += (s, e) =>
    {
        Console.WriteLine(e);
    };
    foo.trigger(new Foo.EventFooArgs { data1 = 1, data2 = "2", data3 = 3 });

    var bar = new Bar();
    bar.addHandler(e =>
    {
        Console.WriteLine(e);
    });
    bar.trigger(new Bar.EventBarArgs { data = 1 });

    var baz = new Baz();
    baz.handler += e =>
    {
        Console.WriteLine(e);
    };
    baz.trigger(new Baz.EventBazArgs { data = 1 });

    var qux = new Qux();
    qux.handler += (s, e) =>
    {
        Console.WriteLine(e);
    };
    qux.trigger(new Qux.EventQuxArgs { data = 1 });
}
Foo.EventFooArgs { data1=1, data2="2", data3=3 }
Bar.EventBarArgs { data=1 }
Baz.EventBazArgs { data=1 }
Qux.EventQuxArgs { data=1 }


覚えられない event + delegate より自分で作った方でいい気もします
せっかくキーワードまであるような機能なので これだけじゃなくもっと機能がなにかがあるような気もしますが あったところでこれだけで十分ともいえます


小さいことですが event を使うと呼び出すときに null かどうかの判定が必要で
this.handler?.Invoke(sender, e);
という書き方になります (? を忘れないように)

今回の自作のだと List なので 0 件でも null は気にしないでよいです
単純に foreach で何も実行されないだけです