¿Hay un diccionario genérico de solo lectura disponible en .NET?

186

Estoy devolviendo una referencia a un diccionario en mi propiedad de solo lectura. ¿Cómo evito que los consumidores cambien mis datos? Si esto fuera un IList, simplemente podría devolverlo AsReadOnly. ¿Hay algo similar que pueda hacer con un diccionario?

Private _mydictionary As Dictionary(Of String, String)
Public ReadOnly Property MyDictionary() As Dictionary(Of String, String)
    Get
        Return _mydictionary
    End Get
End Property
Rob Sobers
fuente
44
Debe haber alguna forma de hacerlo, o de lo contrario no habría una propiedad IsReadOnly en IDictionary ... ( msdn.microsoft.com/en-us/library/bb338949.aspx )
Powerlord
2
Muchos de los beneficios conceptuales de la inmutabilidad se pueden obtener sin el tiempo de ejecución. Si este es un proyecto privado, considere un método disciplinado e informal. Si debe proporcionar datos a un consumidor, debe considerar seriamente las copias detalladas. Cuando considera que una colección inmutable requiere 1) referencia inmutable a la colección 2) evitar la mutación de la secuencia misma y 3) evitar modificar las propiedades de los elementos de la colección, y que algunos de estos pueden ser violados por reflexión, la ejecución en tiempo de ejecución es no practico.
Sprague
27
Desde .NET 4.5, hay un System.Collections.ObjectModel.ReadOnlyDictionary ^ _ ^
Smartkid
2
Ahora también hay colecciones inmutables de Microsoft a través de NuGet msdn.microsoft.com/en-us/library/dn385366%28v=vs.110%29.aspx
VoteCoffee

Respuestas:

156

Aquí hay una implementación simple que envuelve un diccionario:

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");
    }
}
Thomas Levesque
fuente
11
+1 para publicar código completo y no solo un enlace, pero tengo curiosidad, ¿cuál es el punto de un constructor vacío en un ReadOnlyDictionary? :-)
Samuel Neff
20
Cuidado con ese constructor. Si hace una copia de referencia del diccionario aprobado, es posible que un código externo modifique su diccionario "Solo lectura". Su constructor debe hacer una copia completa y profunda del argumento.
askheaves
25
@askheaves: Buena observación, pero en realidad es bastante útil usar la referencia original en los tipos de solo lectura: mantenga en su variable privada el original y modifíquelo para los consumidores externos. Por ejemplo, vea los objetos ReadOnlyObservableCollection o ReadOnlyCollection que están integrados: Thomas proporcionó algo que funciona exactamente como los inherentes al marco .Net. Gracias Thomas! +1
Matt DeKrey
13
@ user420667: la forma en que se implementa es una "vista de solo lectura de un diccionario que no es de solo lectura". Algún otro código puede cambiar el contenido del diccionario original, y estos cambios se reflejarán en el diccionario de solo lectura. Podría ser el comportamiento deseado, o no, dependiendo de lo que quieras lograr ...
Thomas Levesque
66
@Thomas: Eso es lo mismo que una ReadOnlyCollection en .NET BCL. Es una vista de solo lectura en una colección posiblemente mutable. ReadOnly no significa inmutable y no debe esperarse inmutabilidad.
Jeff Yates
229

.NET 4.5

El .NET Framework 4.5 BCL presenta ReadOnlyDictionary<TKey, TValue>( fuente ).

Como .NET Framework 4.5 BCL no incluye un AsReadOnlydiccionario, deberá escribir el suyo propio (si lo desea). Sería algo como lo siguiente, cuya simplicidad quizás resalta por qué no era una prioridad para .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 y menos

Antes de .NET 4.5, no existe una clase de .NET Framework que se ajusta un Dictionary<TKey, TValue>como el ReadOnlyCollection envuelve una lista . Sin embargo, no es difícil crear uno.

Aquí hay un ejemplo : hay muchos otros si busca en Google ReadOnlyDictionary .

Jeff Yates
fuente
77
No parece que hayan recordado hacer un AsReadOnly()método de la forma habitual Dictionary<,>, así que me pregunto cuántas personas descubrirán su nuevo tipo. Sin embargo, este hilo de desbordamiento de pila ayudará.
Jeppe Stig Nielsen
@Jeppe: Dudo que tenga algo que ver con recordar. Todas las funciones cuestan y dudo que AsReadOnly ocupara un lugar destacado en la lista de prioridades, especialmente porque es muy fácil de escribir.
Jeff Yates el
1
Tenga en cuenta que esto es simplemente un contenedor; los cambios en el diccionario subyacente (el que se pasó al constructor) aún mutarán el diccionario de solo lectura. Consulte también stackoverflow.com/questions/139592/…
TrueWill el
1
@JeffYates Teniendo en cuenta lo simple que es, escribirlo hubiera llevado menos tiempo que decidir si dedicar o no tiempo a escribirlo. Por eso, mi apuesta es "se olvidaron".
Dan Bechard
Como dijo TrueWill, el diccionario subyacente aún puede mutarse. Es posible que desee considerar la aprobación de un clon del diccionario original al constructor si quieres verdadera inmutabilidad (suponiendo clave y el tipo de valor también son inmutables.)
user420667
19

