Skip to content

Instantly share code, notes, and snippets.

@matarillo
Last active August 29, 2015 14:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matarillo/e0782d32bceb19685333 to your computer and use it in GitHub Desktop.
Save matarillo/e0782d32bceb19685333 to your computer and use it in GitHub Desktop.

Javaによる高階型変数の実装 ... のC#版(本文)

順番に進めよう。

たとえば、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な型ができてしまいそうだからである。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment