0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiita100万記事感謝祭!記事投稿キャンペーン開催のお知らせ

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

Last updated at Posted at 2025-01-10

はじめに

enum(列挙型)を使用すれば変数に特定の固定値のみを代入することが可能であり、可読性の向上や予期せぬ値の代入の防止など、様々な場面で役に立ちます。
C#においてもenumは使用可能であり、以下のように実装できます。

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

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

こんな感じに継承したいですが、エラーが出ます
public enum CharacterV2 : Character
{
	Reisen,
 	Aya,
  	Sanae
}

C#のenumはSystem.Enumから継承されており、enumを継承しようとすると多重継承になってしまいます。(C#は多重継承ができない)
そのため、enumが継承できないのも仕方が無いですが、そうは言っても継承したい場面は少なくないことでしょう。
実際に、以下のように先人たちがenumの継承っぽいことをしようと試行錯誤しています。

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

継承可能なenumっぽい物を作る

以下のようにしてenumっぽい物を作りました。

// enumっぽいクラスの列挙子
public record Character
{
	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 class Characters
{
	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");
}

// 継承されたenumっぽいクラス
public class CharactersV2 : Characters
{
	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");
}

classのstatic変数にenumの値を代入する事で、enumっぽい物を実現しています。
Character character = Characters.Sakuya; のように使用できますので、かなりenumっぽい使用感です。
classを使用しているので継承も可能です。
しかし、classを使用する事によって元のenumでは出来ていたことが行えなくなりました。
以下の3点が特に気になります。

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

上記の Character の実装では record を使用する事で1と2の問題を解決しています。
C#9.0からrecordが使用可能になりました。
recordはデータをカプセル化するための組み込み機能であり、不変なデータを保持するには便利な機能です。
また、recordは継承も可能です。

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

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

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

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

enumとenumっぽい物の使用感の比較
using UnityEngine;

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

	public record CharacterRecord
	{
		public readonly int ID;
		public readonly string String;

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

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

	public class Characters
	{
		public static readonly CharacterRecord Reimu = new CharacterRecord (0, "Reimu");
		public static readonly CharacterRecord Marisa = new CharacterRecord (1, "Marisa");
		public static readonly CharacterRecord Sakuya = new CharacterRecord (2, "Sakuya");
		public static readonly CharacterRecord Youmu = new CharacterRecord (3, "Youmu");
	}

	public class CharactersV2 : Characters
	{
		public static readonly CharacterRecord Reisen = new CharacterRecord (4, "Reisen");
		public static readonly CharacterRecord Aya = new CharacterRecord (5, "Aya");
		public static readonly CharacterRecord Sanae = new CharacterRecord (6, "Sanae");
	}

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

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

		Debug.Log (characterEnum == CharacterEnum.Sakuya); // true
		Debug.Log (characterRecord == Characters.Sakuya); // true
		Debug.Log (characterEnum != CharacterEnum.Youmu); // true
		Debug.Log (characterRecord != Characters.Youmu); // true
		Debug.Log (characterEnum > CharacterEnum.Reimu); // true
		Debug.Log (characterRecord > Characters.Reimu); // true
		Debug.Log (characterEnum >= CharacterEnum.Marisa); // true
		Debug.Log (characterRecord >= Characters.Marisa); // true
		Debug.Log (characterEnum < CharacterEnum.Sakuya); // false
		Debug.Log (characterRecord < Characters.Sakuya); // false
		Debug.Log (characterEnum <= CharacterEnum.Youmu); // true
		Debug.Log (characterRecord <= Characters.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 (characterRecord)
		{
		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 (characterRecord == Characters.Reimu)
		{
			Debug.Log ("霊夢");
		}
		else if (characterRecord == Characters.Marisa)
		{
			Debug.Log ("魔理沙");
		}
		else if (characterRecord == Characters.Sakuya)
		{
			Debug.Log ("咲夜"); // このログが出力される
		}
		else if (characterRecord == Characters.Youmu)
		{
			Debug.Log ("妖夢");
		}
		else
		{
			Debug.Log ("");
		}
	}
}

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?