En la reciente conferencia BUILD se anunció que, desde .NET 4.5, la interfaz System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>está incluida. La prueba está aquí (Mono) y aquí (Microsoft);)

No estoy seguro si también ReadOnlyDictionaryestá incluido, pero al menos con la interfaz no debería ser difícil crear ahora una implementación que exponga una interfaz genérica .NET oficial :)

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

Siéntase libre de usar mi envoltura simple. NO implementa IDictionary, por lo que no tiene que lanzar excepciones en tiempo de ejecución para los métodos de diccionario que cambiarían el diccionario. Los métodos de cambio simplemente no están ahí. Hice mi propia interfaz para ello llamada 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(); }
}
Dale Barnard
fuente
44
+1 por no violar el IDictionarycontrato. Creo que es más correcto desde una perspectiva OOP IDictionaryheredar de IReadOnlyDictionary.
Sam
@Sam estuvo de acuerdo, y si pudiéramos regresar, creo que sería lo mejor y más correcto tener IDictionary(para el actual IReadOnlyDictionary) y IMutableDictionary(para el actual IDictionary).
MasterMastic
1
@MasterMastic Esa es una propuesta extraña. No recuerdo ninguna otra clase incorporada que se base en el supuesto inverso de que una colección inmutable es lo que un usuario espera de forma predeterminada.
Dan Bechard
11

IsReadOnly on IDictionary<TKey,TValue>se hereda de ICollection<T>(se IDictionary<TKey,TValue>extiende ICollection<T>como ICollection<KeyValuePair<TKey,TValue>>). No se usa ni implementa de ninguna manera (y de hecho está "oculto" mediante el uso de la implementación explícita de los ICollection<T>miembros asociados ).

Hay al menos 3 formas en que puedo ver para resolver el problema:

  1. Implemente un solo lectura personalizado IDictionary<TKey, TValue>y ajuste / delegue a un diccionario interno como se ha sugerido
  2. Devuelve un ICollection<KeyValuePair<TKey, TValue>>conjunto como de solo lectura o IEnumerable<KeyValuePair<TKey, TValue>>dependiendo del uso del valor
  3. Clone el diccionario usando el constructor de copias .ctor(IDictionary<TKey, TValue>)y devuelva una copia, de esa manera el usuario puede hacerlo libremente, y no afecta el estado del objeto que aloja el diccionario fuente. Tenga en cuenta que si el diccionario que está clonando contiene tipos de referencia (no cadenas como se muestra en el ejemplo), tendrá que hacer la copia "manualmente" y clonar también los tipos de referencia.

Como un aparte; al exponer colecciones, intente exponer la interfaz más pequeña posible; en el caso de ejemplo, debe ser IDictionary, ya que le permite variar la implementación subyacente sin romper el contrato público que expone el tipo.

Neal
fuente
8

Un diccionario de solo lectura puede ser reemplazado hasta cierto punto por Func<TKey, TValue>: por lo general, lo uso en una API si solo quiero que las personas realicen búsquedas; es simple, y en particular, es simple reemplazar el backend si alguna vez lo desea. Sin embargo, no proporciona la lista de claves; si eso importa depende de lo que estés haciendo.

Eamon Nerbonne
fuente
4

No, pero sería fácil rodar el tuyo. IDictionary define una propiedad IsReadOnly. Simplemente envuelva un diccionario y arroje una excepción NotSupportedException de los métodos apropiados.

wekempf
fuente
3

Ninguno disponible en el BCL. Sin embargo, publiqué un ReadOnlyDictionary (llamado ImmutableMap) en mi proyecto BCL Extras

Además de ser un diccionario totalmente inmutable, admite la producción de un objeto proxy que implementa IDictionary y puede usarse en cualquier lugar donde se tome IDictionary. Lanzará una excepción cada vez que se llame a una de las API mutantes

