В .NET доступен общий словарь только для чтения?


186

Я возвращаю ссылку на словарь в моем свойстве только для чтения. Как я могу запретить потребителям изменять мои данные? Если бы это было, IListя мог бы просто вернуть его AsReadOnly. Есть ли что-то подобное, что я могу сделать со словарем?

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

4
Должен быть какой-то способ сделать это, иначе не было бы свойства IsReadOnly в IDictionary ... ( msdn.microsoft.com/en-us/library/bb338949.aspx )
Powerlord

2
Многие из концептуальных преимуществ неизменности могут быть получены без его принудительного выполнения. Если это частный проект, рассмотрите дисциплинированный, неформальный метод. Если вы должны предоставить данные потребителю, вам следует серьезно подумать о глубоких копиях. Если учесть, что для неизменяемой коллекции требуется 1) неизменная ссылка на коллекцию 2) предотвращение изменения самой последовательности и 3) предотвращение изменения свойств элементов коллекции и что некоторые из них могут быть нарушены при отражении, принудительное выполнение не практично.
Спраг

27
Начиная с .NET 4.5, существует System.Collections.ObjectModel.ReadOnlyDictionary ^ _ ^
Smartkid

2
Также теперь есть коллекции неизменяемых
VoteCoffee,

Ответы:


156

Вот простая реализация, которая оборачивает словарь:

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 за публикацию полного кода, а не просто ссылки, но мне интересно, какой смысл пустого конструктора в ReadOnlyDictionary? :-)
Сэмюэл Нефф

20
Остерегайтесь этого конструктора. Если вы сделаете эталонную копию переданного словаря, внешняя часть кода может изменить ваш словарь «Только для чтения». Ваш конструктор должен сделать полную, глубокую копию аргумента.
Askheaves

25
@askheaves: Хорошее наблюдение, но на самом деле довольно часто полезно использовать исходную ссылку в типах «Только чтение» - сохраняйте в своей личной переменной исходную и изменяйте ее для внешних потребителей. Например, посмотрите встроенные объекты ReadOnlyObservableCollection или ReadOnlyCollection: Томас предоставил что-то, что работает точно так же, как это присуще .Net Framework. Спасибо Томас! +1
Мэтт ДеКрей

13
@ user420667: как это реализовано, это «только для чтения представление словаря не только для чтения». Некоторый другой код может изменить содержимое исходного словаря, и эти изменения будут отражены в словаре только для чтения. Это может быть желаемое поведение или нет, в зависимости от того, чего вы хотите достичь ...
Томас Левеск

6
@ Томас: Это то же самое, что и ReadOnlyCollection в .NET BCL. Это доступное только для чтения представление о возможно изменяемой коллекции. ReadOnly не означает неизменяемость и не следует ожидать неизменности.
Джефф Йейтс

229

.NET 4.5

.NET Framework 4.5 BCL представляет ReadOnlyDictionary<TKey, TValue> ( источник ).

Поскольку .NET Framework 4.5 BCL не включает AsReadOnlyсловари 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 и ниже

До .NET 4.5 не было никакого класса .NET Framework, который оборачивает Dictionary<TKey, TValue>как ReadOnlyCollection обертывание List . Тем не менее, это не сложно создать.

Вот пример - есть много других, если вы Google для ReadOnlyDictionary .


7
Не похоже, что они помнили, чтобы сделать AsReadOnly()метод по обычному Dictionary<,>, поэтому мне интересно, сколько людей обнаружат свой новый тип. Однако этот поток переполнения стека поможет.
Джеппе Стиг Нильсен

@Jeppe: Я сомневаюсь, что это как-то связано с запоминанием. Каждая функция стоит, и я сомневаюсь, что AsReadOnly занял первое место в списке приоритетов, тем более что его так легко написать.
Джефф Йейтс

1
Обратите внимание, что это просто оболочка; Изменения в базовом словаре (передаваемом в конструктор) по-прежнему будут изменять словарь только для чтения. См. Также stackoverflow.com/questions/139592/…
TrueWill

1
@JeffYates Учитывая, насколько это просто, написание заняло бы меньше времени, чем принятие решения о том, стоит ли тратить время на его написание. Из-за этого моя ставка на "они забыли".
Дэн Бешард

