タイプセーフとは
タイプセーフ(型安全)とは、型の違いによってエラーが起こることを防ぐ仕組みのことを指す。
通常のEnumの問題点
例えば、下記のようなEnumを定義したとする。
enum Position
{
Top = 0,
Right = 1,
Bottom = 2,
Left = 3
}
Enumで定義された列挙値の一つ一つは結局のところ数値型だ。
よって、数値型とEnum型は双方向のキャストが可能である。
しかし、Enumで定義されていない数値をEnumにキャストしても例外が発生することなく代入できてしまう。
Position position = (Position)4; //実行時エラーにならない
なので Enum型は、Enumに定義した値だけが入っているとは限らない。
例えばEnumをメソッドの引数で使用する場合に下記のような暗黙的な制約ができてしまう。
メソッドの作成者は、「Enumの列挙値以外のものが渡されることを想定して処理を書かなければならない。」
メソッドの使用者は、「Enumの列挙値以外の値を引数として渡してはいけない。もしくは、渡しても問題がないかメソッドの中を知る必要がある。」
そもそも開発者がこのルールを知っておかなければいけないというのは、オブジェクト指向のカプセル化の概念にも反する。
これが、引数として型の安全性が保たれていない、いわゆるタイプセーフでないということだ。
意図しない値の代入が可能であることが潜在的なバグの原因になってしまう。
タイプセーフEnumを実装する
では、例で使用したPositionをタイプセーフEnumで実装してみよう。
実装例
struct Position
{
public static readonly Position Top = new Position(0);
public static readonly Position Right = new Position(1);
public static readonly Position Bottom = new Position(2);
public static readonly Position Left = new Position(3);
private Position(int value)
{
this.Value = value;
}
public int Value { get; }
}
わかりやすさを重視するために、一番シンプルな方法を選んでいる。
Positionとintのキャストが必要な場合は、後述の変換演算子(implicitとexplicit)を実装する必要がある。
使用例
private static void Hogehoge(Position position)
{
if (position == Position.Top)
{
Console.WriteLine("上");
}
else if (position == Position.Right)
{
Console.WriteLine("右");
}
else if (position == Position.Bottom)
{
Console.WriteLine("下");
}
else if (position == Position.Left)
{
Console.WriteLine("左");
}
}
このように通常の Enumと同様に比較演算子を使用することができる。
では、タイプセーフ Enumのコードをひとつずつ解説していこう。
コンストラクタをprivateにする
private Position(int value)
{
this.Value = value;
}
外部でインスタンス化できないよう制御している。 もしこれがpublicのままであれば、下記のように外部で色々なPositionが作成できてしまう。
//悪い例
Position topLeft = new Position("左上");
hogehoge(topLeft); // 実行時エラーにならない
このように外部で要素が増やせてしまうと、通常のEnumと同じでタイプセーフでなくなってしまう。
一意の値をコンストラクタの引数で渡す
public static readonly Position Top = new Position(0);
private Position(int value)
{
this.Value = value;
}
public int Value { get; }
タイプセーフEnumでは、通常のEnumのように数値が必ずしも必要ではない。 intをstringにして文字列の値を持つことも可能だ。 また、引数は複数でも構わない。
//引数を複数にする場合の例
public static readonly Position Top => new Position(0, "上");
Private Position(int value, string name)
{
this.Value = value;
this.Name = name;
}
public int Value { get; }
public string Name { get; }
本編とは少し話がそれるが、上記のようにNameプロパティを実装した場合、前述のHogehogeメソッドは下記のソースコードに交換可能だ。
private static void hogehoge(Position position)
{
Console.WriteLine(position.Name);
}
このように、各列挙値がクラスとして複数の要素と処理を持つことが可能になったので、分岐を極力減らすことも可能である。
staticな変数でEnumの各列挙値を定義する
public static readonly Position Top = new Position(0);
public static readonly Position Right = new Position(1);
public static readonly Position Bottom = new Position(2);
public static readonly Position Left = new Position(3);
Enum同様、列挙数分だけ定義する。
キャストが必要な場合
intと Enumの双方向のキャストが必要な場合、下記の変換演算子を実装する。
//Positionからintへの変換
public static implicit operator int(Position position)
{
return position.Value;
}
//intからPositionへの変換
public static explicit operator Position(int value)
{
foreach (FieldInfo fi in typeof(Position).GetFields())
{
if (!fi.IsStatic) continue;
var p = (Position)fi.GetValue(null);
if (p.Value == value) return p;
}
throw new ArgumentException();
}
タイプセーフEnumのデメリット
- タイプセーフEnumのインスタンスは定数ではないためswitch文では使えない。
- ビットフィールドとしてEnumを使いたい場合はタイプセーフEnumにはできない(たぶん)。
- 通常のEnumに比べて実装がめんどう。
まとめ
なんでもかんでもタイプセーフEnumにすることはなくケースバイケース。
だが、タイプセーフEnumで実装しておいたほうが、拡張性や汎用性が高く、よりオブジェクト指向的なプログラミングができる。