Implementing a Collection Using .NET
Implementing a Collection Using .NET
Common Behaviors of Collections
Introduction
Although you can create a collection class from nothing, most collections classes have some fundamental behaviors and characteristics. To give a skeleton of these default requirements, the .NET Framework provides a few interfaces that you can implement. For example the System.Collections.IComparer or the System.Collections.Generic.IComparer interfaces allow you to define how two objects would be compared for similarity or difference.
The System.Collections.IDictionary or the System.Collections.Generic.IDictionary interface can be used to create a collection class where each item is made of a key=value combination.
Practical Learning: Introducing .NET Framework Collections
Introduction to the ICollection Interface
One of the primary pieces of information you should provide about the values in a collection is the number of values that a list is (currently) holding. When creating a collection class, to prepare it to provide this valuable information, you can (should) implement an interface named ICollection. The ICollection interface is defined in the System.Collections namespace. The ICollection interface derives from the IEnumerable initerface:
public interface ICollection : IEnumerable
The generic equivalent of the ICollection interface is defined in the System.Collections.Generic namespace. The ICollection<> interface inherits from the IEnumerable<> and the IEnumerable interfaces:
public interface ICollection<T> : IEnumerable<T>, IEnumerable
Therefore, to start a collection class, you can creating implement one of these interfaces. Here is an example for the System.Collections.ICollection<> interface:
public class Collection<T> : ICollection<T>
{
}
Thanks to the flexibility of arrays in the .NET Framework, you can create the items of the collection as an array and give it an initial size. Here is an example:
public class Collection<T> : ICollection<T> { private int size; private T[] items; public Collection() { size = 0; items = new T[10]; } }
Implementing ICollection
To assist you with keeping track of the number of items in a collection, the ICollection<T> interface is equipped with an integral property named Count, which you must implement. To do this, you can create a private member variable that will actually keep a count of the number of items. The Count property can then be used to communicate this information to the clients of the class. Here is an example:
public class Collection<T> : ICollection<T>
{
private int size;
public Collection()
{
size = 0;
}
public virtual int Count
{
get { return size; }
}
}
The ICollection<> interface also allows its implementer to copy some of its items to an array. To provide this functionality, the interface is equipped with a method named CopyTo, which you must implement. The syntax of this method is:
void CopyTo(T[] array, int arrayIndex);
This method takes two arguments. The first argument is the array that will receive the items. The second argument is the index of the item from where the copying operation will begin. Here is an example:
public class Collection<T> : ICollection<T>
{
private int size;
private T[] items;
public Collection()
{
size = 0;
items = new T[10];
}
public virtual int Count
{
get { return size; }
}
public void CopyTo(T[] array, int index)
{
T[] values = new T[size];
for (int i = 0; i < size; i++)
values[i] = items[i];
array = values;
}
}
The System.Collections.ICollection (and the System.Collections.Generic) interface extends the IEnumerable<> interface. This means that you should be able to use foreach in your ICollection<>-based class but you must create the functionality yourself, which is done by implementing the GetEnumerator() method. Because the ICollection<> interface inherits from IEnumerable<> that itself inherits from IEnumerable, you must implement two versions of the GetEnumerator() methods. As we saw in the previous lesson, their syntaxes are:
IEnumerator<T> GetEnumerator(); IEnumerator GetEnumerator();
Even if you don't want to support this feature, you still must provide at least a skeleton for this method. Here is an example:
public class Collection<T> : ICollection<T>
{
private int size;
private T[] items;
public Collection()
{
size = 0;
items = new T[10];
}
public virtual int Count
{
get { return size; }
}
public void CopyTo(T[] array, int index)
{
T[] values = new T[size];
for (int i = 0; i < size; i++)
values[i] = items[i];
array = values;
}
public IEnumerator<T> GetEnumerator()
{
int counter = 0;
while (counter < Count)
{
yield return items[counter];
counter++;
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
int counter = 0;
while (counter < Count)
{
yield return items[counter];
counter++;
}
}
}
The IList Interface
Introduction
While it provides the minimum functionality of a collection, the System.Collections.Generic.ICollection<> (and the System.Collections.ICollection) interface(s) is(are) not equipped to perform the regular operations of a collection class, such as adding, retrieving, or deleting items from a set.
To assist you with creating a collection class as complete as possible, the .NET Framework provides the IList<> (and the IList) interface. The IList<> interface is defined in the System.Collections.Generic namespace and its non-generic equivalent of the same name is defined in the System.Collections namespace. The interface is equipped with the methods necessary to add, insert, delete, or retrieve items from a collection. Because the functionalities of these methods may not suited you, to use these features, you must create a class that implements them.
Implementing IList
This System.Collections.IList interface is declared as follows:
public interface IList : ICollection, IEnumerable
This System.Collections.Generic.IList<> generic interface is declared as follows:
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
As mentioned above, to create a collection, you can implement the IList<> generic interface. Here is an example:
using System.Collections.Generic; public class Collection<T> : IList<T> { }
This means that the IList<> interface extends the ICollection<>, the IEnumerable<>, and the IEnumerable interfaces. This also implies that you must implement the members of these parent interfaces, including the Count and the IsReadOnly properties, the CopyTo() and the GetEnumerator() methods. From what we learned with ICollection, here are examples of implementing these members for the System.Collections.IList interface:
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
public Collection()
{
size = 0;
items = new T[10];
}
public void CopyTo(T[] array, int arrayIndex)
{
T[] values = new T[size];
for (int i = 0; i < size; i++)
values[i] = items[i];
array = values;
}
public int Count
{
get { return size; }
}
public IEnumerator<T> GetEnumerator()
{
int counter = 0;
while (counter < Count)
{
yield return items[counter];
counter++;
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
int counter = 0;
while (counter < Count)
{
yield return items[counter];
counter++;
}
}
}
Populating the Collection
A Read-Only Collection
Most collections are made to receive new values. If you want, you can create a list that cannot receive new values. To support this, the IList interface is equipped with the Boolean IsReadOnly property. If a list is read-only, it would prevent the clients from adding items to it.
Here is an example of implementing the IsReadOnly property for the System.Collections.IList interface:
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
public Collection()
{
size = 0;
items = new T[10];
}
public void CopyTo(T[] array, int arrayIndex)
{
T[] values = new T[size];
for (int i = 0; i < size; i++)
values[i] = items[i];
array = values;
}
public int Count
{
get { return size; }
}
public bool IsReadOnly
{
get { return false; }
}
public IEnumerator<T> GetEnumerator()
{
int counter = 0;
while (counter < Count)
{
yield return items[counter];
counter++;
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
int counter = 0;
while (counter < Count)
{
yield return items[counter];
counter++;
}
}
}
Adding an Item
As it should be obvious, the primary operation to perform on a list is to populate it with at least one value. To support this, the IList<> interface is equipped with a method named Add. Its syntax is:
void Add(T item);
This method takes one argument as the value to add to the collection. If your collection is an array, you can first check that there is still enough room in the list to add a new item. In reality, this is never an issue with the System.Collections.IList interface:
We saw already (from the previous lesson) that the Add() method can be defined as follows::
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
. . .
public int Add(double item)
{
this.items[this.size] = item;
this.size++;
return size;
}
}
By default, an array has a fixed size. If you try adding an item in a position higher than the maximum number of items, the compiler would throw an IndexOutOfRangeException. Fortunately, the Array class is equipped with a method named Resize. This allows you to increase the size of an array when you judge it necessary. As a consequence, you can have an array as large as possible. Based on this, you can implement the Add() method as follows:
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
. . .
private void CheckAndIncreaseIfNecessary()
{
if (size >= items.Length)
Array.Resize<T>(ref items, items.Length + 5);
}
public void Add(T value)
{
// Check whether there is still room in the array.
// If there is not enough room, then increase the capacity
CheckAndIncreaseIfNecessary();
// Add the item at the end
this.items[this.size] = value;
// Increase the current number of items
this.size++;
}
}
Inserting an Item
When you call the Add() method, it adds the new value to the end of the list. Sometimes, you will want the new value to be inserted somewhere inside the list. To support this operation, both the System.Collections.IList and the System.Collections.Generic.IList interfaces provide a method named Insert. The syntax of the System.Collections.IList.Insert() method is:
void Insert(int index, object value);
The syntax of the System.Collections.Generic.IList.Insert() method is:
void Insert(int index, T value);
This method takes two arguments. The second argument is the value that will be inserted into the list. The argument must hold a valid value. Because the System.Collections.IList.Insert() method takes an object value, if your collection is using a different type of value, you may have to cast it to object. The first argument is the index of the item that will precede the new one.
As mentioned for the System.Collections.IList.Add() method, there are a few things you should know about this operation's success or lack of it:
The System.Collections.Generic.IList.Insert() method can be implemented as follows:
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
. . .
private void CheckAndIncreaseIfNecessary()
{
if (size >= items.Length)
Array.Resize<T>(ref items, items.Length + 5);
}
public void Add(T value)
{
// Check whether there is still room in the array.
// If there is not enough room, then increase the capacity
CheckAndIncreaseIfNecessary();
// Add the item at the end
this.items[this.size] = value;
// Increase the current number of items
this.size++;
}
public void Insert(int index, T value)
{
// If the index is the same as the current number of items
// then call Add() and proceed as if we want to add a new item
if (index >= size)
{
Add(value);
return;
}
// If the index is positive but less than the current size,
// then you need to insert the item inside the collection.
if ((index >= 0) && (index < size))
{
// Firs, push each item one position up to create
// an empty space
for (int i = (size - 1); i > index - 1; i--)
items[i + 1] = items[i];
// Then put the new item in the indicated position
items[index] = value;
// Since the new item has been added, increase the
// current number of items
size++;
}
}
When calling the IList<>.Insert() method, if you pass an invalid index, the compiler would throw an ArgumentOutOfRangeException.
Locating an Item in the Collection
this Item of a Collection
While using a list, various operations require that you know the object you are currently accessing. To provide this operation, IList and the IList<> interfaces are equipped with an indexed property named Item that you must implement. The Item property of the System.Collection.Generic.IList interface is declared as follows:
T this[int index] { get; set; }
If you implement it right, this property can perform one of three operations:
Here is an example of implementing the indexed property of the IList<> interface:
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
. . .
// The index property can be used to add an item to a collection,
// or to retrieve an item from the collection
public T this[int index]
{
get
{
return this.items[index];
}
set
{
this.items[index] = value;
}
}
}
After creating this property, you can then access an item using its index and applying the [] operator on its instance.
Checking the Existence of an Item
One of the routine operations you can perform on a list is to find out whether it contains a certain value. To assist you with this operation, the IList<> interface is equipped with a method named Contains. Its syntax is:
bool Contains(T item);
This method takes as argument the value to look for. If the value is found in the collection, the method returns true. If no value is found in the collection, this method returns false.
Here is an example of implementing this method:
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
. . .
public bool Contains(T value)
{
for (int i = 0; i < Count; i++)
if (items[i]!.Equals(value))
return true;
return false;
}
}
This method calls the Equals() method of the objects that make up the list to find out whether the value argument exists in the collection. If this method produces a wrong result, especially if you are using your own class to represent the item, you should override the Equals() method.
Getting the Index of an Item
The Contains() method is used to check whether a particular value (already) exists in the collection. If you know that a certain item exists but you don't know its index inside the list, the IList<> interface can assist you through a method named IndexOf. Its syntax is:
int IndexOf(T item);
This method takes as argument the value to look for in the list. If the value is found in the collection, the method returns its index. If there is no value defined like that, the method returns -1. Here is an example of implementing this method:
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
. . .
public int IndexOf(T value)
{
for (int i = 0; i < Count; i++)
if (items[i]!.Equals(value))
return i;
return -1;
}
}
Once again, this method calls the Equals() method of the objects that make up the collection. In most cases, you should make sure you override the Equals() method in the class that T represents.
Deleting Items
Deleting a Value by its Index
If a value is not necessary in your collection, you can delete it. Probably the simplest way to delete a value is to specify its position in the list. To support this operation, both the System.Collections.IList and the System.Collections.Generic.IList interfaces are equipped with a method named RemoveAt. The syntax of the RemoveAt() method is is the same for both interfaces and it is:
void RemoveAt(int index);
This method takes as argument the index of the value to be removed. If the index is valid, the method removes it. If the index is not valid, the compiler will throw and ArgumentOutOfRangeException. Here is an example:
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
. . .
// This method is used to delete an item based on its position
public void RemoveAt(int index)
{
// First check that the user provided a positive index that
// is lower than the current total number of items
if ((index >= 0) && (index < size))
{
// If so, starting at that index, get the item ahead of
// each position and assign it to the current item
for (int i = index; i < size - 1; i++)
items[i] = items[i + 1];
// Since the last item is not useful anymore,
// decrease the number of items
size--;
}
}
}
If the item cannot be deleted for another reason, the compiler would throw an NotSupportedException.
Deleting an Item by its Value
The problem with deleting a value based on its index is that, if you are not sure, you could delete the wrong value. An alternative is to first precisely define the value you want to get rid of, and then hand the value itself to the compiler that would remove it. To support this approach, the System.Collections.IList interface is equipped with a method named Remove() and whose syntax is:
bool Remove(T item);
This method takes as argument the value to be deleted. This means that the compiler will first look for the value in the list. If it finds that value, it removes it. If there is no value like that, nothing happens. Here is an example of implementing this method:
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
. . .
// This method is used to delete an item
public bool Remove(T value)
{
// First try to get the index of the item to find out if it exists
int index = IndexOf(value);
// If the item was found ...
if (index >= 0)
{
// ... delete it, using its index
RemoveAt(index);
// Since the item has been removed, return true
return true;
}
// If the item was not removed (for any reason), return false
return false;
}
}
Clearing a Collection
To remove all values from a list at once, you can implement Clear() method of the IList<> interface. Its syntax is:
void Clear();
Here is an example of implementing it:
public class Collection<T> : IList<T> { private int size; private T[] items; public Collection() { size = 0; items = new T[10]; } public void CopyTo(T[] array, int arrayIndex) { T[] values = new T[size]; for (int i = 0; i < size; i++) values[i] = items[i]; array = values; } public int Count { get { return size; } } public bool IsReadOnly { get { return false; } } private void CheckAndIncreaseIfNecessary() { if (size >= items.Length) Array.Resize<T>(ref items, items.Length + 5); } // The index property can be used to add an item to a collection, // or to retrieve an item from the collection public T this[int index] { get { return this.items[index]; } set { this.items[index] = value; } } public void Add(T value) { // Check whether there is still room in the array. // If there is not enough room, then increase the capacity CheckAndIncreaseIfNecessary(); // Add the item at the end this.items[this.size] = value; // Increase the current number of items this.size++; } public void Insert(int index, T value) { // If the index is the same as the current number of items // then call Add() and proceed as if we want to add a new item if (index >= size) { Add(value); return; } // If the index is positive but less than the current size, // then you need to insert the item inside the collection. if ((index >= 0) && (index < size)) { // Firs, push each item one position up to create // an empty space for (int i = (size - 1); i > index - 1; i--) items[i + 1] = items[i]; // Then put the new item in the indicated position items[index] = value; // Since the new item has been added, increase the // current number of items size++; } } public bool Contains(T value) { for (int i = 0; i < Count; i++) if (items[i]!.Equals(value)) return true; return false; } public int IndexOf(T value) { for (int i = 0; i < Count; i++) if (items[i]!.Equals(value)) return i; return -1; } // This method is used to delete an item based on its position public void RemoveAt(int index) { // First check that the user provided a positive index that // is lower than the current total number of items if ((index >= 0) && (index < size)) { // If so, starting at that index, get the item ahead of // each position and assign it to the current item for (int i = index; i < size - 1; i++) items[i] = items[i + 1]; // Since the last item is not useful anymore, // decrease the number of items size--; } } // This method is used to delete an item public bool Remove(T value) { // First try to get the index of the item to find out if it exists int index = IndexOf(value); // If the item was found ... if (index >= 0) { // ... delete it, using its index RemoveAt(index); // Since the item has been removed, return true return true; } // If the item was not removed (for any reason), return false return false; } // This method is used to delete all items from a collection public void Clear() { // Visit each item in the collection and set it to the default value // (This is not really necessary) for (int i = 0; i < size; i++) items[i] = default!; // Reset the number of items of the collection to 0 size = 0; } public IEnumerator<T> GetEnumerator() { int counter = 0; while (counter < Count) { yield return items[counter]; counter++; } } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { int counter = 0; while (counter < Count) { yield return items[counter]; counter++; } } }
Additional Operations on a Collection
Introduction
The operations we saw above are the most regular of a collection. Still, there are many other ways you can assist a user of your collection class by defining more functionality. For example, you can make it possible to add more than one item with a single action, to delete more than one item in one step, to move items inside the collection by changing their positions, etc.
Checking Whether the Collection is Empty
Checking the emptiness of a collection allows you to find out whether it contains at least one item. Probably the easiest way to get this information is to enquire about the total number of items in the list. This can be done as follows:
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
. . .
// Checks whether the list is empty
public bool IsEmpty()
{
if (size == 0)
return true;
return false;
}
}
Adding a Collection to a Collection
Instead of adding one item at a time, you can allow a collection to receive an already created list of items. To do this, you can create a method to which you would pass a list as argument. In the body of the method, you can use a while, a for, or a foreach loop to navigate from one item to the next. When you get to an item, replace it with an item of the corresponding position of the argument. Here is an example:
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
. . .
public void AddRange(T[] values)
{
for (int i = 0; i < values.Length; i++)
Add(values[i]);
}
}
The First Item of a Collection
A collection that has at least one item has a starting point. There are various ways you can get the first item of a collection. If you are using an array-based list, you can simply get the item at index 0. Here is an example:
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
. . .
// Gets the first item in the list
public T First
{
get
{
if (IsEmpty())
return default(T);
return items[0];
}
}
}
Fortunately, remember that the IList<> interface is derived from IEnumerable<>. That interface is equipped with a method named First. Its syntax is:
public static TSource First<TSource>(this IEnumerable<TSource> source);
This method allows you to get the first member of a collection.
The Last Item of a Collection
If you are using an array-based list, to get the last item, get the item at the total number of items. Here is an example:
public class Collection<T> : IList<T>
{
private int size;
private T[] items;
. . .
// Gets the last item in the list
public T Last
{
get
{
if (IsEmpty())
return default(T);
return items[size - 1];
}
}
}
Remember that IList<> derives from the IEnumerable<> interface. That interface provides a method named Last. Its syntax is:
public static TSource Last<TSource>(this IEnumerable<TSource> source);
This method allows you to get the first member of a collection.
Replacing an Item
We saw that, using the indexed property, you can replace an existing item. This operation is not too difficult. You can proceed exactly as done with the set or the init accessor of the indexed property, except that you should check the validity of the index. Here is an example:
public class Collection<T> : IList<T> { private int size; private T[] items; . . . /* Replaces an existing item in the list with a new item * Equivalent to editing an existing item */ public void Replace(int index, T one) { if (index < 0) { // Invalid position // Normally, we should throw an exception, // such as IndexOutOfRangeException return; } if (index > size) { // Invalid position // Normally, we should throw an exception, // such as IndexOutOfRangeException return; } items[index] = one; } }
Moving an Item
Moving an item consists of changing its position. That is, the item's index changes from where it is, the source, to another position, the target. To perform this operation, you can pass two positions to a method. When defining the method, first make sure the positions are valid. Whenever any position is not valid, you should cancel the operation and throw an exception. If the positions are valid:
Here is an example:
public class Collection<T> : IList<T> { private int size; private T[] items; . . . // Changes the current position of an existing item. // Moves the item from its current curIndex position to the new toIndex position public void Move(int curIndex, int toIndex) { if (curIndex < 0) { // Invalid starting index // Normally, we should throw an exception, such as IndexOutOfRange return; } if (curIndex > (size - 1)) { // Invalid starting index // Normally, we should throw an exception, such as IndexOutOfRange return; } if (toIndex < 0) { // Invalid end index // Normally, we should throw an exception, such as IndexOutOfRange return; } if (toIndex > (size - 1)) { // Invalid end index // Normally, we should throw an exception, such as IndexOutOfRange return; } if (curIndex == toIndex) { // Same index, do nothing return; } // Get the item at the starting position T objLower = this[curIndex]; // Remove the item at the starting position RemoveAt(curIndex); // move the item to target position Insert(toIndex, objLower); } }
Swapping Items
Swapping two items consists of exchanging their positions. That is, the item at index x will move to index y and the item at index y will move to index x. To perform this operation, you pass two positions to a method. In the method, first make sure the positions are valid. If any position is invalid, you should cancel the operation and throw an exception. If the positions are valid:
Here is an example of how this can be done:
public class Collection<T> : IList<T> { private int size; private T[] items; . . . // Swaps the positions of two existing items // The first item goes to position posSecond // and the second item moves to position posFirst public void Exchange(int posFirst, int posSecond) { try { // Check that posFirst exists if (posFirst < 0) throw new IndexOutOfRangeException("Invalid first index - Too low"); // Check that posFirst exists if (posFirst > size - 1) throw new IndexOutOfRangeException("Invalid first index - Too high"); // Check that posSecond exists if (posSecond < 0) throw new IndexOutOfRangeException("Invalid second index - Too low"); // Check that posSecond exists if (posSecond > size - 1) throw new IndexOutOfRangeException("Invalid second index - Too high"); T objFirst = this[posFirst]; T objSecond = this[posSecond]; this[posFirst] = objSecond; this[posSecond] = objFirst; } catch (IndexOutOfRangeException) { } } }
Practical Learning: Ending the Lesson
Previous | Copyright © 2010-2024, FunctionX | Monday 06 December 2021 | Next |