|
Sounds good, I would relly like support for object hierarchies, so I can Sort by ContactMethods.Telephone for example, sorting by a propertys property.
Let me know when you get this to work.
Great work by the way, really usefull.
Kind regards,
Allan Ebdrup
System architect, OFiR
|
|
|
|
|
The sorting works great for what I need. However, I need to improve it somehow and I am not sure how to do this. Sometimes I get string properties populated that actually will contain numbers. I still need to sort this by the actual number instead of a string value. To account for this I added a type to the SortProperties. If set I use the type passed in instead of the actual type of the property. This is not working. Any ideas on what I am doing wrong or how to accomplish this? The code is below.
System.Type propertyType; // set before the foreach loopo
foreach (SortProperty property in sortProperties)
{
propertyName = property.Name;
propertyInfo = typeof(T).GetProperty(propertyName);
if (property.Type == null)
propertyType = propertyInfo.PropertyType;
else
propertyType = property.Type;
// other code
ilGen.EmitCall(OpCodes.Callvirt, propertyType.GetMethod("CompareTo", new Type[] { propertyType }), null); }
|
|
|
|
|
Hi midavis.
It would be a great help if you could elaborate a little on what is actually going wrong... Any errors or such. My guess is that since you are passing in a "wrong" type you are in fact trying to compare two strings using the int's CompareTo method. You could probably solve this by converting the strings into real numbers by calling something like the Convert.ToInt32() function before you call CompareTo. However this is only my best guess since I don't have that much time at the moment. Hope you figure it out.
Kind regards,
Johannes Hansen
frontAvenue A/S
|
|
|
|
|
Yes, I believe that is is still comparing two strings. I am not sure how to modify this to fit my needs. What I want out of this is the ability to override what type of comparison is going on. In this case a string comparison would be going on but I want to override it saying I want a double comparison.
|
|
|
|
|
Hi again. What you need to do is inject some IL to convert the string into the type you want to do the comparison with. In your case it is the "double" type. You can convert into this type by calling double.Parse() or Convert.ToDouble() depending on your needs. Unfortunately this is all the help I can give you at the moment since my work in RealLife™ is taking all of my time after my vacation. I'm willing to try and implement this but it might take a week or 2 before I get around to it.
Kind regards,
Johannes Hansen
frontAvenue A/S
|
|
|
|
|
I am not familiar with Dynamic Methods so I may just wait for you to come back . Also, I really can't just call double.Parse() because I want this to work always. I was thinking of doing a doing casting the value first. If you would not mind showing me how to implement this in VB.NET and C# I would appreciate it.
|
|
|
|
|
Instead of double.Parse(string) you would just have to call Convert.ChangeType(object, type). If you call Convert.ChangeType("1,234.56", typeof(double)) the call would eventually be routed to double.Parse(string, IFormatProvider). However this trick only works if the type of the value, passed to Convert.ChangeType(value, convertType), is implementing IConvertible. Also, there could be some issues with null values, value types vs. reference types, nullable value types etc.
Finally please consider the performance impact this have... On smaller lists say with less than a couple of hundred items you probably wouldn't feel a difference but I suppose it could have a rather big impact on larger lists.
Kind regards,
Johannes Hansen
frontAvenue A/S
|
|
|
|
|
Excellent article! I use it often.
Could you expand your DynamicSorting to support BinarySearch, Find, etc
that a List<type> supports (as well as your Sort?).
Don
|
|
|
|
|
Hi Don.
First of all sorry for the late reply but my vacation got in between your question and this answer.
Your question is very good and the short answer is "yes". Actually you could use the DynamicComparer in the BinarySearch method though you'ld probably want to implement a specific DynamicSearcher for that purpose. The "Find" method on a generic list is special since it doesn't take a IComparer but instead a generic Predicate. An example of usage could be:
myList.Find(delegate (int a) { return a == 1; });
Which would be the same as writing:
myList.Find(new Predicate<int>(delegate (int a) { return a == 1; }));
This actually makes it a little easier to implement since we don't need to worry about implementing the IComparer interface. You could implement a DynamicFinder like so:
<br />
public static class DynamicFinder<br />
{<br />
public static Predicate<T> GetDynamicFinder<T>(string searchArgs)<br />
{<br />
DynamicMethod dm = CreateDynamicFinderMethod(searchArgs, typeof(T));
return (Predicate<T>)dm.CreateDelegate(typeof(Predicate<T>));<br />
}<br />
<br />
private static DynamicMethod CreateDynamicFinderMethod(string searchArgs, Type entityType)<br />
{<br />
DynamicMethod dm = new DynamicMethod("DynamicFinder", typeof(bool), new Type[] {entityType}, typeof(DynamicFinder));<br />
return dm;<br />
}<br />
}<br />
And then use it like this:
myList.Find(DynamicFinder.GetDynamicFinder<Person>("FirstName='Johannes' && LastName='Hansen'"));
Of course you would need to parse the filter string passed into the "GetDynamicFinder" somehow and match it against the fields and properties of the entity type. But there you have it...
I'm sorry I can't help you with a more complete DynamicFinder implementation but I hope this will help you get started on writing your own.
Kind regards,
Johannes Hansen
frontAvenue A/S
|
|
|
|
|
Which category in your performance table above does the following code fall under?
List< Person > people = new List< Person >();
people.Sort(new Comparison< Person >(Person x, Person y)
{
return x.FirstName.CompareTo(y.FirstName);
});
-Bill
|
|
|
|
|
Hi Bill
The code in your message would go under the category "hard-coded".
Kind regards,
Johannes Hansen
frontAvenue A/S
|
|
|
|
|
How can I make this work with a list of Structs instead of a list of classes? It throws an AccessViolation when I use it with structs.
|
|
|
|
|
Looks like (first glance) that you'll get an exception if you try to access the properties of a struct, but playing with the fields is fine. I'll see if I can fix the properties, but i nthe meantime just use the field.
struct Foo
{
private string works;
public string throws
{
get { return works; }
}
}
|
|
|
|
|
Nope, fields only failed to throw, but they didn't really work either.
I've made the changes and my ZIP file. Get it here.
|
|
|
|
|
Hi,
This is great article.
But I think there's more simple way which will handle all the cases (including Enums and Nullable types). Simply a call to the Compare method of Comparer<T>.Default must be emitted. And no checks will be performed for nullables, enums, value types etc.
Am I right ?
P.S. Frankly saying I haven't checked the speed of this approach, but I think it must not dramatically impact the performance.
Regards,
Robert Hovhannisyan.
-- modified at 8:41 Friday 7th April, 2006
|
|
|
|
|
Hi Robert.
First of all thank you for the feedback!
Actually what you're suggesting isn't solving the same problem as DynamicComparer is solving. DynamicComparer is capable of sorting on several fields within the same type where Comparer<t> is using IComparable to call the Compare function on T. Of course you could implement the Compare function to compare several fields but it would be static and you would have to recompile to change the set of fields you do the comparison on, DynamicComparer doesn't have this problem. The performance of the DynamicComparer is probably even a little faster *theoretically* than ordinary compiled comparers since we are able to optimize the comparison better than the .NET compiler is able to.
Kind regards,
Johannes Hansen
frontAvenue A/S
|
|
|
|
|
Hi Johannes,
I think I didn't exactly explained the idea. I am suggesting to emit a call to Compare method of Comparer<T>.Default in CreateDynamicCompareMethod function.
Regards,
Robert Hovhannisyan.
|
|
|
|
|
Hi Robert,
Sure that would probably work... I'll have to try it out first. But, we'll loose the chance to optimize the IL manually. However, I think your suggestion seem like a very good tradeoff since the IL optimization only increases the performance theoretically. Thanks for the suggestion!
Kind regards,
Johannes Hansen
frontAvenue A/S
|
|
|
|
|
Hi again, Johannes !
Just to clarify. I think the confusion was in T used in DynamicComparer<T> and Comparer<T>.Default.
As you understand, idea is to emit a call to Compare method of Comparer<T>.Default for each SortProperty in the sort expression, where T is the PropertyType of the corresponding property with name SortProperty.Name, and not the type parameter T of DynamicComparer<T>.
Regards,
Robert Hovhannisyan.
|
|
|
|
|
Hi,
This is great stuff.
I am running into a problem when trying to sort a list of objects by a property that returns an Enum. I was wondering if you have any thoughts on how this could be done?
Keep up the good work.
-Rich
|
|
|
|
|
If you can post or email me a test case, I'll make it pass
|
|
|
|
|
Thanks Marc. Just sent a test case.
|
|
|
|
|
i Marc,
Thanks for the quick response. This could really get me out of a jam.
I modified the person class and added an Enum for called "Sex."
I versioned the test program (EnumProgram) and loaded up two people and set the "Sex" property for each. You'll see the error when you attempt to sort on "Sex."
Modified Person.cs
------------------
using System;
namespace DynamicComparerSample
{
///
/// Added this enum for test purposes
///
public enum Sex
{
Female,Male
}
public class Person: IComparable<person>
{
private Sex sex;
public Sex Sex
{
get { return sex; }
set { sex = value; }
}
private string firstName;
public string FirstName
{
get { return firstName; }
set { firstName = value; }
}
private string lastName;
public string LastName
{
get { return lastName; }
set { lastName = value; }
}
private double age;
public double Age
{
get { return age; }
set { age = value; }
}
public Person(string firstName, string lastName, double age)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public Person(string firstName, string lastName, double age, Sex sex)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.sex = sex;
}
public int CompareTo(Person other)
{
if (other == null)
return 1;
int diff = this.lastName.CompareTo(other.lastName);
if (diff == 0)
diff = this.firstName.CompareTo(other.firstName);
if (diff == 0)
diff = this.age.CompareTo(other.age);
return diff;
}
}
}
Modified Programs.cs
--------------------
using System;
using System.Collections.Generic;
using System.Text;
using DynamicComparer;
using System.Reflection;
using System.Diagnostics;
namespace DynamicComparerSample
{
class Program
{
static readonly string[] firstnames = { "Matt", "James", "David", "Homer", "Bart", "Maggie", "Marge", "Lisa", "Monty", "Barney", "Frank", "Phillip" };
static readonly string[] lastnames = { "Groening", "Brooks", "Cohen", "Simpson", "Burns", "Gumble", "Grimes", "Fry" };
static void Main(string[] args)
{
bool exit = false;
while (!exit)
{
Console.CursorVisible = false;
Console.Write("\n\n\n[T]est, [B]enchmark, or E[x]it?");
ConsoleKey option = new ConsoleKey();
while (option != ConsoleKey.T && option != ConsoleKey.B && option != ConsoleKey.X)
option = Console.ReadKey(true).Key;
Console.CursorVisible = true;
Console.WriteLine();
switch (option)
{
case ConsoleKey.T:
TestIt();
break;
case ConsoleKey.B:
BenchIt();
break;
case ConsoleKey.X:
exit = true;
break;
}
}
}
private static void TestIt()
{
Random rnd = new Random();
Console.Clear();
// Make the collection.
List<person> persons = new List<person>(2);
//Just populate the list with two people using new constuctor to add sex
persons.Add(new Person("Homer","Simpson",40,Sex.Male));
persons.Add(new Person("Wilma","Flinstone",80,Sex.Female));
Console.WriteLine("The following properties are available for sorting:");
foreach (PropertyInfo pi in typeof(Person).GetProperties())
{
if (!typeof(IComparable).IsAssignableFrom(pi.PropertyType))
continue;
Console.WriteLine("{0} ({1})", pi.Name, pi.PropertyType.FullName);
}
// Define which properties should be sorted on (must be IComparable).
// Note: You could either make your own SortProperty array and pass that in or you could make an sql-like "order by" string and pass that to the method.
Console.Write("\nPlease enter an \"order by\" clauses (case-sensitive): ");
string orderBy = Console.ReadLine();
try
{
// Create the comparer for Person types and define the sort fields.
DynamicComparer<person> comparer = new DynamicComparer<person>(orderBy);
// Print the unsorted list.
Console.WriteLine("\nUnsorted:");
foreach (Person p in persons)
Console.WriteLine("{0,-7} {1,-8} | {2,2} years old", p.FirstName, p.LastName, p.Age, p.Sex);
// Sort the list.
try
{
// Note: you could also call the sort method like: Sort(comparer) but calling it with Sort(comparer.Compare) is faster.
persons.Sort(comparer.Compare);
}
catch (Exception ex)
{
Console.WriteLine("\n{0}", ex.ToString());
}
// Print the sorted list.
Console.WriteLine("\nSorted:");
foreach (Person p in persons)
Console.WriteLine("{0,-7} {1,-8} | {2,2} years old", p.FirstName, p.LastName, p.Age, p.Sex);
}
catch (Exception ex)
{
Console.WriteLine("\n{0}", ex.ToString());
}
}
private static void BenchIt()
{
Random rnd = new Random(12345); // repeatable seed...
List<person> unsortedPersons = new List<person>(500000);
for (int i = 0; i < unsortedPersons.Capacity; i++)
unsortedPersons.Add(new Person(firstnames[rnd.Next(firstnames.Length)], lastnames[rnd.Next(lastnames.Length)], rnd.NextDouble() * 100d));
BenchOne(unsortedPersons, "Dynamic with field age: ", new DynamicComparer<person>("age"));
BenchOne(unsortedPersons, "Dynamic with property Age: ", new DynamicComparer<person>("Age"));
BenchOne(unsortedPersons, "Dynamic with field lastName: ", new DynamicComparer<person>("lastName"));
BenchOne(unsortedPersons, "Dynamic with property LastName: ", new DynamicComparer<person>("LastName"));
BenchOne(unsortedPersons, "Dynamic with field lastName,firstName,age: ", new DynamicComparer<person>("lastName,firstName,age"));
BenchOne(unsortedPersons, "Dynamic with property LastName,FirstName,Age: ", new DynamicComparer<person>("LastName,FirstName,Age"));
BenchOne(unsortedPersons, "Dynamic object level: ", new DynamicComparer<person>(string.Empty));
BenchOne(unsortedPersons, "Builtin: ", null);
}
private static void BenchOne(List<person> unsortedPersons, string what, DynamicComparer<person> comparer)
{
List<person> sortedPersons = new List<person>(unsortedPersons.Count);
foreach (Person p in unsortedPersons)
sortedPersons.Add(p);
GC.Collect(GC.MaxGeneration);
GC.WaitForPendingFinalizers();
GC.Collect(GC.MaxGeneration);
GC.WaitForPendingFinalizers();
Stopwatch watch = new Stopwatch();
if (comparer != null)
{
watch.Start();
sortedPersons.Sort(comparer);
watch.Stop();
}
else
{
watch.Start();
sortedPersons.Sort();
watch.Stop();
}
Console.WriteLine(what + watch.Elapsed);
}
}
}
|
|
|
|
|
Okay... looks like Enum values need to be boxed first... I've updated the code and posted a new version here. I'll update my Utilities library too.
For information, the new CreateDynamicCompareMethod looks like this:
private DynamicMethod CreateDynamicCompareMethod(SortProperty[] sortProperties)
{
DynamicMethod dm = new DynamicMethod("DynamicCompare"
, MethodAttributes.Static | MethodAttributes.Public, CallingConventions.Standard,
typeof(int), new Type[] { typeof(T), typeof(T) }, typeof(T), false);
dm.InitLocals = false;
#region Generate IL for dynamic method
Dictionary<Type, LocalBuilder> localVariables = new Dictionary<Type, LocalBuilder>();
ILGenerator ilGen = dm.GetILGenerator();
if (sortProperties.Length > 0)
{
Label lbl = ilGen.DefineLabel();
int numberLeft = sortProperties.Length;
foreach (SortProperty property in sortProperties)
{
Type valueType = property.ValueType;
ilGen.Emit(OpCodes.Ldarg_0);
if (property.Get != null)
{
ilGen.EmitCall(OpCodes.Callvirt, property.Get, null);
}
else
{
ilGen.Emit(OpCodes.Ldfld, property.Field);
}
if (valueType.IsEnum)
{
ilGen.Emit(OpCodes.Box, valueType);
}
else if (valueType.IsValueType && !property.IsNullable)
{
LocalBuilder localBuilder;
if (!localVariables.TryGetValue(valueType, out localBuilder))
{
localBuilder = ilGen.DeclareLocal(valueType);
localVariables.Add(valueType, localBuilder);
}
int localIndex = localBuilder.LocalIndex;
ilGen.Emit(OpCodes.Stloc, localIndex);
ilGen.Emit(OpCodes.Ldloca_S, localIndex);
}
ilGen.Emit(OpCodes.Ldarg_1);
if (property.Get != null)
{
ilGen.EmitCall(OpCodes.Callvirt, property.Get, null);
}
else
{
ilGen.Emit(OpCodes.Ldfld, property.Field);
}
if (valueType.IsEnum)
{
ilGen.Emit(OpCodes.Box, valueType);
}
if (property.IsNullable)
{
MethodInfo elementCompare = typeof(Nullable).GetMethod("Compare");
elementCompare = elementCompare.MakeGenericMethod(valueType.GetGenericArguments()[0]);
ilGen.EmitCall(OpCodes.Call, elementCompare, null);
}
else
{
MethodInfo elementCompare = valueType.GetMethod("CompareTo", new Type[] { valueType });
ilGen.EmitCall(OpCodes.Callvirt, elementCompare, null);
}
if (property.Descending)
ilGen.Emit(OpCodes.Neg);
if (--numberLeft > 0)
{
ilGen.Emit(OpCodes.Dup);
ilGen.Emit(OpCodes.Brtrue_S, lbl);
ilGen.Emit(OpCodes.Pop);
}
}
ilGen.MarkLabel(lbl);
}
else
{
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Ldarg_1);
MethodInfo instanceCompare = typeof(T).GetMethod("CompareTo", new Type[] { typeof(T) });
ilGen.EmitCall(OpCodes.Callvirt, instanceCompare, null);
}
ilGen.Emit(OpCodes.Ret);
#endregion
return dm;
}
|
|
|
|
|
Marc,
Your the man!. Works like a champ!
Thanks,
Rich
|
|
|
|
|