Click here to Skip to main content
Email Password   helpLost your password?

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
{
	// Note: These auto-properties require .NET 3.0+.  
	// For 2.0, use old-fashioned fields and properties.
	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:

// .NET 2.0
people.Sort(
	delegate(Person p1, Person p2)
	{
		return p1.Age.CompareTo(p2.Age);
	});

// .NET 3.0+
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:

// .NET 2.0
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); }
);

// .NET 3.0+
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

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralUse LINQ
Paul B.
13:10 3 Feb '09  
Since you are using lambdas in some of your examples, just thought I'd mention using LINQ to sort and would read much better as well. I think people should start there before going this route if they are able to use 3.5
GeneralRe: Use LINQ
Joe Enos
13:34 3 Feb '09  
@Paul:
Using LINQ, or directly using the OrderBy extension method looks like it would work pretty well in this circumstance. It adds a layer of abstraction, but I'm sure it's negligible when dealing with performance.

I don't know if it would let you easily sort by strings representing property or field names, but I'm sure there are some creative ways of making that work as well.

Thanks for the suggestion. I guess I'm stuck in the good old days of 2.0.

Joe Enos
joe@jtenos.com

GeneralRe: Use LINQ
Joe Enos
19:43 12 Apr '09  
As a follow up:
There's another article just submitted to the CodeProject by Troy Beacleay that does sort by a string representing the name of a property, using LINQ and generic lambda expressions. It's more up-to-date than mine, since mine is really 2.0-oriented:

Sort IEnumerableT by column name[^]

Joe Enos
joe@jtenos.com

GeneralStable Sorting
BugByter
11:24 1 Feb '09  
Hello, thanks for your article.

Sorting for multiple attributes is one situation where the instability of the standard quicksort shows: if .NET was using a stable sort, you could simply first sort by age and afterwards, re-sort the whole list by name - and the entries would now be sorted first by name and second by age. with quicksort, however, the list is properly sorted by name but the second order for age is destroyed and doesn't necessarily fit.

Do you know of any proven and tried drop-in replacement for the standard quicksort that is stable?

Cheers,
Buggy
GeneralRe: Stable Sorting
Joe Enos
19:02 1 Feb '09  
@Buggy:

Unfortunately, I don't have any experience with sorting algorithms. My technique is basically a wrapper around the out-of-the-box sort algorithm from the framework, performing the sort in a single step by comparing the entire set of things that we're sorting by.

I've just never had a need to sort collections as fast as possible, or with as little memory footprint as possible - the standard sort algorithm performs fine for anything I've ever needed.

Thanks for the comment.

Joe Enos
joe@jtenos.com

GeneralRe: Stable Sorting
TobiasP
0:43 3 Feb '09  
I think merge sort[^] is the most commonly used stable sorting algorithm and the default choice when programming in some languages. Compared to quick sort, it has the same average time complexity (O(n*log n)), better worst case time complexity (O(n*log n) for MS, O(n*n) for QS), but worse memory footprint (O(n) for MS, O(1) for QS).

An overview of different sorting algorithms can be found at Wikipedia: Sorting algorithm[^].
GeneralStable Sorting HERE
BugByter
7:59 15 Feb '09  
Thanks, that's what I also think. I was just looking for THE reference implementation.

But while searching I stumbled across this: http://www.codeplex.com/PowerCollections

I have no time to test now but in case anybody is looking for it: this ^ code is said to have stable sorting and seems to work well.

Kind regards, Buggy
Generalgood idea however...
Yang Yu
11:13 31 Jan '09  
just one comment, i don't know weather or not list is the most used collection, List is an abstract implementation, it is not really an data structure. even sortedList is still an abstract implementation. the data structure used for sortedlist is probably an hash binary avl tree. im not sure tho. i list.sort using quicksort with worst case time of O(n^2) so this type of operation can be expensive.
GeneralRe: good idea however... [modified]
Joe Enos
11:27 31 Jan '09  
@Yang:
Not sure I understand what you're referring to. A List is a concrete collection type, and in fact is very commonly used. There are other collection types, like the Array, Queue, Stack, and other different types of collections like the Dictionary<tkey,>, SortedList<tkey,>, LinkedList, and others. But when you need a simple type-safe collection of objects, then List is your best option.

Also, when you are talking about performance, the CollectionSorter is basically a wrapper around .NET's standard sorting routines, so it's no more expensive than the out-of-the-box sort.

Thanks for the comment.

Joe Enos
joe@jtenos.com
modified on Saturday, January 31, 2009 6:38 PM


Last Updated 31 Jan 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010