web-dev-qa-db-fra.com

Comment puis-je créer une nouvelle instance d'ImmutableDictionary?

Je voudrais écrire quelque chose comme ceci:

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

(en utilisant ImmutableDictionary de System.Collections.Immutable ). Cela semble être une utilisation simple car je déclare toutes les valeurs d'avance - aucune mutation là-bas. Mais cela me donne une erreur:

Le type 'System.Collections.Immutable.ImmutableDictionary<TKey,TValue> 'n'a pas de constructeurs définis

Comment suis-je censé créer un nouveau dictionnaire immuable avec un contenu statique?

46
Lukáš Lánský

Vous ne pouvez pas créer de collection immuable avec un initialiseur de collection car le compilateur les traduit en une séquence d'appels à la méthode Add. Par exemple, si vous regardez le code IL pour var d = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } }; tu auras

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)

De toute évidence, cela viole le concept de collections immuables.

Votre propre réponse et celle de Jon Skeet sont des moyens de faire face à cela.

// 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();
47
Dirk

Créez d'abord un dictionnaire "normal" et appelez ToImmutableDictionary (selon votre propre réponse), ou utilisez ImmutableDictionary<,>.Builder:

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

C'est dommage que le constructeur n'ait pas de constructeur public pour autant que je sache, car cela vous empêche d'utiliser la syntaxe d'initialisation de la collection, sauf si j'ai oublié quelque chose ... le fait que le Add La méthode renvoie void signifie que vous ne pouvez même pas enchaîner les appels, ce qui le rend plus ennuyeux - autant que je sache, vous ne pouvez pas utiliser un générateur pour créer un dictionnaire immuable dans une seule expression, ce qui est très frustrant :(

35
Jon Skeet

Jusqu'à présent, j'aime le plus:

var d = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } }.ToImmutableDictionary();
13
Lukáš Lánský

Vous pouvez utiliser un assistant comme celui-ci:

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

(J'utilise les nouveaux membres dotés d'une expression C # 6, donc si vous voulez que cela se compile sur les anciennes versions, vous devez les étendre en membres à part entière.)

Avec ce type en place, vous pouvez utiliser la syntaxe d'initialisation de collection comme ceci:

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

ou si vous utilisez C # 6, vous pouvez utiliser la syntaxe d'initialisation d'objet, avec son nouveau support pour les indexeurs (c'est pourquoi j'ai inclus un indexeur en écriture seule dans mon type):

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

Cela combine les avantages des deux avantages proposés:

  • Évite de créer un Dictionary<TKey, TValue>
  • Vous permet d'utiliser des initialiseurs

Le problème avec la construction d'un Dictionary<TKey, TValue> est qu'il y a un tas de frais généraux impliqués dans la construction de cela; c'est une façon inutilement coûteuse de transmettre ce qui est essentiellement une liste de paires clé/valeur, car elle configurera soigneusement une structure de table de hachage pour permettre des recherches efficaces que vous n'utiliserez jamais réellement. (L'objet sur lequel vous effectuerez des recherches est le dictionnaire immuable avec lequel vous vous retrouvez finalement, pas le dictionnaire mutable que vous utilisez lors de l'initialisation.)

ToImmutableDictionary va simplement parcourir le contenu du dictionnaire (un processus rendu moins efficace d'ailleurs Dictionary<TKey, TValue> fonctionne en interne - il faut plus de travail pour ce faire qu'avec une simple liste), ne tirant aucun avantage du travail qui a été nécessaire pour créer le dictionnaire, puis il doit faire le même travail qu'il aurait fait si vous 'avais utilisé le constructeur directement.

Le code de Jon évite cela, en utilisant uniquement le générateur, qui devrait être plus efficace. Mais son approche ne vous permet pas d'utiliser des initialiseurs.

Je partage la frustration de Jon que les collections immuables ne fournissent pas un moyen de le faire hors de la boîte.

Modifié le 10/08/2017: J'ai dû changer le constructeur à zéro argument en un qui prend un argument qu'il ignore, et passer une valeur fictive partout où vous l'utilisez. @ gareth-latty a souligné dans un commentaire qu'un struct ne peut pas avoir de constructeur zéro-args. Lorsque j'ai écrit cet exemple à l'origine, ce n'était pas vrai: pendant un certain temps, les aperçus de C # 6 vous ont permis de fournir un tel constructeur. Cette fonctionnalité a été supprimée avant la livraison de C # 6 (après avoir écrit la réponse d'origine, évidemment), probablement parce que c'était déroutant - il y avait des scénarios dans lesquels le constructeur ne s'exécutait pas. Dans ce cas particulier, il était sûr de l'utiliser, mais malheureusement, la fonction de langue n'existe plus. La suggestion de Gareth était de le changer en classe, mais tout code utilisant cela devrait allouer un objet, provoquant une pression GC inutile - la raison pour laquelle j'ai utilisé un struct était de permettre d'utiliser cette syntaxe avec pas de temps d'exécution supplémentaire.

J'ai essayé de le modifier pour effectuer une initialisation différée de _builder mais il s'avère que le générateur de code JIT n'est pas assez intelligent pour les optimiser, donc même dans les versions, il vérifie _builder pour chaque élément que vous ajoutez. (Et il insère cette vérification et l'appel correspondant à CreateBuilder qui s'avère produire beaucoup de code avec beaucoup de branchements conditionnels). Il est préférable d'avoir une initialisation unique, et cela doit se produire dans le constructeur si vous voulez pouvoir utiliser cette syntaxe d'initialisation. Donc, la seule façon d'utiliser cette syntaxe sans frais supplémentaires est d'avoir une structure qui initialise _builder dans son constructeur, ce qui signifie que nous avons maintenant besoin de cet argument factice laid.

11
Ian Griffiths

Ou ca

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

Une méthode AddRange est également disponible.

10
Miha Markic