Existe-t-il un dictionnaire générique en lecture seule disponible dans .NET?


186

Je renvoie une référence à un dictionnaire dans ma propriété en lecture seule. Comment empêcher les consommateurs de modifier mes données? Si c'était un, IListje pourrais simplement le retourner AsReadOnly. Puis-je faire quelque chose de similaire avec un dictionnaire?

Private _mydictionary As Dictionary(Of String, String)
Public ReadOnly Property MyDictionary() As Dictionary(Of String, String)
    Get
        Return _mydictionary
    End Get
End Property

4
Il doit y avoir un moyen de le faire, sinon il n'y aurait pas de propriété IsReadOnly sur IDictionary ... ( msdn.microsoft.com/en-us/library/bb338949.aspx )
Powerlord

2
La plupart des avantages conceptuels de l'immuabilité peuvent être obtenus sans que le runtime ne l'applique. S'il s'agit d'un projet privé, envisagez une méthode informelle et disciplinée. Si vous devez fournir des données à un consommateur, vous devriez sérieusement envisager des copies complètes. Lorsque vous considérez qu'une collection immuable nécessite 1) une référence immuable à la collection 2) empêchant la mutation de la séquence elle-même et 3) empêchant la modification des propriétés sur les éléments de la collection, et que certains d'entre eux peuvent être violés par réflexion, l'application d'exécution est pas pratique.
Sprague

27
Depuis .NET 4.5, il existe un System.Collections.ObjectModel.ReadOnlyDictionary ^ _ ^
Smartkid

2
Il existe également maintenant des collections immuables Microsoft via NuGet msdn.microsoft.com/en-us/library/dn385366%28v=vs.110%29.aspx
VoteCoffee

Réponses:


156

Voici une implémentation simple qui encapsule un dictionnaire:

public class ReadOnlyDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
    private readonly IDictionary<TKey, TValue> _dictionary;

    public ReadOnlyDictionary()
    {
        _dictionary = new Dictionary<TKey, TValue>();
    }

    public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
    {
        _dictionary = dictionary;
    }

    #region IDictionary<TKey,TValue> Members

    void IDictionary<TKey, TValue>.Add(TKey key, TValue value)
    {
        throw ReadOnlyException();
    }

    public bool ContainsKey(TKey key)
    {
        return _dictionary.ContainsKey(key);
    }

    public ICollection<TKey> Keys
    {
        get { return _dictionary.Keys; }
    }

    bool IDictionary<TKey, TValue>.Remove(TKey key)
    {
        throw ReadOnlyException();
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        return _dictionary.TryGetValue(key, out value);
    }

    public ICollection<TValue> Values
    {
        get { return _dictionary.Values; }
    }

    public TValue this[TKey key]
    {
        get
        {
            return _dictionary[key];
        }
    }

    TValue IDictionary<TKey, TValue>.this[TKey key]
    {
        get
        {
            return this[key];
        }
        set
        {
            throw ReadOnlyException();
        }
    }

    #endregion

    #region ICollection<KeyValuePair<TKey,TValue>> Members

    void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
    {
        throw ReadOnlyException();
    }

    void ICollection<KeyValuePair<TKey, TValue>>.Clear()
    {
        throw ReadOnlyException();
    }

    public bool Contains(KeyValuePair<TKey, TValue> item)
    {
        return _dictionary.Contains(item);
    }

    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
    {
        _dictionary.CopyTo(array, arrayIndex);
    }

    public int Count
    {
        get { return _dictionary.Count; }
    }

    public bool IsReadOnly
    {
        get { return true; }
    }

    bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
    {
        throw ReadOnlyException();
    }

    #endregion

    #region IEnumerable<KeyValuePair<TKey,TValue>> Members

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        return _dictionary.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion

    private static Exception ReadOnlyException()
    {
        return new NotSupportedException("This dictionary is read-only");
    }
}

11
+1 pour publier du code complet et pas seulement un lien, mais je suis curieux de savoir à quoi sert un constructeur vide dans un ReadOnlyDictionary? :-)
Samuel Neff

20
Attention à ce constructeur. Si vous faites une copie de référence du dictionnaire transmis, il est possible qu'un morceau de code extérieur modifie votre dictionnaire en "lecture seule". Votre constructeur doit faire une copie complète et approfondie de l'argument.
askheaves

