Click here to Skip to main content
Licence CPOL
First Posted 31 Jan 2009
Views 10,161
Downloads 44
Bookmarked 30 times

CollectionSorter

By | 31 Jan 2009 | Article
Alternative methods for sorting a .NET List

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

  • 31st January, 2009: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Joe Enos

Software Developer
Desert Schools Federal Credit Union
United States United States

Member

Follow on Twitter Follow on Twitter
Joe Enos is a software engineer in Phoenix, Arizona, with 8 years of .NET experience. Currently working as a software developer on a medium-sized team in Phoenix.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
GeneralUse LINQ PinmemberPaul B.12:10 3 Feb '09  
GeneralRe: Use LINQ PinmemberJoe Enos12:34 3 Feb '09  
GeneralRe: Use LINQ PinmemberJoe Enos18:43 12 Apr '09  
GeneralStable Sorting PinmemberBugByter10:24 1 Feb '09  
GeneralRe: Stable Sorting PinmemberJoe Enos18:02 1 Feb '09  
GeneralRe: Stable Sorting PinmemberTobiasP23:43 2 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 PinmemberBugByter6:59 15 Feb '09  
Generalgood idea however... PinmemberYang Yu10:13 31 Jan '09  
GeneralRe: good idea however... [modified] PinmemberJoe Enos10:27 31 Jan '09  

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Mobile
Web01 | 2.5.120604.1 | Last Updated 31 Jan 2009
Article Copyright 2009 by Joe Enos
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid