Introduction
The generic List<T> collection is probably the most widely used collection type in the .NET Framework. Behind the scenes, a List is simply an array, with methods wrapped around it for adding, inserting, and sorting objects. Sorting a list of objects is reasonably straightforward - you can define a Comparison<T> method (regular or anonymous), or define a class that implements IComparer<T>. In either case, you typically just need to write a method that compares two Ts, and it does the rest. However, sorting by multiple properties is not as simple.
Background
For this article, we'll work with a simple class called Person, defined as follows:
class Person
{
public int Age { get; set; }
public string LastName { get; set; }
}
Suppose you want to sort by Age. You have two options:
Option 1: Use a Comparison<Person> delegate:
people.Sort(
delegate(Person p1, Person p2)
{
return p1.Age.CompareTo(p2.Age);
});
people.Sort((p1, p2) => p1.Age.CompareTo(p2.Age));
Option 2: Use a class that implements IComparer:
class MyComparerByAge : IComparer<Person>
{
int IComparer<Person>.Compare(Person p1, Person p2)
{
return p1.Age.CompareTo(p2.Age);
}
}
people.Sort(new MyComparerByAge());
Seems simple enough - most sorting can be accomplished using the lambda logic, and it's easy enough to write and to read. But suppose you want to sort by Age, followed by LastName. The simple lambda logic no longer works, and the length of your code grows significantly:
people.Sort(
delegate(Person p1, Person p2)
{
int compare = p1.Age.CompareTo(p2);
if (compare == 0)
{
compare = p1.LastName.CompareTo(p2);
}
return compare;
});
You need the extra logic in there that checks to see if the first check resulted in a nonzero value before checking the next one. For each additional layer of sorting, you need another if statement, and your anonymous method continues to grow.
Using the Code
Here's where JTCollections comes in. It gives you the opportunity to sort a List using multiple simple lambda expressions, as follows:
CollectionSorter.SortList(people,
delegate(Person p1, Person p2) { return p1.Age.CompareTo(p2.Age); },
delegate(Person p1, Person p2) { return p1.LastName.CompareTo(p2.LastName); }
);
CollectionSorter.SortList(people,
(p1, p2) => p1.Age.CompareTo(p2.Age),
(p1, p2) => p1.LastName.CompareTo(p2.LastName)
);
In addition, if you are sorting by simple properties like this, then you can sort as follows:
CollectionSorter.SortList(people, "Age", "LastName");
The CollectionSorter code uses reflection to obtain the property matching the strings "Age" and "LastName", and sorts appropriately, using the default sorter for that type. This works well for numeric and string values, which is probably good enough for most situations. Of course, there's also an option to sort one or more properties descending, with just a little extra code:
CollectionSorter.SortList(people,
new CollectionSorter.SortMember("Age", CollectionSorter.SortType.Descending),
new CollectionSorter.SortMember("LastName", CollectionSorter.SortType.Ascending)
);
The code behind these sorting methods is reasonably straightforward. First off, let's take a look at the SortList method:
public static void SortList<T>(List<T> listToSort, params Comparison<T>[] comparisons)
{
Comparison<T> comparisonFunction = delegate(T t1, T t2)
{
int compare = 0;
for (int i = 0; i < comparisons.Length; i++)
{
compare = comparisons[i](t1, t2);
if (compare != 0)
{
break;
}
}
return compare;
};
listToSort.Sort(comparisonFunction);
}
It accepts a param array of Comparison<T> delegates, which can be full delegates or lambda expressions (3.0+). It builds a single delegate that combines the smaller delegates, stopping once a nonzero result is obtained. Basically, it does the same thing as our earlier example, but can be called very easily.
In addition, you can convert any collection into a sorted List using the following method, which just calls the SortList method after converting your collection to a List:
public static List<T> GetSortedList<T>(ICollection<T> collectionToSort,
params Comparison<T>[] comparisons)
{
List<T> result = new List<T>(collectionToSort);
SortList(result, comparisons);
return result;
}
Passing in the property names is slightly more complicated.
First is the SortMember class, which is just a simple container with a string representing the member name, and a SortType which is either Ascending or Descending:
[Serializable]
public class SortMember
{
private string _memberName;
private SortType _sortType;
public SortMember(string memberName, SortType sortType)
{
_memberName = memberName;
_sortType = sortType;
}
public string MemberName { get { return _memberName; }
set { _memberName = value; } }
public SortType SortType { get { return _sortType; }
set { _sortType = value; } }
}
public enum SortType
{
Ascending,
Descending
}
Here's the JTComparer class, which is a private class inside CollectionSorter, and does all of the actual work:
private class JTComparer<T> : IComparer<T>
{
private SortMember[] _sortMembers;
public JTComparer(SortMember[] sortMembers)
{
_sortMembers = sortMembers;
}
public int Compare(T t1, T t2)
{
int compare = 0;
foreach (SortMember sortMember in _sortMembers)
{
if (string.IsNullOrEmpty(sortMember.MemberName))
{
continue;
}
MemberInfo[] memberInfos =
typeof(T).GetMember(sortMember.MemberName);
if ((memberInfos != null) && (memberInfos.Length > 0))
{
MemberInfo memberInfo = memberInfos[0];
switch (memberInfo.MemberType)
{
case MemberTypes.Property:
PropertyInfo propertyInfo = typeof(T).
GetProperty(sortMember.MemberName);
if (typeof(IComparable).IsAssignableFrom
(propertyInfo.PropertyType))
{
IComparable val1 = (IComparable)
propertyInfo.GetValue(t1, null);
IComparable val2 = (IComparable)
propertyInfo.GetValue(t2, null);
compare = val1.CompareTo(val2);
}
break;
case MemberTypes.Field:
FieldInfo fieldInfo = typeof(T).GetField
(sortMember.MemberName);
if (typeof(IComparable).IsAssignableFrom
(fieldInfo.FieldType))
{
IComparable val1 = (IComparable)
fieldInfo.GetValue(t1);
IComparable val2 = (IComparable)
fieldInfo.GetValue(t2);
compare = val1.CompareTo(val2);
}
break;
}
}
if (compare != 0)
{
if (sortMember.SortType == SortType.Descending)
{
return (-1) * compare;
}
return compare;
}
}
return 0;
}
}
If you're not familiar with reflection, it may not be obvious what some of this code does. Let's take a look at some of it:
The following retrieves the member for the given type that matches the first string that is passed in:
MemberInfo[] memberInfos = typeof(T).GetMember(sortMember.MemberName);
Assuming that member exists, the array will contain one element. If the array is empty, it does not error, but simply skips this member. The member will be either a property or a field, so the next step is to compare the values corresponding to that property or field:
case MemberTypes.Property:
PropertyInfo propertyInfo = typeof(T).GetProperty(sortMember.MemberName);
if (typeof(IComparable).IsAssignableFrom(propertyInfo.PropertyType))
{
IComparable val1 = (IComparable)propertyInfo.GetValue(t1, null);
IComparable val2 = (IComparable)propertyInfo.GetValue(t2, null);
compare = val1.CompareTo(val2);
}
break;
case MemberTypes.Field:
FieldInfo fieldInfo = typeof(T).GetField(sortMember.MemberName);
if (typeof(IComparable).IsAssignableFrom(fieldInfo.FieldType))
{
IComparable val1 = (IComparable)fieldInfo.GetValue(t1);
IComparable val2 = (IComparable)fieldInfo.GetValue(t2);
compare = val1.CompareTo(val2);
}
break;
Regardless of whether it's a property or a field, the code is basically the same. You get the PropertyInfo or FieldInfo object relating to the type (not the object itself, just the type). Then, you ensure that the type of the property or field implements IComparable, so that you can use them to compare (strings, numbers, dates, etc. all implement IComparable, so any normal sort will pass this test). Once you've confirmed that the type implements IComparable, you can retrieve the values of that property or field for both objects, and compare them.
Once the comparison has occurred, we're back to the same logic we've used before. Check if it's nonzero, then return it. The only thing left is the ascending/descending question - if this particular sort member is descending, then you actually want to reverse the result you just obtained.
if (compare != 0)
{
if (sortMember.SortType == SortType.Descending)
{
return (-1) * compare;
}
return compare;
}
So with this class in place, all you need is some methods in CollectionSorter that call it - all straightforward:
public static void SortList<T>(List<T> listToSort, params string[] memberNames)
{
SortMember[] sortMembers = new SortMember[memberNames.Length];
for (int i = 0; i < memberNames.Length; i++)
{
sortMembers[i] = new SortMember(memberNames[i], SortType.Ascending);
}
SortList(listToSort, sortMembers);
}
public static void SortList<T>(List<T> listToSort, params SortMember[] sortMembers)
{
listToSort.Sort(new JTComparer<T>(sortMembers));
}
public static List<T> GetSortedList<T>(ICollection<T> collectionToSort,
params string[] memberNames)
{
SortMember[] sortMembers = new SortMember[memberNames.Length];
for (int i = 0; i < memberNames.Length; i++)
{
sortMembers[i] = new SortMember(memberNames[i], SortType.Ascending);
}
return GetSortedList(collectionToSort, sortMembers);
}
public static List<T> GetSortedList<T>(ICollection<T> collectionToSort,
params SortMember[] sortMembers)
{
List<T> result = new List<T>(collectionToSort);
SortList(result, sortMembers);
return result;
}
Conclusion
This class provides a few alternate methods for sorting a List<T>. While it doesn't provide any performance benefits, it does allow you to write less code, code that is easier to read and less prone to mistakes.
The attached ZIP file contains a Visual Studio 2008 solution, with projects designed to compile to the .NET 2.0 Framework. It contains a JTCollections project, which contains a single class (CollectionSorter), and a JTCollectionsTestFixture project, which contains various unit tests designed to run in NUnit.
History
- 31st January, 2009: Initial post