web-dev-qa-db-ja.com

ImmutableDictionaryの新しいインスタンスを作成するにはどうすればよいですか?

私はこのような何かを書きたいです:

var d = new ImmutableDictionary<string, int> { { "a", 1 }, { "b", 2 } };

System.Collections.ImmutableImmutableDictionaryを使用)。私はすべての値を前もって宣言しているので、簡単な使用法のように見えます-そこに突然変異はありません。しかし、これは私にエラーを与えます:

タイプ 'System.Collections.Immutable.ImmutableDictionary<TKey,TValue> 'にはコンストラクタが定義されていません

静的コンテンツを含む新しい不変辞書を作成するにはどうすればよいですか?

37

コレクション初期化子を使用して不変コレクションを作成することはできません。コンパイラがそれらをAddメソッドの呼び出しのシーケンスに変換するためです。たとえば、var d = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } };のILコードを見ると、

IL_0000: newobj instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::.ctor()
IL_0005: dup
IL_0006: ldstr "a"
IL_000b: ldc.i4.1
IL_000c: callvirt instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::Add(!0, !1)
IL_0011: dup
IL_0012: ldstr "b"
IL_0017: ldc.i4.2
IL_0018: callvirt instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::Add(!0, !1)

これは明らかに、不変コレクションの概念に違反しています。

あなた自身の答えとJon Skeetの両方がこれに対処する方法です。

// lukasLansky's solution
var d = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } }.ToImmutableDictionary();

// Jon Skeet's solution
var builder = ImmutableDictionary.CreateBuilder<string, int>();
builder.Add("a", 1);
builder.Add("b", 2);   
var result = builder.ToImmutable();
39
Dirk

最初に「通常の」辞書を作成してToImmutableDictionaryを呼び出す(独自の回答に従って)か、ImmutableDictionary<,>.Builderを使用します。

var builder = ImmutableDictionary.CreateBuilder<string, int>();
builder.Add("a", 1);
builder.Add("b", 2);
var result = builder.ToImmutable();

私が知る限り、ビルダーがパブリックコンストラクターを持っていないのは残念です。何かを逃していない限り、コレクション初期化構文を使用できないので... Addメソッドはvoidを返します。これは呼び出しをチェーンすることさえできず、迷惑になります-私が見る限り、基本的にビルダーを使用して単一の式で不変辞書を作成することはできません。非常にイライラする:(

29
Jon Skeet

次のようなヘルパーを使用できます。

public struct MyDictionaryBuilder<TKey, TValue> : IEnumerable
{
    private ImmutableDictionary<TKey, TValue>.Builder _builder;

    public MyDictionaryBuilder(int dummy)
    {
        _builder = ImmutableDictionary.CreateBuilder<TKey, TValue>();
    }

    public void Add(TKey key, TValue value) => _builder.Add(key, value);

    public TValue this[TKey key]
    {
        set { _builder[key] = value; }
    }

    public ImmutableDictionary<TKey, TValue> ToImmutable() => _builder.ToImmutable();

    public IEnumerator GetEnumerator()
    {
        // Only implementing IEnumerable because collection initializer
        // syntax is unavailable if you don't.
        throw new NotImplementedException();
    }
}

(私は新しいC#6式ボディメンバを使用しているため、これを古いバージョンでコンパイルするには、それらを完全なメンバに展開する必要があります。)

そのタイプを設定すると、次のようなコレクション初期化構文を使用できます。

var d = new MyDictionaryBuilder<int, string>(0)
{
    { 1, "One" },
    { 2, "Two" },
    { 3, "Three" }
}.ToImmutable();

または、C#6を使用している場合、インデクサーの新しいサポートを備えたオブジェクト初期化構文を使用できます(これが、タイプに書き込み専用インデクサーを含めた理由です)。

var d2 = new MyDictionaryBuilder<int, string>(0)
{
    [1] = "One",
    [2] = "Two",
    [3] = "Three"
}.ToImmutable();

これにより、提案されている両方の利点の利点が組み合わされます。

  • 完全なDictionary<TKey, TValue>の構築を避けます
  • 初期化子を使用できます

完全なDictionary<TKey, TValue>を構築する際の問題は、それを構築する際に大量のオーバーヘッドが伴うことです。基本的にキー/値ペアのリストを渡すのに不必要に高価な方法です。実際には使用しない効率的なルックアップを可能にするために、ハッシュテーブル構造を慎重にセットアップするからです。 (ルックアップを実行するオブジェクトは、最終的には最終的に不変のディクショナリであり、初期化中に使用している可変ディクショナリではありません。)

ToImmutableDictionaryはディクショナリのコンテンツを繰り返し処理するだけです(プロセスはlessDictionary<TKey, TValue>が内部的に機能する方法で効率的です-これよりも多くの作業が必要です)辞書を作成する作業からまったく利益を得ることはなく、ビルダーを直接使用した場合と同じ作業を行う必要があります。

Jonのコードはこれを回避し、ビルダーのみを使用するため、より効率的です。しかし、彼のアプローチでは初期化子を使用できません。

不変のコレクションはこれをすぐに実行する方法を提供しないというJonのフラストレーションを共有します。

編集済み2017/08/1:ゼロ引数コンストラクターを無視する引数を取るコンストラクターに変更し、これを使用するすべての場所にダミー値を渡す必要がありました。 @ gareth-lattyは、structが引数ゼロのコンストラクタを持つことはできないというコメントで指摘しました。私が最初にこの例を書いたとき、それは真実ではありませんでした。しばらくの間、C#6のプレビューでそのようなコンストラクターを提供することができました。この機能は、C#6が出荷される前(明らかに元の回答を書いた後)に削除されました。おそらく混乱するためです-コンストラクターが実行されないシナリオがありました。この特定のケースでは安全に使用できましたが、残念ながら言語機能は存在しません。 Garethの提案はそれをクラスに変更することでしたが、これを使用するコードはオブジェクトを割り当てる必要があり、不必要なGCプレッシャーを引き起こします-structを使用した理由はこの構文を使用できるようにすることでした追加のランタイムオーバーヘッドはありません。

これを修正して_builderの遅延初期化を実行しようとしましたが、JITコードジェネレーターはこれらを最適化するほどスマートではないため、リリースビルドでも、追加する各アイテムの_builder 。 (そして、チェックとそれに対応するCreateBuilderの呼び出しをインライン化し、多くの条件分岐を伴う非常に多くのコードを生成します)。一度だけの初期化を行うのが本当に最善であり、この初期化構文を使用できるようにするには、コンストラクターでこれを行う必要があります。したがって、追加のコストなしでこの構文を使用する唯一の方法は、コンストラクタで_builderを初期化する構造体を持つことです。つまり、このnowいダミー引数が必要になります。

11
Ian Griffiths

これまでのところ、私はこれが最も好きです:

var d = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } }.ToImmutableDictionary();
9

またはこれ

ImmutableDictionary<string, int>.Empty
   .Add("a", 1)
   .Add("b", 2);

AddRangeメソッドも利用できます。

7
Miha Markic