25
@askheaves: Bonne observation, mais il est en fait assez souvent utile d'utiliser la référence d'origine dans les types en lecture seule - gardez dans votre variable privée l'original et modifiez-la pour les consommateurs extérieurs. Par exemple, consultez les objets ReadOnlyObservableCollection ou ReadOnlyCollection qui sont intégrés: Thomas a fourni quelque chose qui fonctionne exactement comme ceux inhérents au framework .Net. Merci Thomas! +1
Matt DeKrey

13
@ user420667: la manière dont il est implémenté, c'est une "vue en lecture seule d'un dictionnaire non en lecture seule". Un autre code peut modifier le contenu du dictionnaire d'origine et ces modifications seront reflétées dans le dictionnaire en lecture seule. Cela pourrait être le comportement souhaité, ou non, selon ce que vous voulez réaliser ...
Thomas Levesque

6
@Thomas: C'est la même chose qu'une ReadOnlyCollection dans le .NET BCL. Il s'agit d'une vue en lecture seule sur une collection éventuellement modifiable. ReadOnly ne signifie pas immuable et l'immuabilité ne doit pas être attendue.
Jeff Yates

229

.NET 4.5

Le .NET Framework 4.5 BCL introduit ReadOnlyDictionary<TKey, TValue>( source ).

Comme le .NET Framework 4.5 BCL n'inclut pas AsReadOnlyde dictionnaires for, vous devrez écrire le vôtre (si vous le souhaitez). Ce serait quelque chose comme ce qui suit, dont la simplicité souligne peut-être pourquoi ce n'était pas une priorité pour .NET 4.5.

public static ReadOnlyDictionary<TKey, TValue> AsReadOnly<TKey, TValue>(
    this IDictionary<TKey, TValue> dictionary)
{
    return new ReadOnlyDictionary<TKey, TValue>(dictionary);
}

.NET 4.0 et inférieur

Avant .NET 4.5, il n'y a pas de classe de framework .NET qui encapsule un Dictionary<TKey, TValue>comme ReadOnlyCollection encapsule un List . Cependant, il n'est pas difficile d'en créer un.

Voici un exemple - il y en a beaucoup d'autres si vous recherchez ReadOnlyDictionary sur Google .


7
Il ne semble pas qu'ils se soient souvenus de faire une AsReadOnly()méthode sur l'habituel Dictionary<,>, alors je me demande combien de personnes découvriront leur nouveau type. Ce fil de discussion Stack Overflow aidera, cependant.
Jeppe Stig Nielsen

@Jeppe: Je doute que cela ait quelque chose à voir avec le souvenir. Chaque fonctionnalité coûte et je doute qu'AsReadOnly soit en haut de la liste des priorités, d'autant plus qu'il est si facile à écrire.
Jeff Yates

1
Notez qu'il s'agit simplement d'un wrapper; les modifications apportées au dictionnaire sous-jacent (celui passé au constructeur) muteront toujours le dictionnaire en lecture seule. Voir aussi stackoverflow.com/questions/139592/…
TrueWill

1
@JeffYates Compte tenu de sa simplicité, l'écrire aurait pris moins de temps que de décider de passer ou non du temps à l'écrire. À cause de cela, mon pari est sur «ils ont oublié».
Dan Bechard

Comme TrueWill l'a déclaré, le dictionnaire sous-jacent peut toujours être muté. Vous voudrez peut-être envisager de passer un clone du dictionnaire d'origine au constructeur si vous voulez une véritable immuabilité (en supposant que le type de clé et de valeur sont également immuables.)
user420667

19

Il a été annoncé lors de la récente conférence BUILD que depuis .NET 4.5, l'interface System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>est incluse. La preuve est ici (Mono) et ici (Microsoft);)

Je ne sais pas si ReadOnlyDictionaryest également inclus, mais au moins avec l'interface, il ne devrait pas être difficile de créer maintenant une implémentation qui expose une interface générique officielle .NET :)


5
ReadOnlyDictionary<TKey, TValue>(.Net 4.5) - msdn.microsoft.com/en-us/library/gg712875.aspx
myermian

18

N'hésitez pas à utiliser mon emballage simple. Il n'implémente PAS IDictionary, il n'a donc pas à lever d'exceptions lors de l'exécution pour les méthodes de dictionnaire qui modifieraient le dictionnaire. Les méthodes de changement ne sont tout simplement pas là. J'ai créé ma propre interface appelée IReadOnlyDictionary.

