Универсальный словарь только для чтения на C#


Фреймворк .NET включает несколько типов коллекций, предназначенных для использования в объектных моделях. Среди них есть ReadOnlyCollection, позволяющий создавать коллекции, которые не могут быть изменены. Однако тип Dictionary, предназначенный только для чтения, отсутствует.

Коллекции объектной модели

Фреймворк .NET версий 2.0 и более поздних включает пространство имен System.Collections.ObjectModel. Это пространство имен содержит различные классы коллекций, которые предназначены для использования при создании многократно используемых объектных моделей, например, в библиотеке классов. Эти типы следует предпочесть при создании публичных методов и свойств, возвращающих списки значений или объектов.

Класс ReadOnlyCollection

Одним из интересных классов в пространстве имен System.Collections.ObjectModel является ReadOnlyCollection. Этот класс представляет собой обертку для существующей коллекции, но без членов, которые позволяют добавлять, удалять или изменять элементы. Это полезно при возврате списка значений в вызывающий метод, когда вы не хотите, чтобы коллекция была изменена. NB: Коллекция не может быть изменена, но если она содержит ссылочные типы, то отдельные элементы могут быть изменены.

Чтобы создать экземпляр класса ReadOnlyCollection, необходимо сначала создать другую коллекцию, доступную для записи. Она передается в конструктор объекта ReadOnlyCollection, как показано в следующем примере:

Collection<string> writeable = new Collection<string>();
writeable.Add("Один");
writeable.Add("Два");
writeable.Add("Три");
 
ReadOnlyCollection<string> readOnly = new ReadOnlyCollection<string>(writeable); К сожалению, в .NET framework не предусмотрен класс словаря или хэш-таблицы, доступных только для чтения. В этой статье мы реализуем такой словарь в виде общего класса "ReadOnlyDictionary".

Требования к ReadOnlyDictionary

Класс ReadOnlyDictionary будет вести себя так же, как и общий класс ReadOnlyCollection. Основное отличие будет заключаться в том, что класс будет оберткой для словаря, содержащего пары ключ/значение, а не для простого списка значений. Класс будет отвечать следующим требованиям:

  • Словарь будет содержать пары ключ/значение, где и ключ, и значение являются общими типами, что позволяет хранить любой тип ключа и значения.
  • Добавление, удаление или замена элементов коллекции невозможны. Изменения отдельных элементов коллекции будут разрешены, если это позволяют их типы. Эти изменения будут отражены и в базовом словаре.
  • Класс ReadOnlyDictionary будет реализовывать общий интерфейс IDictionary, чтобы его можно было использовать везде, где ожидается наличие объекта IDictionary. Чтобы реализовать интерфейс IDictionary, словарь только для чтения также будет реализовывать общие интерфейсы ICollection и IEnumerable, а также нестандартный интерфейс IEnumerable.
Создание проекта ReadOnlyDictionary

Класс ReadOnlyDictionary, описанный в этой статье, может быть создан в проекте библиотеки классов или в любом другом проекте C#, в зависимости от ваших требований.

Создание класса ReadOnlyDictionary

Чтобы создать определение класса, измените имя класса по умолчанию в проекте на "ReadOnlyDictionary" и измените объявление для класса следующим образом:

public sealed class ReadOnlyDictionary<TKey, TValue> : IDictionary<TKey,TValue>
{
}
В приведенном выше объявлении класс определен как закрытый, чтобы предотвратить его создание подкласса с типом, который позволял бы изменять содержимое словаря. Если вы предпочитаете разрешить другим классам наследовать от нового типа словаря, удалите ключевое слово sealed.

Словарь, доступный только для чтения, использует два универсальных типа. Первый, с именем "TKey", определяет тип ключа для каждого элемента в коллекции. Второй, "TValue", определяет тип каждого значения. Фактические типы, используемые для экземпляра ReadOnlyDictionary, будут определены при объявлении объекта.

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

Класс будет использовать функциональность из нескольких пространств имен. Чтобы упростить синтаксис, убедитесь, что вы включили следующие директивы using в начало файла кода класса:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
Объявление базового словаря

Класс ReadOnlyDictionary будет предоставлять оболочку для любого класса словаря, который реализует общий интерфейс IDictionary. Чтобы разрешить это, нам нужно создать закрытое поле, содержащее ссылку на этот словарь. Для этого добавьте следующее объявление в блок кода класса.

private IDictionary<TKey, TValue> _dictionary;
Создаем конструктор

Как и в случае с ReadOnlyCollection, класс ReadOnlyDictionary будет содержать один конструктор. Этот конструктор будет принимать параметр, содержащий словарь, который будет использоваться в качестве источника элементов коллекции. Поскольку мы не знаем точно, какой тип словаря будет предоставлен, этот параметр будет принимать любой объект, реализующий IDictionary.

public ReadOnlyDictionary(IDictionary<TKey, TValue> source)
{
    _dictionary = source;
}
ПРИМЕЧАНИЕ: Чтобы сделать пример кода как можно более простым, код проверки не включен. Вы должны добавить проверку по мере необходимости. Например, вы можете захотеть добавить проверку, чтобы убедиться, что исходный словарь не является пустым.

Добавление итератора

Все классы словарей реализуют перечислитель, позволяющий перебирать элементы коллекции в циклах foreach. Для словаря, доступного только для чтения, мы реализуем два итератора в C# 2.0 для поддержки общей и необщей версий интерфейсов IEnumerable.

Для добавления первого перечислителя мы будем использовать ключевое слово yield в цикле перебора элементов базовой коллекции. Каждая итерация цикла будет выдавать KeyValuePair. Добавьте следующий метод:

public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
    foreach (KeyValuePair<TKey, TValue> item in _dictionary)
    {
        yield return item;
    }
}
Второй итератор завершит реализацию интерфейса IEnumerable и будет реализован явно. Этот итератор просто возвращает значение предыдущего.

IEnumerator IEnumerable.GetEnumerator()
{
    return GetEnumerator();
}
Добавление реализованных методов

Некоторые методы IDictionary будут полностью поддерживаться оберткой ReadOnlyDictionary, поскольку они не изменяют содержимое коллекции. Это метод ContainsKey, метод TryGetValue, метод Contains и метод CopyTo. В каждом случае не требуется никакой дополнительной функциональности по сравнению с методами из базовой коллекции. Чтобы добавить эти методы, добавьте в класс следующий код:

public bool ContainsKey(TKey key)
{
    return _dictionary.ContainsKey(key);
}
 
public bool TryGetValue(TKey key, out TValue value)
{
    return _dictionary.TryGetValue(key, out value);
}
 
public bool Contains(KeyValuePair<TKey, TValue> item)
{
    return _dictionary.Contains(item);
}
 
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
    _dictionary.CopyTo(array, arrayIndex);
}
Добавление индексатора

Интерфейс IDictionary определяет индексатор для всех словарей, чтобы можно было извлекать и изменять значения, используя ключи как ссылки на значения. Однако в словаре ReadOnlyDictionary мы хотим разрешить только чтение значений. Чтобы удовлетворить требования интерфейса и сохранить публичный интерфейс нового класса как можно более элегантным, мы реализуем индексатор как в неявном, так и в явном виде.

Для неявного индексатора, который будет виден для самого ReadOnlyDictionary, будет включен только аксессор get. Для этого индексатора добавьте следующий код:

public TValue this[TKey key]
{
    get { return _dictionary[key]; }
}
Вторая вариация индексатора будет реализована явно для интерфейса IDictionary. Он будет включать в себя аксессоры get и set. Однако, поскольку мы не хотим позволять устанавливать значение, аксессуар set будет просто выбрасывать NotSupportedException при вызове.

TValue IDictionary<TKey, TValue>.this[TKey key]
{
    get { return this[key]; }
    set { throw new NotSupportedException(); }
}
Свойства Keys и Values

Класс IDictionary включает в себя еще несколько свойств, которые должны быть реализованы. Первые два из них возвращают полный список либо ключей, либо значений из словаря в коллекции, реализующей интерфейс ICollection. Для обернутого словаря эти две коллекции могут быть получены, но не доступны только для чтения.

Чтобы преобразовать свойства Keys и Values базового словаря в доступные только для чтения, мы должны обернуть их в объекты ReadOnlyCollection. Однако напрямую обернуть объект ICollection в коллекцию, доступную только для чтения, невозможно; мы должны выполнить два преобразования. Сначала коллекция преобразуется в любой тип, поддерживающий интерфейс IList. Затем этот список можно обернуть. На промежуточном этапе мы будем использовать общие объекты List.

public ICollection<TKey> Keys
{
    get
    {
        ReadOnlyCollection<TKey> keys = new ReadOnlyCollection<TKey>(
            new List<TKey>(_dictionary.Keys));
        return (ICollection<TKey>)keys;
    }
}
 
public ICollection<TValue> Values
{
    get
    {
        ReadOnlyCollection<TValue> values = new ReadOnlyCollection<TValue>(
            new List<TValue>(_dictionary.Values));
        return (ICollection<TValue>)values;
    }
}
Свойство Count

Последнее свойство IDictionary, которое будет получено непосредственно из обернутого словаря, - это свойство Count. Оно просто возвращает количество элементов в коллекции. Чтобы добавить это свойство, добавьте следующий код:

public int Count
{
    get { return _dictionary.Count; }
}
Свойство IsReadOnly

Чтобы завершить работу с членами класса, которые реализуются непосредственно ReadOnlyDictionary, нам нужно добавить свойство IsReadOnly. Оно возвращает булево значение, которое указывает, является ли словарь доступным только для чтения или для записи. Для нового словаря возвращаемое значение всегда будет равно true.

public bool IsReadOnly
{
    get { return true; }
}
Реализация IDictionary<> и ICollection<>

Чтобы завершить класс, нам нужно реализовать оставшиеся члены общих интерфейсов IDictionary и ICollection. Каждый из этих членов выполняет некоторую модификацию содержимого ReadOnlyDictionary и поэтому будет выбрасывать исключение. Чтобы завершить класс, добавьте оставшуюся часть кода следующим образом:

void IDictionary<TKey, TValue>.Add(TKey key, TValue value)
{
    throw new NotSupportedException();
}

bool IDictionary<TKey, TValue>.Remove(TKey key)
{
    throw new NotSupportedException();
}

void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
{
    throw new NotSupportedException();
}

bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
{
    throw new NotSupportedException();
}

void ICollection<KeyValuePair<TKey, TValue>>.Clear()
{
    throw new NotSupportedException();
}
Использование ReadOnlyDictionary

Использование ReadOnlyDictionary аналогично использованию встроенной коллекции ReadOnlyCollection. Чтобы создать словарь, доступный только для чтения, мы должны сначала создать словарь, доступный для записи. Затем он оборачивается с помощью конструктора нового класса. Версия примера, приведенного в начале статьи, основанная на словаре, будет выглядеть следующим образом:

Dictionary<int, string> writeable = new Dictionary<int, string>();
writeable.Add(1, "One");
writeable.Add(2, "Два");
writeable.Add(3, "Три");

ReadOnlyDictionary<int, string> readOnly = new ReadOnlyDictionary<int, string>(writeable);
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегизаметки, си шарп

Обсудить или задать вопрос по этой или любой другой теме можно на нашем форуме



Хорда окружности
Простейшая SQL инъекция для чайников
Урок 12. Оператор switch в Java