void Example() { 
  var map = ImmutableMap.Create<int,string>();
  map = map.Add(42,"foobar");
  IDictionary<int,string> dictionary = CollectionUtility.ToIDictionary(map);
}
JaredPar
fuente
9
Su ImmutableMap se implementa como un árbol equilibrado. Dado que, en .NET, la gente generalmente espera que se implemente un "diccionario" a través del hashing, y exhibe las propiedades de complejidad correspondientes, es posible que desee tener cuidado al promocionar ImmutableMap como un "diccionario".
Glenn Slayden
parece que los sitios code.msdn.com están extintos. BCLextras ahora aquí github.com/scottwis/tiny/tree/master/third-party/BclExtras
BozoJoe
1

Puede crear una clase que solo implemente una implementación parcial del diccionario y oculte todas las funciones de agregar / quitar / establecer.

Use un diccionario internamente al que la clase externa pasa todas las solicitudes.

Sin embargo, dado que es probable que su diccionario contenga tipos de referencia, no hay forma de evitar que el usuario establezca valores en las clases que posee el diccionario (a menos que esas clases sean solo de lectura)

Jason Coyne
fuente
1

No creo que haya una manera fácil de hacerlo ... si su diccionario es parte de una clase personalizada, podría lograrlo con un indexador:

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

  public string this[string index]
  {
    get { return _myDictionary[index]; }
  }
}
Jonas
fuente
Necesito poder exponer todo el diccionario, así como un indexador.
Rob Sobers el
Esto parece una muy buena solución. Sin embargo, los clientes de la clase MyClass pueden necesitar saber más sobre el diccionario, por ejemplo, para iterar a través de él. ¿Y qué pasa si no existe una clave (podría ser una buena idea exponer TryGetValue () de alguna forma)? ¿Puedes hacer que tu respuesta y el código de muestra sean más completos?
Peter Mortensen el
1

+1 Buen trabajo, Thomas. Llevé a ReadOnlyDictionary un paso más allá.

Al igual que la solución de Dale, que quería eliminar Add(), Clear(), Remove(),, etc de IntelliSense. Pero quería que mis objetos derivados se implementaran IDictionary<TKey, TValue>.

Además, me gustaría que se rompa el siguiente código: (Nuevamente, la solución de Dale también lo hace)

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

La línea Add () da como resultado:

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

La persona que llama aún puede enviarlo IDictionary<TKey, TValue>, pero se NotSupportedExceptiongenerará si intenta usar los miembros que no son de solo lectura (de la solución de Thomas).

De todos modos, aquí está mi solución para cualquiera que también quisiera esto:

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);
        }
    }
}
Robert H.
fuente
0
public IEnumerable<KeyValuePair<string, string>> MyDictionary()
{
    foreach(KeyValuePair<string, string> item in _mydictionary)
        yield return item;
}
shahkalpesh
fuente
2
O puede hacer:public IEnumerable<KeyValuePair<string, string>> MyDictionary() { return _mydictionary; }
Pat
0

Esta es una mala solución, ver abajo.

Para aquellos que todavía usan .NET 4.0 o anterior, tengo una clase que funciona igual que la de la respuesta aceptada, pero es mucho más corta. Extiende el objeto Diccionario existente, anula (en realidad oculta) ciertos miembros para que arrojen una excepción cuando se les llama.

Si la persona que llama intenta llamar a Agregar, Eliminar u otra operación de mutación que tiene el Diccionario incorporado, el compilador arrojará un error. Uso los atributos Obsoletos para generar estos errores de compilación. De esta manera, puede reemplazar un diccionario con este ReadOnlyDictionary e inmediatamente ver dónde podrían estar los problemas sin tener que ejecutar su aplicación y esperar excepciones de tiempo de ejecución.

Echar un vistazo:

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

Esta solución tiene un problema señalado por @supercat ilustrado aquí:

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

En lugar de dar un error en tiempo de compilación como esperaba, o una excepción en tiempo de ejecución como esperaba, este código se ejecuta sin error. Imprime cuatro números. Eso hace que mi ReadOnlyDictionary sea un ReadWriteDictionary.

usuario2023861
fuente
El problema con ese enfoque es que dicho objeto puede pasarse a un método que espera un Dictionary<TKey,TValue>sin ninguna queja del compilador, y emitir o coaccionar una referencia al tipo de diccionario simple eliminará cualquier protección.
supercat
@ Supercat, mierda, tienes razón. Pensé que también tenía una buena solución.
user2023861
Recuerdo hacer una derivada de Dictionarycon un Clonemétodo que encadenado a MemberwiseClone. Desafortunadamente, si bien debería ser posible clonar eficientemente un diccionario clonando los almacenes de respaldo, el hecho de que los almacenes de respaldo sean privatemás que protectedmedios significa que no hay forma de que una clase derivada los clone; usar MemberwiseClonesin clonar los almacenes de respaldo significará que las modificaciones posteriores realizadas al diccionario original romperán el clon, y las modificaciones realizadas al clon romperán el original.
supercat