Подтвердить что ты не робот

Как создать новый экземпляр ImmutableDictionary?

Я хотел бы написать что-то вроде этого:

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

(используя ImmutableDictionary из System.Collections.Immutable). Это похоже на простое использование, поскольку я объявляю все значения заранее - никакой мутации нет. Но это дает мне ошибку:

Тип 'System.Collections.Immutable.ImmutableDictionary<TKey,TValue>' не имеет конструкторов, определенных

Как я должен создать новый неизменяемый словарь со статическим контентом?

4b9b3361

Ответ 1

Вы не можете создать неизменяемую коллекцию с инициализатором коллекции, потому что компилятор переводит их в последовательность вызовов метода Add. Например, если вы посмотрите на код IL для var d = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } };, вы получите

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)

Очевидно, это нарушает понятие неизменных коллекций.

Как ваш собственный ответ, так и Джон Скит - это способы справиться с этим.

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

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

Ответ 2

Сначала создайте "нормальный" словарь и вызовите ToImmutableDictionary (в соответствии с вашим собственным ответом) или используйте ImmutableDictionary<,>.Builder:

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

Стыдно, что строитель не имеет открытого конструктора, насколько я могу судить, поскольку он мешает вам использовать синтаксис инициализатора коллекции, если я ничего не пропустил... факт, что Add метод возвращает void означает, что вы не можете даже навязывать ему вызовы, делая его более раздражающим - насколько я вижу, вы в принципе не можете использовать построитель для создания неизменяемого словаря в одном выражении, что очень расстраивает: (

Ответ 3

Вы можете использовать вспомогательный помощник:

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();
    }
}

(Я использую новые члены С# 6 с выражением выражения, поэтому, если вы хотите, чтобы это скомпилировалось в более старых версиях, вам нужно было бы расширить их до полных членов.)

Используя этот тип, вы можете использовать синтаксис инициализатора коллекции так:

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

или если вы используете С# 6, вы можете использовать синтаксис инициализатора объекта с его новой поддержкой индексаторов (именно поэтому я включил в свой тип индексатор только для записи):

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

Это сочетает преимущества обоих предлагаемых преимуществ:

  • Предотвращает создание полного Dictionary<TKey, TValue>
  • Позволяет использовать инициализаторы

Проблема с построением полного Dictionary<TKey, TValue> заключается в том, что в его создании есть куча накладных расходов; это неоправданно дорогостоящий способ передачи того, что в основном представляет собой список пар ключ/значение, поскольку он будет тщательно настраивать структуру хеш-таблицы, чтобы обеспечить эффективный поиск, который вы никогда не будете использовать. (Объект, над которым вы будете выполнять поиск, - это неизменный словарь, который вы в конечном итоге оказываете, а не изменяемый словарь, который вы используете во время инициализации.)

ToImmutableDictionary просто собирается перебирать содержимое словаря (процесс менее эффективен, так как Dictionary<TKey, TValue> работает внутренне - для этого требуется больше работы, чем с простым списком), получая абсолютно никакая польза от работы, которая входила в создание словаря, а затем должна выполнить ту же работу, которую она сделала бы, если бы вы использовали конструктор напрямую.

Код Jon избегает этого, используя только построитель, который должен быть более эффективным. Но его подход не позволяет использовать инициализаторы.

Я разделяю разочарование Джона, что неизменные коллекции не дают возможности сделать это из коробки.

Отредактировано 2017/08/10. Мне пришлось сменить конструктор с нулевым аргументом на тот, который принимает аргумент, который он игнорирует, и передавать фиктивное значение везде, где вы его используете. @gareth-latty указал в комментарии, что struct не может иметь конструктор с нулевым аргументом. Когда я изначально написал этот пример, это было неверно: некоторое время предварительные просмотры С# 6 позволили вам предоставить такой конструктор. Эта функция была удалена до отправки С# 6 (после того, как я написал исходный ответ, очевидно), предположительно потому, что это было запутанно - были сценарии, в которых конструктор не запускался. В этом конкретном случае было безопасно использовать его, но, к сожалению, язык больше не существует. Предложение Gareth состояло в том, чтобы изменить его на класс, но тогда любой код, использующий это, должен был бы выделить объект, вызывая ненужное давление GC - вся причина, по которой я использовал struct, заключалась в том, чтобы позволить использовать этот синтаксис без дополнительной среды выполнения накладные расходы.

Я попытался изменить это для выполнения отложенной инициализации _builder, но оказалось, что генератор кода JIT недостаточно умен, чтобы оптимизировать их, поэтому даже в сборке релизов он проверяет _builder для каждого добавляемого вами элемента. (И он строит проверку и соответствующий вызов CreateBuilder, который, как оказалось, создает довольно много кода с большим количеством условных ветвлений). Лучше всего иметь одноразовую инициализацию, и это должно произойти в конструкторе, если вы хотите использовать этот синтаксис инициализатора. Таким образом, единственный способ использовать этот синтаксис без дополнительных затрат - это иметь конструкцию, которая инициализирует _builder в своем конструкторе, что означает, что теперь нам нужен этот уродливый фиктивный аргумент.

Ответ 4

Пока мне больше всего нравится:

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

Ответ 5

Или это

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

Существует также метод AddRange.