Generic List Sort Function






4.41/5 (12 votes)
This allows the programmer to sort a list based on multiple properties
Introduction
In this article, I will talk about how to sort the generic .NET list using reflection and extension methods in C#. This extension method for the generic list is able to sort a list on multiple properties. The usage is simply text-based, for example:
userlist.Sort("Firstname asc, Birthday desc");
Background
Extension methods enable you to "add" methods to existing types without creating a new derived type, recompiling, or otherwise modifying the original type. Extension methods are a special kind of static
method, but they are called as if they were instance methods on the extended type. For client code written in C# and Visual Basic, there is no apparent difference between calling an extension method and the methods that are actually defined in a type.
The Code
The following code is the extension method itself, the sort function.
It creates a list of generic comparers (because we want to enable sorting on multiple properties).
Then it loops through the sortExpressions
and creates new Comparers
.
/// <summary>
/// Sorts the specified list.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list">The list to be sorted.</param>
/// <param name="sortExpression">The sort expression; format:
/// @param1 [sortdirection], @param2 [sortdirection], @param3 [sortdirection].
/// Valid sortDirections are: asc, desc, ascending and descending.</param>
public static void Sort<T>(this List<T> list, string sortExpression)
{
string[] sortExpressions = sortExpression.Split(new string[] { "," },
StringSplitOptions.RemoveEmptyEntries);
List<GenericComparer> comparers = new List<GenericComparer>();
foreach (string sortExpress in sortExpressions)
{
string sortProperty = sortExpress.Trim().Split(' ')[0].Trim();
string sortDirection = sortExpress.Trim().Split(' ')[1].Trim();
Type type = typeof(T);
PropertyInfo PropertyInfo = type.GetProperty(sortProperty);
if (PropertyInfo == null)
{
PropertyInfo[] props = type.GetProperties();
foreach (PropertyInfo info in props)
{
if (info.Name.ToString().ToLower() == sortProperty.ToLower())
{
PropertyInfo = info;
break;
}
}
if (PropertyInfo == null)
{
throw new Exception(String.Format("{0} is not a valid property of type:
\"{1}\"", sortProperty, type.Name));
}
}
SortDirection SortDirection = SortDirection.Ascending;
if (sortDirection.ToLower() == "asc" || sortDirection.ToLower() == "ascending")
{
SortDirection = SortDirection.Ascending;
}
else if (sortDirection.ToLower() == "desc" ||
sortDirection.ToLower() == "descending")
{
SortDirection = SortDirection.Descending;
}
else
{
throw new Exception("Valid SortDirections are:
asc, ascending, desc and descending");
}
comparers.Add(new GenericComparer { SortDirection = SortDirection,
PropertyInfo = PropertyInfo, comparers = comparers });
}
listSort(comparers[0].Compare);
}
The following code shows the GenericComparer
class which compares the two elements. If the elements are equal, it will ask the second comparer to compare the second property and so on. This part is recursive!
public class GenericComparer
{
public List<GenericComparer> comparers { get; set; }
int level = 0;
public SortDirection SortDirection { get; set; }
public PropertyInfo PropertyInfo { get; set; }
public int Compare<T>(T t1, T t2)
{
int ret = 0;
if (level >= comparers.Count)
return 0;
object t1Value = comparers[level].PropertyInfo.GetValue(t1, null);
object t2Value = comparers[level].PropertyInfo.GetValue(t2, null);
if (t1 == null || t1Value == null)
{
if (t2 == null || t2Value == null)
{
ret = 0;
}
else
{
ret = -1;
}
}
else
{
if (t2 == null || t2Value == null)
{
ret = 1;
}
else
{
ret = ((IComparable)t1Value).CompareTo(((IComparable)t2Value));
}
}
if (ret == 0)
{
level += 1;
ret = Compare(t1, t2);
level -= 1;
}
else
{
if (comparers[level].SortDirection == SortDirection.Descending)
{
ret *= -1;
}
}
return ret;
}
}
Using the Code
The following example shows us how to use it and the results:
public class ExampleUser
{
public DateTime Birthday { get; set; }
public string Firstname { get; set; }
}
public class ExampleClass
{
public static void Example()
{
List<ExampleUser> userlist = new List<ExampleUser>();
userlist.Add(new ExampleUser
{ Birthday = new DateTime(1988, 10, 1), Firstname = "Bryan" });
userlist.Add(new ExampleUser
{ Birthday = new DateTime(1986, 11, 4), Firstname = "Michael" });
userlist.Add(new ExampleUser
{ Birthday = new DateTime(1977, 2, 2), Firstname = "Arjan" });
userlist.Add(new ExampleUser
{ Birthday = new DateTime(1990, 6, 13), Firstname = "Pieter" });
userlist.Add(new ExampleUser
{ Birthday = new DateTime(1988, 10, 1), Firstname = "Ruben" });
userlist.Add(new ExampleUser
{ Birthday = new DateTime(1987, 8, 21), Firstname = "Bastiaan" });
userlist.Add(new ExampleUser
{ Birthday = new DateTime(1987, 8, 21), Firstname = "Pieter" });
string unsorted = "Unsorted: ";
foreach (ExampleUser user in userlist)
{
unsorted += String.Format("{0} / {1} {2}", user.Birthday.ToString
("dd-MM-yyyy"), user.Firstname, Environment.NewLine);
}
userlist.Sort("Firstname asc");
string sorted1 = "Sorted by Firstname asc: " + Environment.NewLine;
foreach (ExampleUser user in userlist)
{
sorted1 += String.Format("{0} / {1} {2}", user.Birthday.ToString
("dd-MM-yyyy"), user.Firstname, Environment.NewLine);
}
userlist.Sort("Firstname asc, Birthday desc");
string sorted2 = "Sorted by Firstname asc, Birtday desc: " + Environment.NewLine;
foreach (ExampleUser user in userlist)
{
sorted2 += String.Format("{0} / {1} {2}", user.Birthday.ToString
("dd-MM-yyyy"), user.Firstname, Environment.NewLine);
}
userlist.Sort("Birthday asc, Firstname asc");
string sorted3 = "Sorted by Birthday ascending,
Firstname asc: " + Environment.NewLine;
foreach (ExampleUser user in userlist)
{
sorted3 += String.Format("{0} / {1} {2}", user.Birthday.ToString
("dd-MM-yyyy"), user.Firstname, Environment.NewLine);
}
}
}
Results
Unsorted:
01-10-1988 / Bryan
04-11-1986 / Michael
02-02-1977 / Arjan
13-06-1990 / Pieter
01-10-1988 / Ruben
21-08-1987 / Bastiaan
21-08-1987 / Pieter
Sorted by Firstname ascending:
02-02-1977 / Arjan
21-08-1987 / Bastiaan
01-10-1988 / Bryan
04-11-1986 / Michael
21-08-1987 / Pieter
13-06-1990 / Pieter
01-10-1988 / Ruben
Sorted by Firstname ascending, Birtday descending:
02-02-1977 / Arjan
21-08-1987 / Bastiaan
01-10-1988 / Bryan
04-11-1986 / Michael
13-06-1990 / Pieter
21-08-1987 / Pieter
01-10-1988 / Ruben
Sorted by Birthday ascending, Firstname ascending:
02-02-1977 / Arjan
04-11-1986 / Michael
21-08-1987 / Bastiaan
21-08-1987 / Pieter
01-10-1988 / Bryan
01-10-1988 / Ruben
13-06-1990 / Pieter
Performance Testing
I did some testing using the following code, where list1
is being sorted the usual way and list2
is being sorted using my code.
public class TestClass
{
public string Text { get; set; }
}
public static void Test()
{
List<TestClass> list1 = new List<TestClass>();
List<TestClass> list2 = new List<TestClass>();
for (int index = 0; index < 1000; index++)
{
Guid item = Guid.NewGuid();
list1.Add(new TestClass { Text = item.ToString() });
list2.Add(new TestClass { Text = item.ToString() });
}
DateTime start1 = DateTime.Now;
list1.Sort(delegate(TestClass p1, TestClass p2)
{ return p1.Text.CompareTo(p2.Text); });
DateTime stop1 = DateTime.Now;
TimeSpan diff1 = stop1 - start1;
DateTime start2 = DateTime.Now;
list2.Sort("Text asc");
DateTime stop2 = DateTime.Now;
TimeSpan diff2 = stop2 - start2;
}
Results
index = 1000;
diff1: TotalSeconds = 0.015625
diff2: TotalSeconds = 0.0625
4 times slower.
index = 10.000;
diff1: TotalSeconds = 0.03125
diff2: TotalSeconds = 0.46875
15 times slower.
index = 100.000;
diff1: TotalSeconds = 0.4375
diff2: TotalSeconds = 5.859375
13.4 times slower
History
- 17-07-2008 09:50: Added some test results