順番に進めよう。
たとえば、
var dict = DictionaryGroup<FooKeys>
みたいな型のオブジェクトがあって、dict.Add<int>(FooKeys.IntKeys.Key1, 123);
とか、string value = dict.Get<string>(FooKeys.StringKeys.Key2);
とかできないか。
まずは、FooKeys.StringKeys
型がstring
と対応付いて、FooKeys.IntKeys型
がint
と対応付くようなジェネリックメソッドを考える。
どうやら、FooKeys.StringKeys
型やFooKeys.IntKeys型
がenum
だと不可能のようだ。
Javaのタイプセーフenumみたいにクラスを使えばジェネリックにできるはずだ(構造体もジェネリックにできるが、辞書のキーとして使うときには等値性のことを考えるのが面倒)。
ついでに、型名をFooKeys
からFooKeyGroup
に変えておく。
public sealed class FooKeyGroup
{
public sealed class Key<TValue> { }
public static readonly Key<string> StringKey1 = new Key<string>();
public static readonly Key<string> StringKey2 = new Key<string>();
public static readonly Key<int> IntKey1 = new Key<int>();
public static readonly Key<int> IntKey2 = new Key<int>();
}
public sealed class FooKeyGroupDictionary
{
private Dictionary<object, object> table = new Dictionary<object, object>();
public void Add<TValue>(FooKeyGroup.Key<TValue> key, TValue value)
{
table[key] = value;
}
public TValue Get<TValue>(FooKeyGroup.Key<TValue> key)
{
return (TValue)table[key];
}
}
こんな感じで、FooKeyGroup.Key<TValue>
クラスを使うことが考えられる(等値性は参照が一致するかどうかで見る)。
でもこうすると、辞書側がFooKeyGroup
専用になってしまい、うれしくない。
だからといってKey<TValue>
クラスをFooKeyGroup
の外で定義すると、「FooKeyGroup
では動作するがBarKeyGroup
ではコンパイルエラーになる」を実現できない。
なお、NagiseさんがJavaで試していたのと同じように、継承するのではうまくいかない。
public abstract class KeyGroup
{
public sealed class Key<TValue> { }
}
public sealed class FooKeyGroup : KeyGroup
{
public static readonly FooKeyGroup.Key<string> StringKey1 = new FooKeyGroup.Key<string>();
public static readonly FooKeyGroup.Key<string> StringKey2 = new FooKeyGroup.Key<string>();
public static readonly FooKeyGroup.Key<int> IntKey1 = new FooKeyGroup.Key<int>();
public static readonly FooKeyGroup.Key<int> IntKey2 = new FooKeyGroup.Key<int>();
}
なんだかコンパイルも通るしうまくいきそうなのだが、FooKeyGroup.Key<TValue>
は実はKeyGroup.Key<TValue>
である。
これでは別途BarKeyGroup
クラスを作ったとしても、キーの型が同じになってしまうので、
やっぱり「FooKeyGroup
では動作するがBarKeyGroup
ではコンパイルエラーになる」を実現できない。
で、ここではJavaと似ているが少し違う解決策がとれる。
public abstract class KeyGroup<TKey>
{
public sealed class Key<TValue> { }
}
はい、単純。
辞書の方はこれでいい(はいはい、tableはキャストするから美しくないですよ)。
public sealed class KeyGroupDictionary<TKey>
{
private Dictionary<object, object> table = new Dictionary<object, object>();
public void Add<TValue>(Keys<TKey>.Key<TValue> key, TValue value)
{
table[key] = value;
}
public TValue Get<TValue>(Keys<TKey>.Key<TValue> key)
{
return (TValue)table[key];
}
}
再帰ジェネリクスはいらない……といいたいところだが、こんな感じにする必要はある。
public sealed class FooKeyGroup : KeyGroup<FooKeyGroup>
{
public static readonly Key<string> StringKey1 = new Key<string>();
public static readonly Key<string> StringKey2 = new Key<string>();
public static readonly Key<int> IntKey1 = new Key<int>();
public static readonly Key<int> IntKey2 = new Key<int>();
}
public sealed class BarKeyGroup : KeyGroup<BarKeyGroup>
{
public static readonly Key<string> StringKey1 = new Key<string>();
public static readonly Key<string> StringKey2 = new Key<string>();
public static readonly Key<int> IntKey1 = new Key<int>();
public static readonly Key<int> IntKey2 = new Key<int>();
}
これは単に、KeyGroup<TValue1>
とKeyGroup<TValue2>
が別の型になるため互換性がない、ということを利用したいだけなので、必ずしも自分自身を型引数にする必要はない。
だからといって使いもしない型引数のためだけにクラスを定義するのもアレだから、自分自身を放り込んでいるだけのこと。
(こういう型の使い方は、Haskellで言うところのPhantom Type(幽霊型)と言っていいものだろうか、不勉強なのでよくわからない)
使うときはこうだ。
class Program
{
static void Main(string[] args)
{
var fooTable = new KeyGroupDictionary<FooKeyGroup>();
fooTable.Add(FooKeyGroup.IntKey1, 123);
Console.WriteLine(fooTable.Get(FooKeyGroup.IntKey1));
}
}
Javaほど入り組んではいないと思うが、どうだろうか。
(追記)ところどころsealed
(Javaでいうfinal
)を入れているのは、継承させるとEvilな型ができてしまいそうだからである。