Чи є в .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) запобігання зміні властивостей елементів колекції і що деякі з них можуть бути порушені за допомогою відображення, виконання часу виконання не практичний.
Sprague

27
Оскільки .NET 4.5, існує System.Collections.ObjectModel.ReadOnlyDictionary ^ _ ^
Smartkid

2
Зараз також є Microsoft Immutable Collections через NuGet msdn.microsoft.com/en-us/library/dn385366%28v=vs.110%29.aspx
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: Thomas надав щось, що працює точно так само, як і властиве рамці .Net. Спасибі Томасе! +1
Метт Декре

13
@ user420667: як це реалізовано, це "перегляд лише словника, який не читає". Деякі інші коди можуть змінити вміст оригінального словника, і ці зміни будуть відображені в словнику лише для читання. Це може бути бажана поведінка, чи ні, залежно від того, чого ви хочете досягти ...
Томас Левеск

6
@Thomas: Це те саме, що ReadOnlyCollection в .NET BCL. Це перегляд лише колекції, що можливо змінюється. ReadOnly не означає незмінності, і незмінності не слід очікувати.
Jeff Yates

229

.NET 4.5

Представлено .NET Framework 4.5 BCL ReadOnlyDictionary<TKey, TValue>( джерело ).

Оскільки .NET Framework 4.5 BCL не включає AsReadOnlyсловники, вам потрібно буде написати свій власний (якщо ви цього хочете). Це буде щось на кшталт наступного, простота якого, можливо, підкреслює, чому це не було пріоритетом для .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, який Dictionary<TKey, TValue>би перетворював такий, як ReadOnlyCollection, обгортає список . Однак створити її не важко.

Ось приклад - якщо Google для ReadOnlyDictionary є багато інших .


7
Не схоже, що вони запам'ятали зробити AsReadOnly()метод звичайним Dictionary<,>, тому мені цікаво, скільки людей відкриє свій новий тип. Однак ця нитка переповнення стека допоможе.
Jeppe Stig Nielsen

@Jeppe: Сумніваюсь, це має щось спільне з запам'ятовуванням. Кожна функція коштує, і я сумніваюся, що AsReadOnly опинився в списку пріоритетів, тим більше, що писати так просто.
Джефф Йейтс

1
Зауважте, що це просто обгортка; Зміни в базовому словнику (той, який передається конструктору) все ще буде вимкнути словник, доступний лише для читання. Дивіться також stackoverflow.com/questions/139592 / ...
TrueWill

1
@JeffYates З огляду на те, наскільки це просто, написання цього зайняло б менше часу, ніж вирішити, витрачати час на його написання чи ні. Через це моя ставка на "вони забули".
Ден Бешард

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

19

На недавній конференції BUILD було оголошено, що з .NET 4.5 інтерфейс System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>включений. Доказ є тут (Mono) і тут (Microsoft);)

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


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 у 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

У БКЛ немає. Однак я опублікував ReadOnlyDictionary (названий ImmutableMap) у своєму проекті BCS 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 або раніше, у мене є клас, який працює так само, як і у прийнятій відповіді, але він набагато коротший. Він розширює існуючий об’єкт словника, перекриваючи (фактично приховуючи) певних членів, щоб вони викидали виняток, коли викликаються.

Якщо абонент намагається зателефонувати Додати, Видалити або якусь іншу мутаційну операцію, яку має вбудований словник, компілятор видасть помилку. Я використовую застарілі атрибути, щоб підняти ці помилки компілятора. Таким чином, ви можете замінити словник цим 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

@supercat, лайно, ти маєш рацію. Я подумав, що і у мене є гарне рішення.
користувач2023861

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