public interface IReadOnlyDictionary<TKey, TValue> : IEnumerable
{
    bool ContainsKey(TKey key);
    ICollection<TKey> Keys { get; }
    ICollection<TValue> Values { get; }
    int Count { get; }
    bool TryGetValue(TKey key, out TValue value);
    TValue this[TKey key] { get; }
    bool Contains(KeyValuePair<TKey, TValue> item);
    void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex);
    IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator();
}

public class ReadOnlyDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
{
    readonly IDictionary<TKey, TValue> _dictionary;
    public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
    {
        _dictionary = dictionary;
    }
    public bool ContainsKey(TKey key) { return _dictionary.ContainsKey(key); }
    public ICollection<TKey> Keys { get { return _dictionary.Keys; } }
    public bool TryGetValue(TKey key, out TValue value) { return _dictionary.TryGetValue(key, out value); }
    public ICollection<TValue> Values { get { return _dictionary.Values; } }
    public TValue this[TKey key] { get { return _dictionary[key]; } }
    public bool Contains(KeyValuePair<TKey, TValue> item) { return _dictionary.Contains(item); }
    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) { _dictionary.CopyTo(array, arrayIndex); }
    public int Count { get { return _dictionary.Count; } }
    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() { return _dictionary.GetEnumerator(); }
    IEnumerator IEnumerable.GetEnumerator() { return _dictionary.GetEnumerator(); }
}

4
+1 pour ne pas violer le IDictionarycontrat. Je pense que c'est plus correct du point de vue de la POO pour IDictionaryhériter IReadOnlyDictionary.
Sam

@Sam D'accord, et si nous pouvions revenir en arrière, je pense que ce serait le meilleur et le plus correct d'avoir IDictionary(pour le courant IReadOnlyDictionary) et IMutableDictionary(pour le courant IDictionary).
MasterMastic

1
@MasterMastic C'est une proposition bizarre. Je ne me souviens d'aucune autre classe intégrée reposant sur l'hypothèse inverse selon laquelle une collection immuable est ce qu'un utilisateur attend par défaut.
Dan Bechard

11

