 .NET 2.0+A Generic Read-Only Dictionary
The .NET framework includes several types of collection that are designed for use in object models. Amongst these is the ReadOnlyCollection that allows the creation of collections that may not be modified. However, there is no read-only Dictionary type.
Object Model Collections
The .NET framework versions 2.0 and later include the System.Collections.ObjectModel namespace. This namespace contains various collection classes that are designed to be used when creating reusable object models, such as those held in a class library. These types should be the preferred choices when creating public methods and properties that return lists of values or objects.
ReadOnlyCollection Class
One of the interesting classes in the System.Collections.ObjectModel namespace is the ReadOnlyCollection. This class provides a wrapper to an existing collection but without the members that permit you to add, remove or modify items. This is useful when returning a list of values to a calling method where you do not wish for the collection to be changed. NB: The collection may not be modified but if it contains reference types, the individual items may be.
To create an instance of the ReadOnlyCollection class, you must first create another, writeable collection. This is passed to the constructor of the ReadOnlyCollection object, as in the following sample code:
Collection<string> writeable = new Collection<string>();
writeable.Add("One");
writeable.Add("Two");
writeable.Add("Three");
ReadOnlyCollection<string> readOnly = new ReadOnlyCollection<string>(writeable);
Unfortunately, the .NET framework does not provide a read-only dictionary or hash table class. In this article we will implement such a dictionary as a generic, "ReadOnlyDictionary" class.
ReadOnlyDictionary Requirements
The ReadOnlyDictionary class will behave in a similar manner to the generic ReadOnlyCollection class. The key difference will be that the class will be a wrapper for a dictionary containing key / value pairs, rather than for a simple list of values. The class will meet the following requirements:
- The dictionary will contain key / value pairs where both the key and value are generic types, permitting any type of key and value to be held.
- It will not be possible to add, remove or replace items in the collection. Changes to the individual items in the collection will be permitted if their types permit. These changes will be reflected in the underlying dictionary also.
- The ReadOnlyDictionary class will implement the generic IDictionary interface so that it may be used wherever an IDictionary object is expected. In order to implement the IDictionary interface, the ReadOnlyDictionary will also implement the generic ICollection and IEnumerable interfaces and the non-generic IEnumerable interface.
Creating the ReadOnlyDictionary Project
The ReadOnlyDictionary class described in this article can be created in a class library project or in any other type of C# project, depending upon your requirements. The code that can be downloaded from the link at the top of the page uses a class library project, named "ReadOnlyDictionaryDemo".
Creating the ReadOnlyDictionary Class
To create the class definition, change the name of the default class in the project to "ReadOnlyDictionary" and modify the declaration for the class as follows:
public sealed class ReadOnlyDictionary<TKey, TValue> : IDictionary<TKey,TValue>
{
}
In the above declaration, the class is defined as sealed to prevent it being subclassed with a type that would permit changes to the dictionary's contents. If you prefer to allow other classes to inherit from the new dictionary type, remove the sealed keyword.
The read-only dictionary uses two generic types. The first, named "TKey", defines the type of the key for each element in the collection. The second, "TValue", defines the type of each value. The actual types used for an instance of the ReadOnlyDictionary will be determined when the object is declared.
The final part of the declaration indicates that the new class will implement the generic IDictionary interface, using the same types for the keys and values. This will allow the ReadOnlyDictionary to be used wherever an IDictionary is expected, assuming that it will not require that the dictionary be modified.
The class will use functionality from several namespaces. To allow a simplified syntax, ensure that you include the following using directives at the top of the class' code file:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
Declaring the Underlying Dictionary
The ReadOnlyDictionary class will provide a wrapper to any dictionary class that implements the generic IDictionary interface. To permit this, we need to create a private field to contain a reference to that dictionary. To do so, add the following declaration within the class' code block.
private IDictionary<TKey, TValue> _dictionary;
Creating the Constructor
As with the ReadOnlyCollection, the ReadOnlyDictionary class will include a single constructor. This constructor will accept a parameter containing the dictionary to be used as the source of the collection's items. As we do not know exactly which type of dictionary will be provided, this parameter will accept any object that implements IDictionary.
public ReadOnlyDictionary(IDictionary<TKey, TValue> source)
{
_dictionary = source;
}
NB: To keep the example code as simple as possible, no validation code is included. You should add validation as required. For example, you may wish to add a check to ensure that the source dictionary is not null.
Adding an Iterator
All dictionary classes implement an enumerator to allow the items in the collection to be traversed within foreach loops. For the read-only dictionary, we will implement two C# 2.0 iterators to support the generic and non-generic versions of the IEnumerable interfaces.
To add the first enumerator we will use the yield keyword within a loop through the items in the underlying collection. Each iteration of the loop will yield a KeyValuePair. Add the following method:
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
foreach (KeyValuePair<TKey, TValue> item in _dictionary)
{
yield return item;
}
}
The second iterator will complete the implementation of the IEnumerable interface and will be implemented explicitly. This iterator simply returns the value of the previous one.
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
Adding the Implemented Methods
Some of the IDictionary methods will be fully supported by the ReadOnlyDictionary wrapper because they do not change the contents of the collection. These are the ContainsKey method, the TryGetValue method, the Contains method and the CopyTo method. In each case, no additional functionality over the methods from the underlying collection is required. To add the methods, add the following code to the class:
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);
}
Adding an Indexer
The IDictionary interface defines an indexer for all dictionaries so that values can be retrieved and changed using the keys as references to the values. However, in the ReadOnlyDictionary we only want to permit reading of the values. To satisfy the requirements of the interface and to keep the public interface of the new class as elegant as possible, we will implement the indexer both implicitly and explicitly.
For the implicit indexer, which will be visible for the ReadOnlyDictionary itself, only the get accessor will be included. For this indexer, add the following code:
public TValue this[TKey key]
{
get
{
return _dictionary[key];
}
}
The second variation upon the indexer will be implemented explicitly for the IDictionary interface. This will include both get and set accessors. However, as we do not wish to allow a value to be set, the set accessor will simply throw a NotSupportedException when called.
TValue IDictionary<TKey, TValue>.this[TKey key]
{
get
{
return this[key];
}
set
{
throw new NotSupportedException();
}
}
Keys and Values Properties
The IDictionary class includes several other properties that must be implemented. The first two of these return a full list of either the keys or the values from the dictionary in a collection that implements the ICollection interface. For the wrapped dictionary, these two collections can be retrieved but are not read-only.
To convert the Keys and Values properties of the underlying dictionary to be read-only, we must wrap them in ReadOnlyCollection objects. However, it is not possible to directly wrap an ICollection object in a read-only collection directly; we must perform two conversions. Firstly the collection is converted to any type that supports the IList interface. This list may then be wrapped. We will use generic List objects in the interim step.
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 Property
The final IDictionary property that will be retrieved directly from the wrapped dictionary is the Count property. This simply returns the number of items in the collection. To add this property, add the following code:
public int Count
{
get
{
return _dictionary.Count;
}
}
IsReadOnly Property
To complete the class members that are implemented by the ReadOnlyDictionary directly, we need to add the IsReadOnly property. This returns a Boolean value that indicates whether a dictionary is read-only or writeable. For the new dictionary, the return value will always be true.
public bool IsReadOnly
{
get { return true; }
}
Implementing IDictionary<> and ICollection<>
To complete the class, we now need to implement the remaining members from the IDictionary and ICollection generic interfaces. Each of these members performs some modification of the contents of the ReadOnlyDictionary and therefore will throw an exception. To complete the class, add the remainder of the code as follows:
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();
}
Using the ReadOnlyDictionary
The use of the ReadOnlyDictionary is similar to that of the in-built ReadOnlyCollection. To create a read-only dictionary we must first create a writeable one. This is then wrapped using the constructor of the new class. A dictionary-based version of the example at the beginning of this article would be as follows:
Dictionary<int, string> writeable = new Dictionary<int, string>();
writeable.Add(1, "One");
writeable.Add(2, "Two");
writeable.Add(3, "Three");
ReadOnlyDictionary<int, string> readOnly
= new ReadOnlyDictionary<int, string>(writeable);
|