Как сказал TrueWill, базовый словарь все еще может быть видоизменен. Вы можете рассмотреть возможность передачи клона исходного словаря в конструктор, если вы хотите истинную неизменность (при условии, что ключ и тип значения также неизменны.)
user420667

19

На недавней конференции BUILD было объявлено, что начиная с .NET 4.5, интерфейс System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>включен. Доказательство здесь (моно) и здесь (Microsoft);)

Не уверен, ReadOnlyDictionaryвключен ли он тоже, но, по крайней мере, с интерфейсом теперь не составит труда создать реализацию, которая предоставляет официальный универсальный интерфейс .NET :)


5
ReadOnlyDictionary<TKey, TValue>(.Net 4.5) - msdn.microsoft.com/en-us/library/gg712875.aspx
майерманец

18

Не стесняйтесь использовать мою простую обертку. Он НЕ реализует IDictionary, поэтому он не должен генерировать исключения во время выполнения для методов словаря, которые могли бы изменить словарь. Методы изменения просто отсутствуют. Я сделал свой собственный интерфейс для этого, названный 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 за то, что не нарушил IDictionaryдоговор. Я думаю, что с точки зрения ООП IDictionaryправильнее наследовать IReadOnlyDictionary.
Сэм

@Sam Согласен, и если бы мы могли вернуться, я думаю, что было бы лучше и правильнее иметь IDictionary(для текущего IReadOnlyDictionary) и IMutableDictionary(для текущего IDictionary).
MasterMastic

1
@MasterMastic Это странное предложение. Я не помню никаких других встроенных классов, полагающихся на обратное предположение, что неизменяемая коллекция - это то, что пользователь ожидает по умолчанию.
Дэн Бешард

11

IsReadOnly on IDictionary<TKey,TValue>наследуется от ICollection<T>( IDictionary<TKey,TValue>расширяется ICollection<T>как ICollection<KeyValuePair<TKey,TValue>>). Он не используется и не реализуется каким-либо образом (и фактически является «скрытым» за счет использования явной реализации связаннойICollection<T> членов).

Есть как минимум 3 способа решения проблемы:

  1. Реализация пользовательского только для чтения IDictionary<TKey, TValue>и перенос / делегирование во внутренний словарь, как было предложено
  2. Возврат ICollection<KeyValuePair<TKey, TValue>>набора только для чтения или в IEnumerable<KeyValuePair<TKey, TValue>>зависимости от использования значения
  3. Клонируйте словарь, используя конструктор копирования, .ctor(IDictionary<TKey, TValue>)и верните копию - таким образом, пользователь может делать с ним все, что ему угодно, и это не влияет на состояние объекта, в котором находится исходный словарь. Обратите внимание, что если клонируемый словарь содержит ссылочные типы (а не строки, как показано в примере), вам нужно будет выполнить копирование «вручную» и клонировать ссылочные типы.

Как в сторону; при представлении коллекций стремитесь предоставить наименьший возможный интерфейс - в данном случае это должен быть IDictionary, поскольку это позволяет варьировать базовую реализацию, не нарушая публичный контракт, который предоставляет тип.


8

Доступный только для чтения словарь может быть в некоторой степени заменен на Func<TKey, TValue>- обычно я использую его в API, если мне нужны только люди, выполняющие поиск; это просто и, в частности, просто заменить бэкэнд, если вы когда-нибудь захотите. Однако он не предоставляет список ключей; имеет ли это значение, зависит от того, что вы делаете.


4

Нет, но это было бы легко сделать самостоятельно. IDictionary определяет свойство IsReadOnly. Просто оберните словарь и сгенерируйте NotSupportedException из соответствующих методов.


3

Нет доступных в BCL. Однако я опубликовал ReadOnlyDictionary (названный ImmutableMap) в моем проекте BCL Extras.

Помимо того, что он является полностью неизменным словарем, он поддерживает создание прокси-объекта, который реализует IDictionary и может использоваться в любом месте, где берется IDictionary. Он будет выдавать исключение всякий раз, когда вызывается один из мутирующих API.

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

