Esiste un dizionario generico di sola lettura disponibile in .NET?


186

Sto restituendo un riferimento a un dizionario nella mia proprietà di sola lettura. Come posso impedire ai consumatori di modificare i miei dati? Se questo fosse un, IListpotrei semplicemente restituirlo AsReadOnly. C'è qualcosa di simile che posso fare con un dizionario?

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

4
Ci deve essere un modo per farlo, altrimenti non ci sarebbe una proprietà IsReadOnly su IDictionary ... ( msdn.microsoft.com/en-us/library/bb338949.aspx )
Powerlord

2
Molti dei vantaggi concettuali dell'immutabilità possono essere ottenuti senza che il tempo di esecuzione venga applicato. Se questo è un progetto privato, considera un metodo disciplinato e informale. Se è necessario fornire dati a un consumatore, è necessario prendere seriamente in considerazione copie in profondità. Se si considera che una raccolta immutabile richiede 1) riferimento immutabile alla raccolta 2) impedire la mutazione della sequenza stessa e 3) impedire la modifica delle proprietà sugli elementi della raccolta e che alcuni di questi possono essere violati dalla riflessione, l'applicazione del runtime è non pratico.
Sprague,


2
C'è anche ora Microsoft immutabili Collezioni tramite NuGet msdn.microsoft.com/en-us/library/dn385366%28v=vs.110%29.aspx~~V~~singular~~3rd
VoteCoffee

Risposte:


156

Ecco una semplice implementazione che avvolge un dizionario:

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 per la pubblicazione di codice completo e non solo di un collegamento, ma sono curioso, qual è il punto di un costruttore vuoto in un ReadOnlyDictionary? :-)
Samuel Neff,

20
Fai attenzione a quel costruttore. Se si esegue una copia di riferimento del dizionario passato, è possibile che un codice esterno modifichi il dizionario "Sola lettura". Il tuo costruttore dovrebbe fare una copia completa e approfondita dell'argomento.
askheaves,

25
@askheaves: buona osservazione, ma in realtà è abbastanza spesso utile utilizzare il riferimento originale nei tipi di sola lettura - mantenere nella variabile privata l'originale e modificarlo per i consumatori esterni. Ad esempio, controlla gli oggetti ReadOnlyObservableCollection o ReadOnlyCollection integrati: Thomas ha fornito qualcosa che funziona esattamente come quelli inerenti al framework .Net. Grazie Thomas! +1
Matt DeKrey,

13
@ user420667: il modo in cui è implementato, è una "vista di sola lettura di un dizionario non di sola lettura". Qualche altro codice potrebbe cambiare il contenuto del dizionario originale e queste modifiche si rifletteranno nel dizionario di sola lettura. Potrebbe essere il comportamento desiderato, o no, a seconda di ciò che vuoi ottenere ...
Thomas Levesque,

6
@Thomas: è lo stesso di ReadOnlyCollection in .NET BCL. È una vista di sola lettura su una raccolta eventualmente mutabile. ReadOnly non significa immutabile e l'immutabilità non dovrebbe essere prevista.
Jeff Yates,

229

.NET 4.5

BCL .NET Framework 4.5 introduce ReadOnlyDictionary<TKey, TValue>( fonte ).

Poiché .NET Framework 4.5 BCL non include un AsReadOnlydizionario per i dizionari, sarà necessario scriverne uno proprio (se lo si desidera). Sarebbe qualcosa di simile al seguente, la cui semplicità forse evidenzia perché non fosse una priorità per .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 e versioni precedenti

Prima di .NET 4.5, non esiste una classe di framework .NET che avvolge un oggetto Dictionary<TKey, TValue>simile a ReadOnlyCollection che avvolge un elenco . Tuttavia, non è difficile crearne uno.

Ecco un esempio : ce ne sono molti altri se si utilizza Google per ReadOnlyDictionary .


7
Non sembra che si siano ricordati di fare un AsReadOnly()metodo sul solito Dictionary<,>, quindi mi chiedo quante persone scopriranno il loro nuovo tipo. Questo thread Stack Overflow ti aiuterà, comunque.
Jeppe Stig Nielsen,

@Jeppe: dubito che abbia qualcosa a che fare con il ricordare. Ogni funzione costa e dubito che AsReadOnly fosse in cima alla lista delle priorità, soprattutto perché è così facile da scrivere.
Jeff Yates,

1
Si noti che questo è semplicemente un wrapper; le modifiche al dizionario sottostante (quello passato al costruttore) trasformeranno comunque il dizionario di sola lettura. Vedere anche stackoverflow.com/questions/139592/...
TrueWill