IsReadOnly on IDictionary<TKey,TValue>est hérité de ICollection<T>( IDictionary<TKey,TValue>s'étend en ICollection<T>tant que ICollection<KeyValuePair<TKey,TValue>>). Il n'est en aucun cas utilisé ou implémenté (et est en fait "caché" par l'utilisation de l'implémentation explicite desICollection<T> membres ).

Il y a au moins 3 façons dont je peux voir pour résoudre le problème:

  1. Implémenter une lecture seule personnalisée IDictionary<TKey, TValue>et envelopper / déléguer à un dictionnaire interne comme cela a été suggéré
  2. Renvoie un ICollection<KeyValuePair<TKey, TValue>>ensemble en lecture seule ou en IEnumerable<KeyValuePair<TKey, TValue>>fonction de l'utilisation de la valeur
  3. Clonez le dictionnaire à l'aide du constructeur de copie .ctor(IDictionary<TKey, TValue>)et renvoyez une copie - de cette façon, l'utilisateur est libre d'en faire ce qu'il veut et cela n'a pas d'impact sur l'état de l'objet hébergeant le dictionnaire source. Notez que si le dictionnaire que vous clonez contient des types de référence (pas des chaînes comme indiqué dans l'exemple), vous devrez faire la copie "manuellement" et cloner également les types de référence.

En aparté; lors de l'exposition de collections, essayez d'exposer la plus petite interface possible - dans le cas d'exemple, il devrait s'agir de IDictionary car cela vous permet de faire varier l'implémentation sous-jacente sans rompre le contrat public que le type expose.


8

Un dictionnaire en lecture seule peut dans une certaine mesure être remplacé par Func<TKey, TValue>- J'utilise généralement ceci dans une API si je veux seulement que les gens effectuent des recherches; c'est simple, et en particulier, il est simple de remplacer le backend si jamais vous le souhaitez. Cependant, il ne fournit pas la liste des clés; si cela compte dépend de ce que vous faites.


4

Non, mais ce serait facile de rouler le vôtre. IDictionary définit une propriété IsReadOnly. Enveloppez simplement un dictionnaire et lancez une NotSupportedException à partir des méthodes appropriées.


3

Aucun disponible dans la BCL. Cependant, j'ai publié un ReadOnlyDictionary (nommé ImmutableMap) dans mon projet BCL Extras

En plus d'être un dictionnaire totalement immuable, il prend en charge la production d'un objet proxy qui implémente IDictionary et peut être utilisé dans n'importe quel endroit où IDictionary est pris. Il lèvera une exception chaque fois qu'une des API en mutation est appelée

void Example() { 
  var map = ImmutableMap.Create<int,string>();
  map = map.Add(42,"foobar");
  IDictionary<int,string> dictionary = CollectionUtility.ToIDictionary(map);
}

9
Votre ImmutableMap est implémenté sous forme d'arborescence équilibrée. Étant donné que, dans .NET, les gens s'attendent généralement à ce qu'un «dictionnaire» soit implémenté via le hachage - et présente les propriétés de complexité correspondantes - vous voudrez peut-être faire attention à la promotion d'ImmutableMap en tant que «dictionnaire».
Glenn Slayden

semble que les sites code.msdn.com sont obsolètes. BCLextras maintenant ici github.com/scottwis/tiny/tree/master/third-party/BclExtras
BozoJoe

1

Vous pouvez créer une classe qui n'implémente qu'une implémentation partielle du dictionnaire et masque toutes les fonctions d'ajout / suppression / ensemble.

Utilisez un dictionnaire en interne auquel la classe externe transmet toutes les requêtes.

Cependant, comme votre dictionnaire contient probablement des types de référence, il n'y a aucun moyen d'empêcher l'utilisateur de définir des valeurs sur les classes détenues par le dictionnaire (à moins que ces classes elles-mêmes ne soient en lecture seule)


1

Je ne pense pas qu'il y ait un moyen facile de le faire ... si votre dictionnaire fait partie d'une classe personnalisée, vous pouvez y parvenir avec un indexeur:

public class MyClass
{
  private Dictionary<string, string> _myDictionary;

  public string this[string index]
  {
    get { return _myDictionary[index]; }
  }
}

J'ai besoin de pouvoir exposer l'intégralité du dictionnaire ainsi qu'un indexeur.
Rob Sobers

Cela semble être une très bonne solution. Cependant, les clients de la classe MyClass peuvent avoir besoin d'en savoir plus sur le dictionnaire, par exemple, pour l'itérer. Et si une clé n'existe pas (exposer TryGetValue () sous une forme ou une autre peut être une bonne idée)? Pouvez-vous rendre votre réponse et votre exemple de code plus complets?
Peter Mortensen

1

+1 Excellent travail, Thomas. J'ai poussé ReadOnlyDictionary un peu plus loin.

Tout comme la solution de Dale, je voulais enlever Add(), Clear(), Remove(), etc de IntelliSense. Mais je voulais que mes objets dérivés soient implémentés IDictionary<TKey, TValue>.

De plus, je voudrais que le code suivant se brise: (Encore une fois, la solution de Dale le fait aussi)

ReadOnlyDictionary<int, int> test = new ReadOnlyDictionary<int,int>(new Dictionary<int, int> { { 1, 1} });
test.Add(2, 1);  //CS1061

La ligne Add () donne:

error CS1061: 'System.Collections.Generic.ReadOnlyDictionary<int,int>' does not contain a definition for 'Add' and no extension method 'Add' accepting a first argument 

L'appelant peut toujours le convertir IDictionary<TKey, TValue>, mais le NotSupportedExceptionsera déclenché si vous essayez d'utiliser les membres non en lecture seule (à partir de la solution de Thomas).

Quoi qu'il en soit, voici ma solution pour tous ceux qui voulaient aussi ceci:

namespace System.Collections.Generic
{
    public class ReadOnlyDictionary<TKey, TValue> : IDictionary<TKey, TValue>
    {
        const string READ_ONLY_ERROR_MESSAGE = "This dictionary is read-only";

        protected IDictionary<TKey, TValue> _Dictionary;

        public ReadOnlyDictionary()
        {
            _Dictionary = new Dictionary<TKey, TValue>();
        }

        public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
        {
            _Dictionary = dictionary;
        }

        public bool ContainsKey(TKey key)
        {
            return _Dictionary.ContainsKey(key);
        }

        public ICollection<TKey> Keys
        {
            get { return _Dictionary.Keys; }
        }

        public bool TryGetValue(TKey key, out TValue value)
        {
            return _Dictionary.TryGetValue(key, out value);
        }

        public ICollection<TValue> Values
        {
            get { return _Dictionary.Values; }
        }

        public TValue this[TKey key]
        {
            get { return _Dictionary[key]; }
            set { throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE); }
        }

        public bool Contains(KeyValuePair<TKey, TValue> item)
        {
            return _Dictionary.Contains(item);
        }

        public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
        {
            _Dictionary.CopyTo(array, arrayIndex);
        }

        public int Count
        {
            get { return _Dictionary.Count; }
        }

        public bool IsReadOnly
        {
            get { return true; }
        }

        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
        {
            return _Dictionary.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return (_Dictionary as IEnumerable).GetEnumerator();
        }

        void IDictionary<TKey, TValue>.Add(TKey key, TValue value)
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }

        bool IDictionary<TKey, TValue>.Remove(TKey key)
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }

        void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }

        void ICollection<KeyValuePair<TKey, TValue>>.Clear()
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }
    }
}


