Pregunta

Voy a devolver una referencia a un diccionario en mi única propiedad leer. ¿Cómo evito que los consumidores cambien mis datos? Si esto fuera un IList yo podría simplemente devolverlo AsReadOnly. ¿Hay algo similar que puedo 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
¿Fue útil?

Solución

Esta es una aplicación sencilla que se ajusta 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");
    }
}

Otros consejos

.NET 4.5

The .NET Framework 4.5 BCL introduces ReadOnlyDictionary<TKey, TValue> (source).

As the .NET Framework 4.5 BCL doesn't include an AsReadOnly for dictionaries, you will need to write your own (if you want it). It would be something like the following, the simplicity of which perhaps highlights why it wasn't a priority for .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 and below

Prior to .NET 4.5, there is no .NET framework class that wraps a Dictionary<TKey, TValue> like the ReadOnlyCollection wraps a List. However, it is not difficult to create one.

Here is an example - there are many others if you Google for ReadOnlyDictionary.

It was announced in the recent BUILD conference that since .NET 4.5, the interface System.Collections.Generic.IReadOnlyDictionary<TKey,TValue> is included. The proof is here (Mono) and here (Microsoft) ;)

Not sure if ReadOnlyDictionary is included too, but at least with the interface it shouldn't be difficult to create now an implementation which exposes an official .NET generic interface :)

Feel free to use my simple wrapper. It does NOT implement IDictionary, so it doesn't have to throw exceptions at runtime for dictionary methods that would change the dictionary. Change methods simply aren't there. I made my own interface for it called 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(); }
}

IsReadOnly on IDictionary<TKey,TValue> is inherited from ICollection<T> (IDictionary<TKey,TValue> extends ICollection<T> as ICollection<KeyValuePair<TKey,TValue>>). It is not used or implemented in any way ( and is in fact "hidden" through the use of explicitly implementing the associated ICollection<T> members ).

There are at least 3 ways I can see to solve the problem:

  1. Implement a custom read only IDictionary<TKey, TValue> and wrap / delegate to an inner dictionary as has been suggested
  2. Return an ICollection<KeyValuePair<TKey, TValue>> set as read only or an IEnumerable<KeyValuePair<TKey, TValue>> depending on the use of the value
  3. Clone the dictionary using the copy constructor .ctor(IDictionary<TKey, TValue>) and return a copy - that way the user is free to do with it as they please and it does not impact on the state of the object hosting the source dictionary. Note that if the dictionary you are cloning contains reference types ( not strings as shown in the example ) you will need to do the copying "manually" and clone the reference types as well.

As an aside; when exposing collections, aim to expose the smallest possible interface - in the example case it should be IDictionary as this allows you to vary the underlying implementation without breaking the public contract that the type exposes.

A read-only dictionary can to some extent be replaced by Func<TKey, TValue> - I usually use this in an API if I only want people performing lookups; it's simple, and in particular, it's simple to replace the backend should you ever wish to. It doesn't provide the list of keys, however; whether that matters depends on what you're doing.

No, but it would be easy to roll your own. IDictionary does define an IsReadOnly property. Just wrap a Dictionary and throw a NotSupportedException from the appropriate methods.

None available in the BCL. However I published a ReadOnlyDictionary (named ImmutableMap) in my BCL Extras Project

In addition to being a fully immutable dictionary, it supports producing a proxy object which implements IDictionary and can be used in any place where IDictionary is taken. It will throw an exception whenever one of the mutating APIs are called

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

You could create a class that only implements a partial implementation of the dictionary, and hides all the add/remove/set functions.

Use a dictionary internally that the external class passes all requests to.

However, since your dictionary is likely holding reference types, there is no way you ca stop the user from setting values on the classes held by the dictionary (unless those classes themselves are read only)

I don't think there's an easy way of doing it...if your dictionary is part of a custom class, you could achieve it with an indexer:

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

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

+1 Great job, Thomas. I took ReadOnlyDictionary one step further.

Much like Dale's solution, I wanted to remove Add(), Clear(), Remove(), etc from IntelliSense. But I wanted my derived objects to implement IDictionary<TKey, TValue>.

Furthermore, I would like the following code to break: (Again, Dale's solution does this too)

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

The Add() line results 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 

The caller can still cast it to IDictionary<TKey, TValue>, but the NotSupportedException will be raised if you try to use the non-read only members (from Thomas's solution).

Anyway, here's my solution for anyone that also wanted this:

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

Now, there are Microsoft Immutable Collections (System.Collections.Immutable). Get them via NuGet.

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

This is a bad solution, see at bottom.

For those still using .NET 4.0 or earlier, I have a class that works just like the one in the accepted answer, but it's much shorter. It extends the existing Dictionary object, overriding (actually hiding) certain members to have them throw an exception when called.

If the caller tries to call Add, Remove, or some other mutating operation that the built-in Dictionary has, the compiler will throw an error. I use the Obsolete attributes to raise these compiler errors. This way, you can replace a Dictionary with this ReadOnlyDictionary and immediately see where any problems might be without having to run your application and waiting for run-time exceptions.

Take a look:

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

This solution has a problem pointed out by @supercat illustrated here:

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

Rather than give a compile-time error like I expected, or a runtime-exception like I hoped, this code runs without error. It prints four numbers. That makes my ReadOnlyDictionary a ReadWriteDictionary.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top