C#.NET: Overriding the basic functionality of IComparable<T> and IComparer<T> interfaces to sort custom collections of data using Enumerable.OrderBy<TSource,TKey> aggregate method.






4.99/5 (49 votes)
The following article describes how to implement sorting of custom non-generic collections by overriding the functionality of the generic IComparable<T> and IComparer<T> interfaces used by the LINQ’s Enumerable.OrderBy<TSource, TKey> clause aggregate method.
Introduction
Whenever codding in C#.NET, we're implementing custom collections to store multiple related data object items of the same custom data type, as well as either providing the functionality that allows to manipulate those data objects items specific to their certain data type. In C# programming we also are using the generic features of the Microsoft .NET Framework and other language extensions, such LINQ, that, in turn, are allowing to manipulate the data items stored within the generic collections of data by using the LINQ query and aggregation methods. Unfortunately, these features are typically only used along with the generic collections of data such as List<T>, ArrayList, Stack<T>, etc. The following article describes the aproach that makes it possible to provide the extended functionality to the LINQ queries and aggregation methods when used to manipulate the items of a custom user-defined data type, stored within the custom non-generic collection of data implemented.
Background
In this article, we will discuss the common scenario which allows to override the generic functionality of the LINQ aggregation method Enumerable.OrderBy<TSource, TKey>
to perform the ascending sorting of the data object items of a certain user-defined data type, stored in the custom non-generic collection of data designed. As the example that illustrates the concept, described below, we will use the Enumerable.OrderBy aggregation method to sort the data object items containing the floating-point decimal datafield. This floating-point datafield will represent the key value according to which we'll be performing the sorting and other custom aggregate operations on the items stored in the custom collection of data. In order, to use the LINQ aggregation methods with data object items of the custom data type we actually need to override the generic delegate comparer methods, that perform the comparisson of the two data object items. For that purpose we need to derive a class from either IComparable<T> or IComparer<T> and re-implement the methods by providing the custom functionality that allows to sort the data object item's values of a specific data type. Refer to the using the code section of this article for the detailed step-by-step explaination of the following approach.
Using the code
Suppose, we have a class Item
designed, that represents an item stored within a certain custom non-genetic collection of data. Normally, this class contains either the data fields that are assigned to a single value of a certain data type, or methods to manipulate each particular item of the collection of data implemented. In this case, we're declaring a private variable m_value
of floating-point data type. Also we're providing the constructor that is used to assign the value to the m_value
variable at the point when a Item
class object is being initialized. To be able to use each Item
object along with the LINQ aggregate functions, we also should derive this class from the generic IComparable<T>
interface and specify the type parameter T a value of the Item
class. The IComparable<T> interface contains only one method int CompareTo(T object)
, that is invoked by the LINQ aggregate methods like OrderBy<T, TKey>, Average<T>, Max<T>, Min<T>
, to compare each item within a certain collection of data.
Normally, this method inherited will take another Item
object value as the parameter and is comparing it with the current data object item. As the result of comparing the two data object items this method returns the value that is greater than zero if the current item is less than another item compared, less than zero if it's greater, or zero if the two object items are equal respectively. Another method implemented in the following class is float GetItem()
used to retrieve the value of the private float data field used by the value retrieving and sorting methods described below.
class Item : IComparable<Item>
{
private float m_value; // private float data field
public Item(float value) { m_value = value; }
public float GetItem() { return m_value; }
public int CompareTo(Item other)
{
// Comparing each item floating value using the following conditions
return (this.GetItem() != other.GetItem()) ?
((this.GetItem() < other.GetItem()) ? 1 : -1) : 0;
}
}
Another non-generic class we have to implement is the ItemsComparer
. This class, when is derived from IComparer<T>
interface, encapsulates the basic mechanism of the each collection's data object item comparison performed by an aggregation method invoked. So, when designing the ItemsComparer
class we're actually deriving it from the IComparer<T>
interface and, similarly to the previous class Item
implementation, assign the type parameter T the value of the Item
class, which means that the all methods derived from IComparer<T> will have its parameters of the Item
class type. Similarly to the class Item
a class derived from IComparer<T>, ItemsComparer
class can only inherit one method int Compare(T x, T y)
overloaded. This method provides the basic functionality for comparing a pair of the collection's items of a certain type. In this case, the difference between this method and, previously recalled, IComparable<T>.CompareTo
is that the following method is invoked to compare the entire object items, stored in the collection of data designed, while the IComparable<T>.CompareTo(T other) is used to compare particular data fields within the data object item's class Item.
For example, if IComparable<T>.CompareTo(T other) is used to compare two float values of m_value
member variable from two different instances of an Item
object, then IComparer<T>.Compare(T x, Ty)
will be used to perform the same comparisson on the pair of instances of the Item
data object type within the collection of data designed. The returning value of the IComparer<T>.Compare(T x, T y) method can be evaluated as follows: if the value of x is less than y it returns -1, or if the value of x is greater than y the following method returns 1, it returns 0 if both x and y are equal. In this particular case, we actually are not evaluating the returning value of this method, instead we're invoking the IComparable<T>.CompareTo method implemented in the class Item and assign it has returned to value to the returning value of the IComparable<T>.CompareTo method, so this value is returned outside of these methods and is used by the Linq aggregation methods such as Enumerable.OrderBy<TSource, TKey> at the time it has been invoked.
class ItemsComparer : IComparer<Item>
{
public int Compare(Item x, Item y)
{
// Invoking a CompareTo method to compare each
// item x of the collection with the current item y specified
return x.CompareTo(y);
}
}
The following portion of C# code implements a custom non-generic collection of data that allows store the array of values of the Item object data type as well as providing some functionality of either appending and retrieving items from the collection, or implementing the IEnumerable.GetEnumerator()
methods instances that allow to obtain the collection's enumerator object, that will further be invoked by the C# foreach statement to iterate through the items stored in the collection of data. Normally, to implement a custom non-generic collection of data, in this C# code sample, we won't use the existing generic collections of data such as List<T>, ArrayList
, Stack<T>
, etc. Instead, we're implementing the collection based on the using C# simple arrays to store the multiple data objects of the Item
data type within. The reason why we don't derive the MyItemsCollection
class from the one of the generic collections is that they already implement the sorting and other aggregate methods to manipulate the data items stored in the collection of data. We actually are implementing the custom non-generic collection from "a scratch" and don't need any of those generic methods to be overloaded. To sort the items stored in the collection, we're implementing the Sort()
method that invokes the LINQ's Enumerable.OrderBy
clause. Let's now recall, that not only the generic collections such as List<T>
, ArrayList
,..., but also simple arrays in C# are treated as the objects derived from the generic IEnumerable<T>
interface, from which we inherit the all methods provided by IEnumerable
interface as well as from the other generic interfaces, the IEnumerable is derived from. Normally we're passing the first and second type parameters the values of either the type of data items being sorted or the type of key the value by which the sort will be performed, respectively.
In this case, these two type parameters are assigned to the value of the Item
data object. The first argument of the Enumerable.OrderBy method is the lambda expression which is used to specified what particular sequence of values will be ordered. The next argument of the following method is an object of the IComparer<T>
derived class constructed. This object will further be used to invoke the IComparer.Compare method which is intended to performs the actual comparisson between the two object items of Item type.
class MyItemsCollection : IEnumerable<Item>
{
// The array of items of Item data type
private static Item[] m_Items = null;
// The array items index variable m_ItemIndex
private static int m_ItemIndex = 0;
public MyItemsCollection()
{
// Initializing the array of items
if (m_Items == null)
m_Items = new Item[0];
}
public void Add(Item item)
{
// Growing the size of the array
Array.Resize<Item>(ref m_Items, m_ItemIndex + 1);
// Assigning a value to the current item of m_Items array
// accessed by its m_ItemIndex index
m_Items[m_ItemIndex++] = item;
}
public void Sort()
{
// Invoking orderby LINQ aggregation method to sort
// the items stored in the array m_Items
m_Items = m_Items.OrderBy<Item, Item>(item => item,
new ItemsComparer()).ToArray();
}
public IEnumerator GetEnumerator()
{
// Retriving the value of the generic enumerator
// for the array m_Items
return m_Items.GetEnumerator();
}
IEnumerator<Item> IEnumerable<Item>.GetEnumerator()
{
// Invoking the GetEnumerator() methods of this object
// and assign its returning value to the returning value
// of this function explicitly casting it to the type of IEnumerator<Item>
return (IEnumerator<Item>)this.GetEnumerator();
}
}
Finally, the C# code implemented in the Program
class is intended to initialize the MyItemsCollection
class object. After the initialization we're appending five data object items, each one is the dynamically constructed object of the described above Item
class. In this case, the value of the float type is assigned of the constructor parameter of each Item
class object is being instantiated. After the collection initialization, we use the C# foreach statement to iterate through the items in the collection of data, fetching each float value by invoking GetItem()
method for each instance of the data item object and performing the console output for each float item's value retrieved.
class Program
{
static void Main(string[] args)
{
// Initializing the custom collection's object
MyItemsCollection items = new MyItemsCollection();
// Adding items to the collection of data
items.Add(new Item(3.5F));
items.Add(new Item(0.58F));
items.Add(new Item(1.16F));
items.Add(new Item(0.32F));
items.Add(new Item(0.57F));
// Sorting the collection of data
items.Sort();
// Iterating through the collection of data constructed
// Retrieving items using GetItem() method and providing
// the console output for each item fetched from the current collection
foreach (Item item in items)
Console.WriteLine("{0:F}", item.GetItem());
Console.ReadKey();
}
}
Points of Interest
The programming approach described in the following article can be used for implementing the custom non-generic collections of data in C#.NET as well as providing the functionality for manipulating the data object items of the user-defined type, stored in custom collections of data, by using the LINQ aggregation methods. This article is also intended for the beginner C# developers and enterpreneurs as the brief tutorial to the basics of the custom non-generic collections of data implementation and other object-oriented programming techniques used in C# programming language.
History
- June 28, 2015 - The first version of the article was published