0
public IEnumerable<KeyValuePair<string, string>> MyDictionary()
{
    foreach(KeyValuePair<string, string> item in _mydictionary)
        yield return item;
}

2
Ou vous pouvez faire:public IEnumerable<KeyValuePair<string, string>> MyDictionary() { return _mydictionary; }
Pat

0

C'est une mauvaise solution, voir en bas.

Pour ceux qui utilisent encore .NET 4.0 ou une version antérieure, j'ai une classe qui fonctionne exactement comme celle de la réponse acceptée, mais elle est beaucoup plus courte. Il étend l'objet Dictionary existant, remplaçant (masquant en fait) certains membres pour qu'ils lèvent une exception lorsqu'ils sont appelés.

Si l'appelant essaie d'appeler Add, Remove ou une autre opération de mutation du dictionnaire intégré, le compilateur lèvera une erreur. J'utilise les attributs obsolètes pour signaler ces erreurs de compilation. De cette façon, vous pouvez remplacer un dictionnaire par ce ReadOnlyDictionary et voir immédiatement où peuvent se trouver les problèmes sans avoir à exécuter votre application et à attendre les exceptions d'exécution.

Regarde:

public class ReadOnlyException : Exception
{
}

public class ReadOnlyDictionary<TKey, TValue> : Dictionary<TKey, TValue>
{
    public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
        : base(dictionary) { }

    public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer)
        : base(dictionary, comparer) { }

    //The following four constructors don't make sense for a read-only dictionary

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public ReadOnlyDictionary() { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public ReadOnlyDictionary(IEqualityComparer<TKey> comparer) { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public ReadOnlyDictionary(int capacity) { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public ReadOnlyDictionary(int capacity, IEqualityComparer<TKey> comparer) { throw new ReadOnlyException(); }


    //Use hiding to override the behavior of the following four members
    public new TValue this[TKey key]
    {
        get { return base[key]; }
        //The lack of a set accessor hides the Dictionary.this[] setter
    }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public new void Add(TKey key, TValue value) { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public new void Clear() { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public new bool Remove(TKey key) { throw new ReadOnlyException(); }
}

Cette solution a un problème signalé par @supercat illustré ici:

var dict = new Dictionary<int, string>
{
    { 1, "one" },
    { 2, "two" },
    { 3, "three" },
};

var rodict = new ReadOnlyDictionary<int, string>(dict);
var rwdict = rodict as Dictionary<int, string>;
rwdict.Add(4, "four");

foreach (var item in rodict)
{
    Console.WriteLine("{0}, {1}", item.Key, item.Value);
}

Plutôt que de donner une erreur de compilation comme je m'y attendais, ou une exception d'exécution comme je l'espérais, ce code s'exécute sans erreur. Il imprime quatre nombres. Cela fait de mon ReadOnlyDictionary un ReadWriteDictionary.


Le problème avec cette approche est qu'un tel objet peut être passé à une méthode qui attend un Dictionary<TKey,TValue>sans aucune plainte du compilateur, et le cast ou la contrainte d'une référence au type de dictionnaire simple supprimera toutes les protections.
supercat

@supercat, merde, tu as raison. Je pensais aussi avoir une bonne solution.
user2023861

Je me souviens avoir fait un dérivé de Dictionaryavec une Cloneméthode enchaînée MemberwiseClone. Malheureusement, alors qu'il devrait être possible de cloner efficacement un dictionnaire en clonant les magasins de sauvegarde, le fait que les magasins de sauvegarde soient privateplutôt que protectedsignifie qu'il n'y a aucun moyen pour une classe dérivée de les cloner; l'utilisation MemberwiseClonesans cloner également les magasins de sauvegarde signifiera que les modifications ultérieures apportées au dictionnaire original casseront le clone, et les modifications apportées au clone casseront l'original.
supercat
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.