前振り
普段、何気なく書いているコードですが、「環境Aでは高速に動作するのに、環境Bで実行してみたら遅くなった」という経験をされた方いませんか?
或いは、「環境Aで高速に動作するから、環境Bでも大丈夫」と高を括り、知らない内に環境Bでは遅く動いている、なんて事もあるかも知れませんが、心当たりのある方いませんか?
今回は、Enumに纏わるお話です。
たかがEnum、されどEnum、今一度Enumを見直す機会と捉えてご覧頂けると幸いです。
本題
Enumは、環境だけでなく、ユースケースでも実行パフォーマンスにバラつきがある
Enumには、主に三通りの扱い方があります。
- enum
- System.Enum
- where System.Enum
これらは、Enumを使う上で付いて回るものなので、回避のしようがありません。
よく使われるケースは、以下の3つでしょうか。
- 配列の添え字(intへのキャスト)
- switch case
- Flags属性
つまりは、以下の表の空欄毎に実行パフォーマンスが異なり、更に環境毎でも異なるという話です。
パフォーマンス表 | enum | System.Enum | where System.Enum |
---|---|---|---|
配列の添え字(intへのキャスト) | |||
switch case | |||
Flags属性 |
では、この問題をどう解決するか
遅くなりようの無いコードを書く、それだけです。
しかし、ただ利点を享受するだけでは済まないのが、今回紹介するイディオム「CEnum」です。
このイディオムには2つ欠点がありますが、ともあれ話を進めます。
先ずは、基底classです。
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){}
}
CEnumのユーザー定義はこの様になります。
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){}
}
継承めいた事も出来ます。
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属性めいた事も出来ます。
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){}
}
そう、とにかく面倒くさい事この上ないのが、このイディオム1つ目の欠点です。
面倒くさいの一言で片付けましたが、色々と言いたい事はあるでしょう。
まぁ今は、そうした事は全て飲み込んで頂いて、話を進めて行きますね。
CEnumの使い方
CEnumでは、要素の扱い方が二通りあります。
- 確定型を使う場合
- public constで定義した名前を使う
- 利点:定数値を直接扱える
- 欠点:CEnum型情報を失う(whereで制約できない)
- public constで定義した名前を使う
- CEnum型を使う場合
- public static readonlyで定義した名前を使う
- 利点:whereで制約できる
- 欠点:CEnumの運用ルールを覚える必要がある(このイディオム2つ目の欠点)
- public static readonlyで定義した名前を使う
CEnumの運用ルール
確定型を使う場面
//定数値を配列の添え字に使用(Enumとは異なり、intへのキャストは不要)
SomeArray[EUser.Hoge] = SomeValue;
//caseの条件
switch (){
case EUser.Hoge: break;
case EUser.Fuga: break;
}
//Flags
if (SomeFlags & EFlags.IsHoge){}
if (SomeFlags & EFlags.IsFuga){}
CEnum型を使う場面
//変数に取る
var e = EUser.EHoge;
//変数値を配列の添え字に使用(Valueメンバ変数を使う)
SomeArray[e.Value] = SomeValue;
//ジェネリック メソッドの引数に渡す
SomeGenericMethod(EUser.EHoge);
SomeGenericMethod(e);
//switchの因子(Valueメンバ変数を使う)
switch (e.Value){
}
ジェネリック(Generic)の型制約
CEnumは、whereで制約できるので、継承めいた事をする時に役立ちます。
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で扱った場合
サンプルコードを用意したので、気になる方はお試し下さい。
まとめ
c#標準の列挙型を使う場合、enumで使えるケースはさほど多くなく、大抵の場合はwhere System.Enumで使われ、System.Enumは止む無く使われる、という温度感だと思います。
コード中にあるSystem.Enumキーワードは、そこに多大な負荷が掛かるリスクを示している、ということは覚えておいて損はないでしょう。
CEnumの利点は、どの様な場合においても、enumと同等の速度で安定していることです。
余談
如何だったでしょうか?
幾つか欠点はありますが、Source GeneratorsでenumからCEnumに自動変換できるなら、割と有用かも知れないなーと思いつつ、勇者が現れることを期待して、今回はこれにて終了と致します。
最後までご覧いただき、有難う御座いました。
ところで、CEnumのCって何?
光速の定数c
これ以上は速くならない、という意味で肖らせてもらおうかと。