（c#/Unity/IL2CPP/Mono）Enum：如何なる場面でも高速に動作するけれど、如何ほどのスピード狂でも使用を躊躇うイディオム「CEnum」の紹介

# 前振り

普段、何気なく書いているコードですが、「環境Aでは高速に動作するのに、環境Bで実行してみたら遅くなった」という経験をされた方いませんか？  
<br>

或いは、「環境Aで高速に動作するから、環境Bでも大丈夫」と高を括り、知らない内に環境Bでは遅く動いている、なんて事もあるかも知れませんが、心当たりのある方いませんか？  
<br>

今回は、Enumに纏わるお話です。  
たかがEnum、されどEnum、今一度Enumを見直す機会と捉えてご覧頂けると幸いです。  

# 本題

## Enumは、環境だけでなく、ユースケースでも実行パフォーマンスにバラつきがある

Enumには、主に三通りの扱い方があります。  

- enum
- System.Enum
- where System.Enum

これらは、Enumを使う上で付いて回るものなので、回避のしようがありません。  
<br>

よく使われるケースは、以下の３つでしょうか。  
 
- 配列の添え字（intへのキャスト）
- switch case
- Flags属性
<br>

つまりは、以下の表の空欄毎に実行パフォーマンスが異なり、更に環境毎でも異なるという話です。  
|パフォーマンス表| enum | System.Enum | where System.Enum |
|:-:|:-:|:-:|:-:|
|配列の添え字（intへのキャスト）|   |   |   |
|switch case|   |   |   |
|Flags属性|   |   |   |
<br>

## では、この問題をどう解決するか

遅くなりようの無いコードを書く、それだけです。  

しかし、ただ利点を享受するだけでは済まないのが、今回紹介するイディオム「CEnum」です。  
このイディオムには２つ欠点がありますが、ともあれ話を進めます。  
<br>

先ずは、基底classです。  

~~~ c#
public class CEnum<T>//型不定の基底class
{
	public readonly T Value;
	
	protected CEnum(T v){ Value = v; }
}

public class CEnum : CEnum<int>//型確定した基底class
{
	public const int Length = 0;//このCEnumの長さ
	
	public const int Default = 0; public static readonly CEnum EDefault = new(Default);//Defaultの定義
	
	protected CEnum(int v):base(v){}
}
~~~
<br>

CEnumのユーザー定義はこの様になります。  

~~~ c#
public class EUser : CEnum//ユーザー定義class
{
	public new const int Length = CEnum.Length + 2;//このCEnumの長さ
	
	public const int Hoge = 0; public static readonly EUser EHoge = new(Hoge);//要素の定義
	public const int Fuga = 1; public static readonly EUser EFuga = new(Fuga);//要素の定義
	
	protected EUser(int v):base(v){}
}
~~~

継承めいた事も出来ます。  

~~~ c#
public class ETest : EUser//EUserの継承class
{
	public new const int Length = EUser.Length + 1;//このCEnumの長さ
	
	public const int Piyo = 2; public static readonly ETest EPiyo = new(Piyo);//要素の定義
	
	protected ETest(int v):base(v){}
}
~~~

Flags属性めいた事も出来ます。

~~~ c#
public class EFlags : CEnum//Flags属性の真似事class
{
	public new const int Length = CEnum.Length + 2;//このCEnumの長さ
	
	public const int IsHoge = (1<<0); public static readonly EFlags EIsHoge = new(IsHoge);//要素の定義
	public const int IsFuga = (1<<1); public static readonly EFlags EIsFuga = new(IsFuga);//要素の定義
	
	protected EFlags(int v):base(v){}
}
~~~
<br>

そう、とにかく面倒くさい事この上ないのが、このイディオム１つ目の欠点です。  

面倒くさいの一言で片付けましたが、色々と言いたい事はあるでしょう。  
まぁ今は、そうした事は全て飲み込んで頂いて、話を進めて行きますね。  

## CEnumの使い方

CEnumでは、要素の扱い方が二通りあります。  
- 確定型を使う場合
  - public constで定義した名前を使う
      - 利点：定数値を直接扱える
      - 欠点：CEnum型情報を失う（whereで制約できない）
- CEnum型を使う場合
  - public static readonlyで定義した名前を使う
      - 利点：whereで制約できる
      - 欠点：CEnumの運用ルールを覚える必要がある（このイディオム２つ目の欠点）

## CEnumの運用ルール

確定型を使う場面

~~~ c#
//定数値を配列の添え字に使用（Enumとは異なり、intへのキャストは不要）
SomeArray[EUser.Hoge] = SomeValue;
~~~

~~~ c#
//caseの条件
switch (){
    case EUser.Hoge: break;
    case EUser.Fuga: break;
}
~~~

~~~ c#
//Flags
if (SomeFlags & EFlags.IsHoge){}
if (SomeFlags & EFlags.IsFuga){}
~~~
<br>

CEnum型を使う場面

~~~ c#
//変数に取る
var e = EUser.EHoge;
~~~

~~~ c#
//変数値を配列の添え字に使用（Valueメンバ変数を使う）
SomeArray[e.Value] = SomeValue;
~~~

~~~ c#
//ジェネリック メソッドの引数に渡す
SomeGenericMethod(EUser.EHoge);
SomeGenericMethod(e);
~~~

~~~ c#
//switchの因子（Valueメンバ変数を使う）
switch (e.Value){
}
~~~

## ジェネリック（Generic）の型制約

CEnumは、whereで制約できるので、継承めいた事をする時に役立ちます。  

~~~ c#
void SomeGenericMethod<E>(E e) where E : EUser
{
}
~~~

# ベンチマーク

環境依存になるので、あくまで参考程度です。  
横軸は秒で、グラフが短いほど高速です。  

- 列挙型種
  - CEnum：CEnumを扱った場合
  - enum：enumを扱った場合
  - Enum：enumをSystem.Enumとして扱った場合
  - where：enumをwhere System.Enumで扱った場合
- テスト内容
  - value：列挙型種をintとして扱った場合
  - switch：列挙型種をswitch caseで扱った場合

![Visual Studio 2022（10,000,000回ループ）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/379361/a99d612e-19e0-6f8a-b1e5-f238d13039d2.png)

![Unity 2022.3.35f1 IL2CPP（1,000,000回ループ）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/379361/955218ab-dd00-62e0-17f5-e6b78fabfb7a.png)

![Unity 2022.3.35f1 Mono（1,000,000回ループ）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/379361/51d9e798-2759-6e35-b0b2-bcc0c2aba382.png)

[サンプルコード](https://github.com/EmuraDaisuke/CEnum/tree/main/CEnum)を用意したので、気になる方はお試し下さい。  

# まとめ

c#標準の列挙型を使う場合、enumで使えるケースはさほど多くなく、大抵の場合はwhere System.Enumで使われ、System.Enumは止む無く使われる、という温度感だと思います。  

コード中にあるSystem.Enumキーワードは、そこに多大な負荷が掛かるリスクを示している、ということは覚えておいて損はないでしょう。  
<br>

CEnumの利点は、どの様な場合においても、enumと同等の速度で安定していることです。  

# 余談

如何だったでしょうか？  

幾つか欠点はありますが、Source GeneratorsでenumからCEnumに自動変換できるなら、割と有用かも知れないなーと思いつつ、勇者が現れることを期待して、今回はこれにて終了と致します。  

最後までご覧いただき、有難う御座いました。  
<br>

### ところで、CEnumのCって何？

光速の定数c  
これ以上は速くならない、という意味で肖らせてもらおうかと。