この記事は StudioZ Tech Advent Calendar 2019 の11日目の記事です。
はじめに
5年ぐらい前からUnityを使うようになり、JavaからC#に転向しました。
似たところの多い言語なのでほとんど違和感なく使うことができたのですが、唯一困ったのがenumの仕様。
そこでJavaのenumをC#で再現できないか考えてみました。
Javaのenumの特性と実装方法
- 列挙子は列挙型のインスタンスを持つ
- 普通にクラスを使えば満たせます
- 外部からインスタンスの生成ができない
- コンストラクタをprivateにすることで対応可能です
- 列挙型.列挙子でアクセスできる
- 列挙子のインスタンスをstaticフィールドで管理すればアクセス可能になります
- 同名の列挙子は定義できない
- 同名のフィールドは定義できないので、staticフィールドで管理する時点で満たされます
ここまでの条件をコードにするとこんな感じになります
public class EnumSample {
public static EnumSample Enum1 = new EnumSample();
public static EnumSample Enum2 = new EnumSample();
public static EnumSample Enum3 = new EnumSample();
public static EnumSample Enum4 = new EnumSample();
// 外部からのインスタンス生成不可
private EnumSample() {}
}
- 列挙子は固有の値を持つことができる
- コンストラクタで値を受け取ってフィールドに持つようにします
- ただし静的に重複のチェックをすることはできません(実行時のチェックは可能)
public class EnumSample2 {
public static EnumSample2 Enum1 = new EnumSample2( 1 );
public static EnumSample2 Enum2 = new EnumSample2( 2 );
public static EnumSample2 Enum3 = new EnumSample2( 3 );
public static EnumSample2 Enum4 = new EnumSample2( 4 );
public int Num;
// 外部からのインスタンス生成不可
private EnumSample2( int num ) {
Num = num;
// 重複チェックを入れるとしたらこの辺でやる
// Dictionaryに詰めていってContainsKeyでチェックするのが楽そう
}
}
- インスタンスごとにメソッドを実装できる
- 列挙体クラスを抽象クラスにし、列挙子ごとにクラスを継承してメソッドを実装します
- 列挙子ごとに実装必須にしたい場合はabstract、デフォルト動作を定義したい場合はvirtualにします
- 列挙子間で共通化したい処理は親クラス側、差別化したい処理は子クラス側に実装することで重複処理をなくすことができます
- 列挙体はインタフェースを実装できる
- 列挙型にインタフェースを実装する宣言をするだけです
// ドロップアイテムのインタフェース
public interface IDropItem {
string Name { get; }
int Price { get; }
string Description { get; }
}
// ドロップアイテムとして扱える武器の列挙型
public abstract class Weapon : IDropItem {
public static Weapon Sword = new WeaponSword();
private class WeaponSword : Weapon {
// コンストラクタがpublicでもクラス自体はprivateなので外部からはインスタンス生成不可
public WeaponSword() {}
public override string Name { get { return "剣"; } }
public override int Price { get { return 100; } }
public override string Description { get { return "いい感じの説明"; } }
public override void Attack() {
base.Slash();
}
}
public static Weapon Spear = new WeaponSpear();
private class WeaponSpear : Weapon {
public WeaponSpear() {}
public override string Name { get { return "槍"; } }
public override int Price { get { return 50; } }
public override string Description { get { return "そんな感じの説明"; } }
public override void Attack() {
base.Pierce();
}
}
public static Weapon Club = new WeaponClub();
private class WeaponClub : Weapon {
public WeaponClub() {}
public override string Name { get { return "棍棒"; } }
public override int Price { get { return 10; } }
public override string Description { get { return "とてもアレな説明"; } }
// Attack()の実装は省略可能
}
protected Weapon() {}
// 名前、価格、説明は必ず列挙子で定義する
public abstract string Name { get; }
public abstract int Price { get; }
public abstract string Description { get; }
// 攻撃処理の実装は省略可能
public virtual void Attack() {
// オーバーライドしない場合はデフォルトとして殴る処理
Strike();
}
// protectedで宣言したプロパティやメソッドは列挙子側からアクセスできる
protected void Slash() {
// 切る武器の処理
}
protected void Pierce() {
// 刺す武器の処理
}
protected void Strike() {
// 殴る武器の処理
}
}
C#ではできないこと
- 列挙子をswitchの条件に使う
- caseには定数しか使えないので、実態がクラスである列挙子を使うことはできません
- とはいえ列挙子側に処理を実装できるので、switchは基本的に使う必要はないと思います
おわりに
ぱっと見面倒そうに見えますが、Java式enumを使うことによって、ポリモーフィズムを実現しやすくなります
機会があったらぜひ試してみてください