C# Dictionary のキーに列挙型・構造体型を使った場合のボックス化について

2020/9/9

はじめに

「ボックス化したくないなら、Dictionary のキーに列挙型と構造体型を使うな」という話は比較的有名ですが、今回はコードベースで調査してみたいと思います。

調査結果

Dictionary 実装について

Dictionary の多くの操作(値取得、値追加、値削除等)において、該当キーが保有しているデータのキーと一致するかをチェックする処理が不可欠です。 Dictionary のコードを読む限り、色んな箇所に散在はしていますが、一致するかは下記で判断しているようです。

  • キーの GetHashCode メソッド呼び出し結果が一致するかをチェック
  • EqualityComparer<T>.Default プロパティの Equals メソッドでチェック

ちなみに、公式ドキュメントによると、EqualityComparer<T>.Default.Equals メソッドは該当型の IEquatable<T>.Equals メソッドを利用するとのことなので、

後者は、

  • キーの Equals メソッドでチェック

と言い換えて良いかと思います。

なので、列挙型と構造体型の GetHashCode メソッドと、Equals メソッドのボックス化について深掘りしてみます。

関連メソッド呼び出しにおけるボックス化実態

列挙型と構造体型で前述メソッドを呼び出した場合の IL を見てみます。

サンプルコード

public class C {
    struct S
    {
    }

    enum E
    {
        E1,
        E2,
    }

    public void M() {
        var s = new S();
        var sHashCode = s.GetHashCode();
        var sEqualsResult = s.Equals(s);
        var e = E.E1;
        var eHashCode = e.GetHashCode();
        var eEqualsResult = e.Equals(e);
    }
}

構造体型 IL ピックアップ

// IL_0009: ldloca.s 0
// IL_000b: constrained. C/S
// IL_0011: callvirt instance int32 [System.Private.CoreLib]System.Object::GetHashCode()
// IL_0016: stloc.1
var sHashCode = s.GetHashCode();
// IL_0017: ldloca.s 0
// IL_0019: ldloc.0
// IL_001a: box C/S
// IL_001f: constrained. C/S
// IL_0025: callvirt instance bool [System.Private.CoreLib]System.Object::Equals(object)
// IL_002a: stloc.2
var sEqualsResult = s.Equals(s);

列挙型 IL ピックアップ

// IL_002d: ldloca.s 3
// IL_002f: constrained. C/E
// IL_0035: callvirt instance int32 [System.Private.CoreLib]System.Object::GetHashCode()
// IL_003a: stloc.s 4
var eHashCode = e.GetHashCode();
// IL_003c: ldloca.s 3
// IL_003e: ldloc.3
// IL_003f: box C/E
// IL_0044: constrained. C/E
// IL_004a: callvirt instance bool [System.Private.CoreLib]System.Object::Equals(object)
// IL_004f: stloc.s 5
var eEqualsResult = e.Equals(e);

ぱっと見、GetHashCode ではボックス化していないように見えますが、 constrainedObject クラス、ValueType クラス、Enum クラスで定義されているが、該当値型で実装していない場合、ボックス化を伴うとのことなので、残念ながらボックス化しているようです。

Equals の場合は引数を object として取ってしまう分かり易いボックス化に加えて、上記同様、constrained によるボックス化を含みます。

結論

キーに値型を利用すると、キーが一致するかチェックにおいて、ボックス化が(複数回)発生してしまいます。該当処理は、値取得時、追加、削除時にも呼ばれるので、多くのケースでボックス化が頻発することになります。

対応策

列挙型は整数数値型にキャストして利用

整数数値型における該当メソッド呼び出しではボックス化が発生しません。

構造体型に該当メソッドを実装

constrained で Box 化が発生する条件を崩してあげれば良いです。また、Equals の引数がボックス化しないように、該当型をそのまま受け取る Equals メソッドも忘れずに実装する必要があります。

独自 IEqualityComparer 実装を利用

コンストラクタ引数として IEqualityComparer<T>? comparer を渡してあげれば、上記ボックス化を伴う処理ではなく、comparer.GetHashCode(key)comparer.Equals(key1, key2) を使ってくれます。