9
Ваша ImmutableMap реализована в виде сбалансированного дерева. Поскольку в .NET люди, как правило, ожидают, что «словарь» будет реализован с помощью хеширования, и будут демонстрировать соответствующие свойства сложности, вы можете быть осторожны с продвижением ImmutableMap в качестве «словаря».
Гленн Слэйден

Похоже, что сайты code.msdn.com не существует. BCLextras теперь здесь github.com/scottwis/tiny/tree/master/third-party/BclExtras
BozoJoe

1

Вы можете создать класс, который реализует только частичную реализацию словаря и скрывает все функции добавления / удаления / установки.

Используйте внутренний словарь, к которому внешний класс передает все запросы.

Однако, поскольку ваш словарь, скорее всего, содержит ссылочные типы, вы не сможете остановить пользователя от установки значений для классов, содержащихся в словаре (если только эти классы не предназначены только для чтения)


1

Я не думаю, что есть простой способ сделать это ... если ваш словарь является частью пользовательского класса, вы можете достичь этого с помощью индексатора:

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

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

Мне нужно иметь возможность раскрыть весь словарь, а также индексатор.
Роб Соберс

Это кажется очень хорошим решением. Однако клиентам класса MyClass может потребоваться больше узнать о словаре, например, для его итерации. А что, если ключ не существует (может быть, хорошая идея TryGetValue () в какой-то форме)? Можете ли вы сделать свой ответ и пример кода более полными?
Питер Мортенсен

1

+1 Отличная работа, Томас. Я сделал ReadOnlyDictionary еще на один шаг вперед.

Многое , как решение Дэйла, я хотел удалить Add(), Clear(), Remove()и т.д. от IntelliSense. Но я хотел, чтобы мои производные объекты реализовалиIDictionary<TKey, TValue> .

Кроме того, я хотел бы, чтобы следующий код сломался: (Опять же, решение Дейла делает это тоже)

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

Строка Add () приводит к:

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

Вызывающий все еще может использовать его IDictionary<TKey, TValue>, ноNotSupportedException он будет поднят, если вы попытаетесь использовать не только для чтения участников (из решения Томаса).

Во всяком случае, вот мое решение для тех, кто также хотел это:

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
Или вы можете сделать:public IEnumerable<KeyValuePair<string, string>> MyDictionary() { return _mydictionary; }
Пэт

0

Это плохое решение, смотрите внизу.

Для тех, кто все еще использует .NET 4.0 или более раннюю версию, у меня есть класс, который работает так же, как и в принятом ответе, но он намного короче. Он расширяет существующий объект Dictionary, переопределяя (фактически скрывая) определенные члены, чтобы они вызывали исключение при вызове.

Если вызывающая сторона пытается вызвать Add, Remove или какую-либо другую мутирующую операцию, имеющуюся во встроенном словаре, компилятор выдаст ошибку. Я использую устаревшие атрибуты, чтобы вызвать эти ошибки компилятора. Таким образом, вы можете заменить Dictionary на этот ReadOnlyDictionary и сразу увидеть, где могут быть какие-либо проблемы, без необходимости запуска приложения и ожидания исключений во время выполнения.

Взглянуть:

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

У этого решения есть проблема, указанная @supercat, показанной здесь:

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

Вместо того, чтобы выдавать ошибку времени компиляции, как я ожидал, или исключение времени выполнения, как я надеялся, этот код выполняется без ошибок. Он печатает четыре номера. Это делает мой ReadOnlyDictionary ReadWriteDictionary.


Проблема с этим подходом состоит в том, что такой объект может быть передан методу, который ожидает, Dictionary<TKey,TValue>без каких-либо жалоб компилятора, и приведение или приведение ссылки к типу простого словаря снимет любые защиты.
суперкат

@supercat, дерьмо, ты прав. Я думал, что у меня тоже есть хорошее решение.
user2023861

Я не забываю делать производные Dictionaryс Cloneметодом, который прикован к MemberwiseClone. К сожалению, хотя должна быть возможность эффективно клонировать словарь путем клонирования резервных хранилищ, тот факт, что резервные хранилища, privateа не protectedозначает, что у производного класса нет возможности клонировать их; Использование MemberwiseCloneбез клонирования резервных хранилищ будет означать, что последующие модификации, сделанные в исходном словаре, будут ломать клон, а модификации, сделанные в клоне, будут ломать оригинал.
суперкат
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.