1
@JeffYates Considerando quanto sia semplice, scriverlo ci sarebbe voluto meno tempo che decidere se dedicare o meno il tempo a scriverlo. Per questo motivo, la mia scommessa è su "si sono dimenticati".
Dan Bechard,

Come affermato da TrueWill, il dizionario sottostante può ancora essere modificato. Potresti prendere in considerazione l'idea di passare al costruttore un clone del dizionario originale se desideri la vera immutabilità (supponendo che anche la chiave e il tipo di valore siano immutabili).
user420667

19

È stato annunciato nella recente conferenza BUILD che da .NET 4.5 l'interfaccia System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>è inclusa. La prova è qui (Mono) e qui (Microsoft);)

Non sono sicuro che ReadOnlyDictionarysia incluso anche, ma almeno con l'interfaccia non dovrebbe essere difficile creare ora un'implementazione che espone un'interfaccia generica .NET ufficiale :)


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

18

Sentiti libero di usare il mio semplice wrapper. NON implementa IDictionary, quindi non deve generare eccezioni in fase di esecuzione per i metodi di dizionario che potrebbero modificare il dizionario. I metodi di modifica semplicemente non ci sono. Ho creato la mia interfaccia per questo chiamata 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 per non aver violato il IDictionarycontratto. Penso che sia più corretto dal punto di vista OOP per cui IDictionaryereditare IReadOnlyDictionary.
Sam,

@ Sam concordato, e se potessimo tornare indietro penso che sarebbe il migliore e più corretto avere IDictionary(per corrente IReadOnlyDictionary) e IMutableDictionary(per corrente IDictionary).
MasterMastic

1
@MasterMastic Questa è una proposta bizzarra. Non ricordo altre classi integrate che si basano sul presupposto inverso che una raccolta immutabile è ciò che un utente si aspetta per impostazione predefinita.
Dan Bechard,

11

