[Unity] enumと自作のEnumerationクラスの整合性をチェックしたい

# はじめに
Unityを使ったゲーム開発において、enumを使うことはよくあると思います。特に[SerializedField]を用いてインスペクター上から何かしら設定を行うときに便利です。
しかし、enumの機能は少なく、拡張性に乏しいため、自作のEnumerationクラスを作る場合があると思います。

そこで、enumに対して自作のEnumerationクラスが正しく定義できているかどうかをチェックする関数を作ってみました。

# コード
```EnumEnumerationChecker.cs
public static class EnumEnumerationChecker
    {
        // Method to check if TEnum and TEnumeration have the same number of elements and matching names
        public static bool CheckEnum<TEnum,TEnumeration>() 
            where TEnum : Enum
            where TEnumeration : Enumeration
        {
            // Ensure that the number of elements in TEnum matches the number of elements in TEnumeration
            if(Enum.GetValues(typeof(TEnum)).Length != Enumeration.GetAll<TEnumeration>().Count())
            {
                Debug.LogWarning($"The number of {typeof(TEnum)} and {typeof(TEnumeration)} is different.");
                return false;
            }
            
            foreach (var type in Enum.GetValues(typeof(TEnum)).Cast<TEnum>())
            {
                var enumeration = Enumeration.GetAll<TEnumeration>().FirstOrDefault(x => x.Name == type.ToString());
                if (enumeration == null)
                {
                    Debug.LogWarning($"{typeof(TEnumeration)} with name {type.ToString()} not found.");
                    return false;
                }
            }

            return true;
        }
    }
```

```Enumeration.cs
    public abstract class Enumeration : IComparable
    {
        public int Id { get; }
        public string Name { get; }
        
        public static IEnumerable<T> GetAll<T>() where T : Enumeration =>
            typeof(T).GetFields(BindingFlags.Public |
                                BindingFlags.Static |
                                BindingFlags.DeclaredOnly)
                .Select(f => f.GetValue(null))
                .Cast<T>();
        
        protected Enumeration(int id, string name)
        {
            Id = id;
            Name = name;
        }

        #region Overrides
        public int CompareTo(object? other)
        {
            if (other == null) return 1;
            return Id.CompareTo(((Enumeration) other).Id);
        }

        public static bool operator ==(Enumeration? a, Enumeration? b)
        {
            if (ReferenceEquals(a, b))
                return true;

            if (a is null || b is null)
                return false;

            return a.Id == b.Id && a.Name == b.Name;
        }

        public static bool operator !=(Enumeration? a, Enumeration? b)
        {
            return !(a == b);
        }

        public override bool Equals(object? obj)
        {
            if (obj is Enumeration enemyEnum) return this == enemyEnum;
            return false;
        }

        public override int GetHashCode()
        {
            return Id.GetHashCode() ^ Name.GetHashCode();
        }

        public override string ToString()
        {
            return Name;
        }

        #endregion
    }

```

# テストコード

<details>
  <summary>テストコード</summary>
  
```EnumEnumerationCheckerTest.cs
public class EnumEnumerationCheckerTest
{
    [Test]
    public void CheckEnumType1()
    {
        Assert.IsTrue(EnumEnumerationChecker.CheckEnum<EnumType1, TestEnum>());
    }

    [Test]
    public void CheckEnumType2()
    {
        Assert.IsFalse(EnumEnumerationChecker.CheckEnum<EnumType2, TestEnum>());
    }
    
    [Test]
    public void CheckEnumType3()
    {
        Assert.IsFalse(EnumEnumerationChecker.CheckEnum<EnumType3, TestEnum>());
    }
    
    [Test]
    public void CheckEnumType4()
    {
        Assert.IsFalse(EnumEnumerationChecker.CheckEnum<EnumType4, TestEnum>());
    }
    enum EnumType1
    {
        A,
        B
    }

    enum EnumType2
    {
        A,
        B,
        C
    }

    enum EnumType3
    {
        A
    }

    enum EnumType4
    {
        X,Y
    }
    
    class TestEnum : Enumeration
    {
        public static TestEnum[] Values { get; }

        TestEnum(int id, string name) : base(id, name)
        {
        }

        static TestEnum()
        {
            Values = new[]
            {
                A, B
            };
        }

        public static TestEnum A = new(0, "A");
        public static TestEnum B = new(1, "B");
    }
}

```

</details>




# 使用例
```EnemyEnum.cs
public class EnemyEnum : Enumeration
{
    public bool IsBoss { get; }
    
    // Static array to hold all the possible values of EnemyEnum
    public static EnemyEnum[] Values { get; }
    
    EnemyEnum(int id, string name, bool isBoss = false) : base(id, name)
    {
        IsBoss = isBoss; 
    }

    // Static constructor to initialize the Values array
    static EnemyEnum()
    {
        Values = new[]
        {
            None,
            A,
            B,
            C,
            Boss,
        };
    }
    
    public static EnemyEnum None = new(0, "None");
    public static EnemyEnum A = new(1, "A");
    public static EnemyEnum B = new(2, "B");
    public static EnemyEnum C = new(3, "C");
    public static EnemyEnum Boss = new(4, "Boss", true);
}
```

```EnemyParam.cs
[Serializable]
public sealed class EnemyParam
{
    [Header("敵のパラメータを設定するクラス")]

    [Header("敵の種類")]
    [SerializeField]
    EnemyType enemyType = EnemyType.None;  // privateにすることで、EnemyEnumを使うことを強制する

    [Header("攻撃力やHPなどのパラメータ")]
    public EnemyAttackParam enemyAttackParam = null!;
    public EnemyHpParam enemyHpParam = null!;
    // ... その他パラメータ

    public EnemyParam()
    {
        // ここでチェックする
        EnumEnumerationChecker.CheckEnum<EnemyType,EnemyEnum>();
    }

    public EnemyEnum GetEnemyEnum => EnemyEnum.Values.First(x => x.Name == enemyType.ToString()); 

    // EnemyTypeはprivateとしておく。実際のコードはEnumerationクラスのサブクラスである、EnemyEnumを使うことになる。
    enum EnemyType
    {
        None,
        A,
        B,
        C,
        Boss
    }
}

```

# 参考記事
https://learn.microsoft.com/ja-jp/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/enumeration-classes-over-enum-types