表題の通り、Heterogeneous DictionaryをC#で実装してみた。
コード全体を参照したい場合は下記のリポジトリへどうぞ。
Heterogeneous Dictionaryとは
上記の記事で yyu さんが丁寧に解説されている。
必読。
参考になるかもしれない他言語実装
上で紹介した記事はSwift実装だが、ここでは他にScalaのライブラリを紹介しておく。
アクセス可能なKeyValuePairの定義
Swift実装ではassociatedtype、Scalaではhigher kinded polymorphismが用いられている。
これはなぜかというと、HDictionaryを生成する際にKeyとValueの型を固定したくないからだ。
しかし、C#に似たような機能は存在しない。
KeyやValueの型に依存しないinterfaceと、型パラメータを保持するinterfaceを定義することで代用する。
メソッドやプロパティは特に必要ない。
public interface Relation { }
public interface Relation<K, V> : Relation { }
HDictionaryの定義
Immutableにするためやや冗長な実装だが、基本的にやるべきことは他の言語と大差ない。
型パラメータT
ではKeyやValueを認知しないinterfaceを用いるのが肝である。
また、安全でないメソッドは他のライブラリから呼び出せないようにしておく。
public class HDict<T> where T : Relation
{
private Dictionary<object, object> underlying;
public HDict(Dictionary<object, object> underlying)
{
this.underlying = underlying;
}
public HDict() : this(new Dictionary<object, object>()) { }
internal bool TryGetValue<K, V>(K key, out V value)
{
object v;
if (underlying.TryGetValue(key, out v))
{
value = (V)v;
return true;
}
else
{
value = default(V);
return false;
}
}
internal HDict<T> Add<K, V>(K key, V value)
{
var dict = new Dictionary<object, object>(underlying);
if (dict.ContainsKey(key))
{
dict.Remove(key);
}
dict.Add(key, value);
return new HDict<T>(dict);
}
}
気が向いたら内部で使用するdictionaryをImmutableDictionaryに差し替えるかもしれない。
安全なアクセス
ではどうやって型安全にアクセスするか……もちろん、拡張メソッドだ。
public static class HDictExtensions
{
public static bool TryGetValue<T, K, V>(this HDict<T> dict, K key, out V value) where T : Relation<K, V>
{
return dict.TryGetValue(key, out value);
}
public static HDict<T> Add<T, K, V>(this HDict<T> dict, K key, V value) where T : Relation<K, V>
{
return dict.Add(key, value);
}
}
Relation<K, V>
で型を制限する。
追加、取得できるのはここで定義した拡張メソッドのみなので、型安全であろう。
使い方
まずクラスを定義する。
class RelationIS : Relation<int, string>, Relation<string, int> { }
あとは普通に追加、取得するだけだ。
var hd =
new HDict<RelationIS>()
.Add(1, "foo")
.Add("bar", 1);
// Relation<int, int>はないのでコンパイルエラー
//.Add(1, 1);
string foo;
hd.TryGetValue(1, out foo); // true
int bar;
hd.TryGetValue("bar", out bar)) // false
int buz;
hd.TryGetValue("buz", out buz) // false
// Relation<string, string>はないのでコンパイルエラー
//string hoge;
//hd.TryGetValue("hoge", out hoge);
コメントアウトを外してコンパイルするとコンパイルエラーになることがわかる。
問題点
クラスを定義してインターフェースを実装する方式なので、該当のクラスが外部ライブラリにある場合は継承するか自前で再定義するしかなくなる。
基底クラスの場合は後者しか選択肢がなくなるため、クラスが増えがちになる。
[2016/11/04追記]
Twitterで別の問題に言及している方をみかけたので追記しておく。
class AnotherRelation : Relation<int, string>, Relation<int, int> { }
var hd =
new HDict<AnotherRelation>()
.Add(1, "foo")
.Add(1, 1);
// oops!
string foo;
hd.TryGetValue(1, out foo);
異なるValue型を持つRelationを持つ場合、このコードはキャストに失敗し例外を投げる。
まとめ
C#でも問題なく型安全なDictionaryが作成できた。