IsReadOnly on IDictionary<TKey,TValue>è ereditato da ICollection<T>( IDictionary<TKey,TValue>estende ICollection<T>come ICollection<KeyValuePair<TKey,TValue>>). Non viene utilizzato o implementato in alcun modo (ed è in effetti "nascosto" attraverso l'uso esplicito dei ICollection<T>membri associati ).

Ci sono almeno 3 modi che posso vedere per risolvere il problema:

  1. Implementa una lettura personalizzata IDictionary<TKey, TValue>e esegui il wrapping / delega in un dizionario interno come è stato suggerito
  2. Restituisce un ICollection<KeyValuePair<TKey, TValue>>set in sola lettura o in IEnumerable<KeyValuePair<TKey, TValue>>base all'uso del valore
  3. Clonare il dizionario usando il costruttore di copie .ctor(IDictionary<TKey, TValue>)e restituire una copia - in questo modo l'utente è libero di farcela a suo piacimento e non ha alcun impatto sullo stato dell'oggetto che ospita il dizionario di origine. Nota che se il dizionario che stai clonando contiene tipi di riferimento (non stringhe come mostrato nell'esempio) dovrai eseguire la copia "manualmente" e clonare anche i tipi di riferimento.

A parte; quando si espongono le raccolte, mirare a esporre l'interfaccia più piccola possibile - nel caso di esempio dovrebbe essere IDictionary in quanto ciò consente di variare l'implementazione sottostante senza interrompere il contratto pubblico che il tipo espone.


8

Un dizionario di sola lettura può in qualche modo essere sostituito da Func<TKey, TValue>- Di solito lo uso in un'API se voglio solo che le persone eseguano ricerche; è semplice e, in particolare, è semplice sostituire il backend se lo desideri. Tuttavia non fornisce l'elenco di chiavi; se ciò che conta dipende da cosa stai facendo.


4

No, ma sarebbe facile realizzarne uno tuo. IDictionary definisce una proprietà IsReadOnly. Basta racchiudere un dizionario e generare NotSupportedException dai metodi appropriati.


3

Nessuno disponibile nel BCL. Tuttavia ho pubblicato un ReadOnlyDictionary (chiamato ImmutableMap) nel mio progetto BCL Extras

Oltre ad essere un dizionario completamente immutabile, supporta la produzione di un oggetto proxy che implementa IDictionary e può essere utilizzato in qualsiasi luogo in cui viene preso IDictionary. Genererà un'eccezione ogni volta che viene chiamata una delle API mutanti

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

9
ImmutableMap è implementato come un albero bilanciato. Dato che, in .NET, le persone generalmente si aspettano che un "dizionario" venga implementato tramite hashing - e presentano le proprietà di complessità corrispondenti - potresti voler fare attenzione a promuovere ImmutableMap come "dizionario".
Glenn Slayden,

sembra che i siti code.msdn.com siano defunti. BCLextras ora qui github.com/scottwis/tiny/tree/master/third-party/BclExtras
BozoJoe

1

È possibile creare una classe che implementa solo un'implementazione parziale del dizionario e nasconde tutte le funzioni Aggiungi / Rimuovi / Imposta.

Utilizzare internamente un dizionario a cui la classe esterna passa tutte le richieste.

Tuttavia, poiché è probabile che il tuo dizionario contenga tipi di riferimento, non puoi in alcun modo impedire all'utente di impostare valori sulle classi detenute dal dizionario (a meno che tali classi non siano di sola lettura)


1

Non penso che ci sia un modo semplice per farlo ... se il tuo dizionario fa parte di una classe personalizzata, potresti ottenerlo con un indicizzatore:

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

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

Devo essere in grado di esporre l'intero dizionario e un indicizzatore.
Rob Sobers,

Questa sembra un'ottima soluzione. Tuttavia, i client della classe MyClass potrebbero aver bisogno di saperne di più sul dizionario, ad esempio per l'iterazione attraverso di esso. E se una chiave non esistesse (esporre TryGetValue () in qualche modo potrebbe essere una buona idea)? Puoi rendere più completa la tua risposta e il tuo codice di esempio?
Peter Mortensen,

1

+1 Ottimo lavoro, Thomas. Ho fatto un ulteriore passo avanti su ReadOnlyDictionary.

Proprio come la soluzione di Dale, ho voluto rimuovere Add(), Clear(), Remove(), ecc da IntelliSense. Ma volevo implementare i miei oggetti derivati IDictionary<TKey, TValue>.

Inoltre, vorrei che il seguente codice si rompesse: (Anche in questo caso la soluzione di Dale fa questo)

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

La riga Aggiungi () risulta in:

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

Il chiamante può ancora lanciarlo IDictionary<TKey, TValue>, ma NotSupportedExceptionverrà sollevato se si tenta di utilizzare i membri di sola lettura (dalla soluzione di Thomas).

Comunque, ecco la mia soluzione per chiunque volesse anche questo:

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
Oppure puoi fare:public IEnumerable<KeyValuePair<string, string>> MyDictionary() { return _mydictionary; }
Pat

0

Questa è una cattiva soluzione, vedi in fondo.

Per quelli che usano ancora .NET 4.0 o precedenti, ho una classe che funziona esattamente come quella nella risposta accettata, ma è molto più breve. Estende l'oggetto Dizionario esistente, sovrascrivendo (effettivamente nascondendo) alcuni membri per far loro generare un'eccezione quando viene chiamato.

Se il chiamante prova a chiamare Aggiungi, Rimuovi o qualche altra operazione di mutazione che ha il Dizionario incorporato, il compilatore genererà un errore. Uso gli attributi Obsoleti per aumentare questi errori del compilatore. In questo modo, è possibile sostituire un dizionario con questo ReadOnlyDictionary e vedere immediatamente dove potrebbero esserci problemi senza dover eseguire l'applicazione e attendere eccezioni di runtime.

Guarda:

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

Questa soluzione presenta un problema segnalato da @supercat illustrato qui:

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

Invece di dare un errore di compilazione come mi aspettavo o un'eccezione di runtime come speravo, questo codice viene eseguito senza errori. Stampa quattro numeri. Ciò rende il mio ReadOnlyDictionary un ReadWriteDictionary.


Il problema con questo approccio è che un tale oggetto può essere passato a un metodo che prevede una Dictionary<TKey,TValue>senza alcuna lamentela da parte del compilatore, e il cast o la coercizione di un riferimento al tipo di dizionario semplice rimuoverà eventuali protezioni.
supercat

@supercat, merda, hai ragione. Pensavo di avere anche una buona soluzione.
user2023861

Ricordo di aver fatto un derivato Dictionarycon un Clonemetodo a cui era incatenato MemberwiseClone. Sfortunatamente, mentre dovrebbe essere possibile clonare in modo efficiente un dizionario clonando i negozi di supporto, il fatto che i negozi di supporto siano privatepiuttosto che protectedsignifica che non è possibile clonarli per una classe derivata; l'utilizzo MemberwiseClonesenza clonare anche i negozi di supporto comporterà che le successive modifiche apportate al dizionario originale spezzeranno il clone e le modifiche apportate al clone romperanno l'originale.
supercat
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.