C#で「継承可能なenum」っぽい物をrecordを使って実装する

## はじめに
enum(列挙型)を使用すれば変数に特定の固定値のみを代入することが可能であり、可読性の向上や予期せぬ値の代入の防止など、様々な場面で役に立ちます。
[C#においてもenumは使用可能](https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/enum)であり、以下のように実装できます。

```cs
public enum Character
{
	Reimu,
	Marisa,
 	Sakuya,
  	Youmu
}
```

しかし、C#のenumには1つ不便な点があります。
それは **継承が不可能** だという点です。

<details><summary>こんな感じに継承したいですが、エラーが出ます</summary>

```cs
public enum CharacterV2 : Character
{
	Reisen,
 	Aya,
  	Sanae
}
```
</details>

C#のenumは[System.Enum](https://learn.microsoft.com/ja-jp/dotnet/api/system.enum?view=net-9.0)から継承されており、enumを継承しようとすると多重継承になってしまいます。(C#は多重継承ができない)
そのため、enumが継承できないのも仕方が無いですが、そうは言っても継承したい場面は少なくないことでしょう。
実際に、以下のように先人たちがenumの継承っぽいことをしようと試行錯誤しています。

- [C#でenumを継承したい問題](https://qiita.com/__poosuke/items/04c191e3339026f4bac0)
- [Enumを継承したい！と思ったときに僕らが求めているもの](https://qiita.com/amenone_games/items/3a4425eb058c89a5d618)

ただ、上記の記事の手法では割と面倒な事をしているのと、コードを書いている時の感触がenumと異なるのが個人的に気になりました。
ですので、この記事ではなるべく単純な実装で、コードの開発時の感触がenumに近いenumっぽい物の実装を目指します。

## 継承可能なenumっぽい物を作る
以下のようにしてenumっぽい物を作りました。

```cs
// enumっぽいレコード
public record Character
{
    // enumっぽいレコードの列挙子
	public static readonly Character Reimu = new Character (0, "Reimu");
	public static readonly Character Marisa = new Character (1, "Marisa");
	public static readonly Character Sakuya = new Character (2, "Sakuya");
	public static readonly Character Youmu = new Character (3, "Youmu");

	public readonly int ID;
	public readonly string String;

	public Character (int id, string str)
	{
		ID = id;
		String = str;
	}

	public static bool operator > (Character a, Character b) => a.ID > b.ID;
	public static bool operator >= (Character a, Character b) => a.ID >= b.ID;
	public static bool operator < (Character a, Character b) => a.ID < b.ID;
	public static bool operator <= (Character a, Character b) => a.ID <= b.ID;
}

// 継承されたenumっぽいレコード
public record CharacterV2 : Character
{
	public static readonly Character Reisen = new Character (4, "Reisen");
	public static readonly Character Aya = new Character (5, "Aya");
	public static readonly Character Sanae = new Character (6, "Sanae");
	public static new readonly Character Youmu = new Character (7, "Youmu"); // 継承元のプロパティの上書きも可能

	public CharacterV2 (int id, string str) : base (id, str)
	{

	}
}
```

C#9.0から[record](https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/record)が使用可能になりました。
recordはデータをカプセル化するための組み込み機能であり、不変なデータを保持するには便利な機能です。
また、recordは継承も可能です。
なのでrecordを使用し、static変数にenumの値を代入する事でenumっぽい物を実現しています。
`Character character = Character.Sakuya;` のように使用できますので、かなりenumっぽい使用感です。
しかし、recordを使用する事によって元のenumでは出来ていたことが行えなくなりました。
以下の3点が特に気になります。

1. `ToString` が使えない
1. 演算子が使えない
1. switch文のcase句で使えない

上記の `Character` の実装では `String` プロパティを定義しています。
これはenumを文字列にした時に返す値であり、 `Character.Marisa.String` のようにToStringに近い感覚で使えます。
また、比較演算子の挙動も定義し、ID(enumの値)を比較するようにしています。
recordはclassと違って `==` と `!=` 演算子の定義がすでにされているので、実装が楽なrecordを使用しています。

ただ、switch文のcase句で `Character` が使えない問題は解決できていません。
Characterの値の違いで分岐をしたい時は以下のようにする必要があります。

- 頑張ってif文を書く
   - 特定のCharacterの時のみ別の値で、他は同じ値の時はこちらが良いでしょう
- `String` プロパティのように、Characterごとに違う値を持つプロパティを定義する
   - キャラの名前など、Characterごとに変えたい場合はこちらが良いでしょう

Unityで動作する、Characterの使用感や挙動を確認可能なコードは以下です。
switch文が使えないこと以外はかなりenumに似ていると言えると思います。

<details><summary>enumとenumっぽい物の使用感の比較</summary>

```cs
using UnityEngine;

public class CharacterRecordTest : MonoBehaviour
{
	public enum CharacterEnum
	{
		Reimu,
		Marisa,
		Sakuya,
		Youmu
	}

	public record Character
	{
		public static readonly Character Reimu = new Character (0, "Reimu");
		public static readonly Character Marisa = new Character (1, "Marisa");
		public static readonly Character Sakuya = new Character (2, "Sakuya");
		public static readonly Character Youmu = new Character (3, "Youmu");

		public readonly int ID;
		public readonly string String;

		public Character (int id, string str)
		{
			ID = id;
			String = str;
		}

		public static bool operator > (Character a, Character b) => a.ID > b.ID;
		public static bool operator >= (Character a, Character b) => a.ID >= b.ID;
		public static bool operator < (Character a, Character b) => a.ID < b.ID;
		public static bool operator <= (Character a, Character b) => a.ID <= b.ID;
	}

	public record CharacterV2 : Character
	{
		public static readonly Character Reisen = new Character (4, "Reisen");
		public static readonly Character Aya = new Character (5, "Aya");
		public static readonly Character Sanae = new Character (6, "Sanae");
		public static new readonly Character Youmu = new Character (7, "Youmu"); // 継承元のプロパティの上書きも可能

		public CharacterV2 (int id, string str) : base (id, str)
		{

		}
	}

	private void Awake ()
	{
		Debug.Log (CharacterEnum.Reimu); // Reimu
		Debug.Log (Character.Reimu); // CharacterRecord { ID = 0, String = Reimu }
		Debug.Log ((int)CharacterEnum.Marisa); // 1
		Debug.Log (Character.Marisa.ID); // 1
		Debug.Log (CharacterV2.Marisa.ID); // 1。継承先でも継承元を参照可能なことを確認
		Debug.Log (CharacterV2.Sanae.ID); // 6。継承できていることを確認
		Debug.Log (CharacterEnum.Reimu.ToString ()); // Reimu
		Debug.Log (Character.Reimu.String); // Reimu
		Debug.Log (CharacterV2.Aya.String); // Aya
		Debug.Log (Character.Youmu.ID); // 3
		Debug.Log (CharacterV2.Youmu.ID); // 7

		// ここから比較演算子の挙動がenumと同じである事の確認
		CharacterEnum characterEnum = CharacterEnum.Sakuya;
		Character character = Character.Sakuya;

		Debug.Log (characterEnum == CharacterEnum.Sakuya); // true
		Debug.Log (character == Character.Sakuya); // true
		Debug.Log (characterEnum != CharacterEnum.Youmu); // true
		Debug.Log (character != Character.Youmu); // true
		Debug.Log (characterEnum > CharacterEnum.Reimu); // true
		Debug.Log (character > Character.Reimu); // true
		Debug.Log (characterEnum >= CharacterEnum.Marisa); // true
		Debug.Log (character >= Character.Marisa); // true
		Debug.Log (characterEnum < CharacterEnum.Sakuya); // false
		Debug.Log (character < Character.Sakuya); // false
		Debug.Log (characterEnum <= CharacterEnum.Youmu); // true
		Debug.Log (character <= Character.Youmu); // true

		switch (characterEnum)
		{
		case CharacterEnum.Reimu:
			Debug.Log ("霊夢");
			break;
		case CharacterEnum.Marisa:
			Debug.Log ("魔理沙");
			break;
		case CharacterEnum.Sakuya:
			Debug.Log ("咲夜"); // このログが出力される
			break;
		case CharacterEnum.Youmu:
			Debug.Log ("妖夢");
			break;
		default:
			Debug.Log ("");
			break;
		}

		/*
		// コンパイルエラーが出る
		switch (character)
		{
		case Characters.Reimu:
			Debug.Log ("霊夢");
			break;
		case Characters.Marisa:
			Debug.Log ("魔理沙");
			break;
		case Characters.Sakuya:
			Debug.Log ("咲夜");
			break;
		case Characters.Youmu:
			Debug.Log ("妖夢");
			break;
		default:
			Debug.Log ("");
			break;
		}
		*/

		if (character == Character.Reimu)
		{
			Debug.Log ("霊夢");
		}
		else if (character == Character.Marisa)
		{
			Debug.Log ("魔理沙");
		}
		else if (character == Character.Sakuya)
		{
			Debug.Log ("咲夜"); // このログが出力される
		}
		else if (character == Character.Youmu)
		{
			Debug.Log ("妖夢");
		}
		else
		{
			Debug.Log ("");
		}
	}
}

```
</details>

以下のGitHubに上記のコードとUnityで動作確認用のシーンがあります。
Unityで `CharacterRecordTest.unitypackage` をインポートし、 `CharacterRecordTest` シーンを開いてエディターでログを確認してください。

https://github.com/TD12734/samples/tree/main/CharacterRecordTest