ジェネリックでstaticな変数

C#ジェネリック。 ListやDictionaryなどのコンテナクラスに実装されており「型情報を渡してその型を持つクラスを生成する」機能

こちらのサイトがとてもわかり易いですね! ufcpp.net

ジェネリックを使用すれば型毎にクラスを作成する必要はなく.. 便利ですがその中で一つこんな使い方もできるよ。というものをメモ✍

ジェネリックな変数をstaticとして使用する。ということ

using System;


public class Hoge<T>
{
    // ジェネリックな
    public static Hoge<T> Instance;
    
    public T Param { get; private set; }
    
    
    public Hoge(T param) =>
        Param = param;
}

public static class Test
{
    public static void Main()
    {
        if (Hoge<int>.Instance == null)
        {
            Hoge<int>.Instance = new Hoge<int>(1);
        }
        
        if (Hoge<string>.Instance == null)
        {
            Hoge<string>.Instance = new Hoge<string>("AAA");
        }
        
        Console.WriteLine(Hoge<int>.Instance.Param);
        Console.WriteLine(Hoge<string>.Instance.Param);
    }
}

------
出力

1
AAA

ジェネリック変数をstaticでもってシングルトンとして扱うと。 これを使用したシングルトンパターンの基底クラスを作成するのがよく見る使い方

using System;


public abstract class Singleton<T> where T : Singleton<T>, new()
{
    private static T instance;
    
    // インスタンスを取得する。なければ作成
    public static T Instance 
    {
        get 
        {
            if (instance == null)
            {
                instance = new T();
            }
            
            return Instance;
        }
    }
    
    // インスタンスを削除する
    public static void Destroy()
    {
        if (instance != null)
        {
            instance = null;
        }
    }
}

// Singleton<T>を継承したシングルトンクラス
public class TestSingleton : Singleton<TestSingleton>
{
    public int Hoge;
}


public static class Test
{
    public static void Main()
    {
        TestSingleton.Instance.Hoge = 1;
        
        Console.WriteLine(TestSingleton.Instance.Hoge);
        
        // シングルトン消す
        TestSingleton.Destroy();
    }
}




また変わった使い方の一つとして、 このstatic変数を応用してイベントを管理するクラスを作成してみる。

あるクラスから「A」というイベントが通知された場合、Aを購読しているクラスが反応する。 クラス間を疎結合にするためのイベント管理システム。

つまり、イベントを投げるクラスとイベントを購読するクラスはお互いを知らない。 イベント管理システムだけ知ってれば良い。

これを実現する方法は幾つもあります。Keyを文字列にするとか。

今回はジェネリックでstaticな変数を利用してみます。(そもそもジェネリックでstaticな変数を使わなくていいよね。という話は置いておいて)

いきなりコード

sharplab.io

/*
  SharpLab tools in Run mode:
    • value.Inspect()
    • Inspect.Heap(object)
    • Inspect.Stack(value)
    • Inspect.MemoryGraph(value1, value2, …)
*/
using System;
using System.Collections.Generic;

public class EventManager
{
    // EventManager内のみで使用されるイベント購読者を格納するいれもの
    // ジェネリックなので一つだけあればいい
    private class Container<TEvent> where TEvent : class
    {
        public static Dictionary<System.Object, Action<TEvent>> Listeners = new Dictionary<System.Object, Action<TEvent>>();
    }
    
    
    private static EventManager instance;
    
    public static EventManager Instance 
    {
        get 
        {
            if (instance == null)
            {
                instance = new EventManager();
            }
            
            return instance;
        }
    }
    
    
    public void Add<TEvent>(System.Object listener, System.Action<TEvent> action) where TEvent : class
    {
        var listeners = Container<TEvent>.Listeners;
        listeners.Add(listener, action);
    }
    
    public void Remove<TListener, TEvent>(TListener listener) where TEvent : class
    {
        var listeners = Container<TEvent>.Listeners;
        listeners.Remove(listener);
    }
    

    // 指定したイベントを購読しているデリゲートすべてを呼びだす
    public void Trigger<TEvent>(TEvent ev) where TEvent : class
    {
       var listeners = Container<TEvent>.Listeners;
       foreach (var elm in listeners.Values)
       {
           elm(ev);
       }
    }
}


public static class Test
{
    // イベント
    public class HogeEvent
    {
        public int Num;
    }
    
    
    // イベント受ける側
    public class Listener
    {
        public Listener()
        {
            EventManager.Instance.Add<HogeEvent>(this, 
                                                 ev =>
                                                 {
                                                     // 受け取ったイベントの中身を出力
                                                     Console.WriteLine(ev.Num);
                                                 });
        }
    }
    
    // イベント送る側
    public class Sender
    {
        public void TriggerEvent()
        {
            EventManager.Instance.Trigger(new HogeEvent { Num = 999 });
        }
    }
    
        
    public static void Main()
    {
        // イベント受ける側と送る側を作成
        // ListenerAとBはお互い知らなくていい
        var listener = new Listener();
        
        var sender = new Sender();
        sender.TriggerEvent();
    }
}

-----
出力

999

EventManagerがシングルトンクラスで内部でとあるイベントクラスに紐付いてイベントを購読するデリゲートを管理している。

そして、Listenerはイベントを待ち受けるデリゲートをEventManagerに送るだけで HogeEvent が通知されたときに勝手にデリゲートが呼び出される。



システム的には強力なのかなと。 これだけでかんたんにクラス間を疎結合にできますし、このイベント通知システムは結構便利です。

ただ、まぁめちゃめちゃなコードですね。

ContainerのListenersはstatic変数で破棄されていないのでEventクラスが増殖するたびにDIctionaryもその分増えていくと。

起動時一度しか使われないイベントでもDictionaryを生成するのでゴミDictionaryが増えていくわけです。
ListenerがRemoveし忘れるとEventManagerでそのデリゲートを保持し続けるわけですから、Listener自体も消えずにゾンビになります。

Listenerが消えるときにRemoveを忘れないようにしなければならない。

しかし破棄の処理はRemoveAllというメソッドがないと使いにくいですね。

後、Triggerのデリゲート内でRemoveした場合、foreach最中なのでExceptionが飛んできます。

色々足りないコードですが、機能を理解するためだけであれば十分かなと。

そしてジェネリックでstaticな変数を利用すればこのようにインナークラスで色んな型のイベントを購読するクラスも簡単に作れました。



このEventManagerですが、UniRxにある MessageBroker がこのイベント購読に近い機能かなと。 あちらのほうがパワフルで AddTo(this) するだけでMonoBehaviour削除時にイベント破棄してくれます。

ただこの短いコードでもMessageBrokerに似たことが出来たので、「全イベント破棄処理。」「MonoBehaviour削除時に自動で購読破棄。」 などなど、足りない機能を自作して自分なりのEventManagerを作るのも勉強になり面白いですね