一个动态、通用、类型安全的比较器
一个 IComparer 实现,它允许通过任意数量和顺序的属性进行比较。 类型安全是驱动力。
引言
在本文中,我将介绍一个泛型、动态和类型安全的 IComparer<T>
对象实现。
- 通用:它是一个 .NET 泛型类。
- 动态:排序顺序可在运行时设置和更改。
- 类型安全:安全用于编译时检查、重构,并且不涉及字符串。
类型安全体现在几个方面,因为编译器执行检查以验证构成排序顺序的属性是否满足以下要求
- 它们确实存在于被比较的类型上。
- 调用者可以访问它们(不是
private
等)。 - 它们的类型实现了
IComparable
。
Using the Code
使用该代码只需实例化一个 DynamicComparer<T>
对象,并调用其 SortOrder
方法来指定比较中使用的属性的顺序。 这是一个代码示例(关键行已突出显示)
public class Person
{
public Person(string firstName, string lastName, int age)
{
FirstName = firstName;
LastName = lastName;
Age = age;
}
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public override string ToString()
{
return FirstName + " " + LastName + ", " + Age;
}
}
class Program
{
static void Main(string[] args)
{
var comp = new DynamicComparer<Person>();
Person[] ppl = new Person[] {
new Person("Aviad","P.",35),
new Person("Goerge","Smith",33),
new Person("Harry","Burke",30),
new Person("Harry","Smith",20),
new Person("George","Harrison",19)
};
comp.SortOrder(x => x.FirstName, x => x.LastName, x => x.Age);
Demo(comp, ppl);
comp.SortOrder(x => x.FirstName, x => x.Age, x => x.LastName);
Demo(comp, ppl);
comp.SortOrder(x => x.Age);
Demo(comp, ppl);
}
private static void Demo(DynamicComparer<Person> comp, Person[] ppl)
{
Console.WriteLine(comp);
Console.WriteLine("------------------------");
Array.Sort(ppl, comp);
PrintPeople(ppl);
}
private static void PrintPeople(Person[] ppl)
{
foreach (var p in ppl)
Console.WriteLine(p);
}
}
请注意,排序顺序是如何指定为 lambda 表达式列表的,这是此类类型安全的主要因素。
代码特技 - 有趣的部分
魔力是通过使用 lambda 表达式与表达式树相结合来实现的,以强制编译器在编译时执行必要的检查,以确保一切正常。
这是 SortOrder
方法的实现
public void SortOrder(params Expression<Func<T, IComparable>>[] sortProperties)
{
sortOrder.Clear();
sortOrderNames.Clear();
sortOrder.AddRange(sortProperties.Select(x => x.Compile()));
sortOrderNames.AddRange(sortProperties.Select(x => GetNameFromExpression(x)));
}
请注意,它期望获得一系列 lambda 表达式,这些表达式接受要比较的类型的对象,并返回一个 IComparable
。 当调用此函数时,编译器会确保使用的 lambda 表达式满足这些条件。 如果我们尝试引用不存在的属性,或者我们引用的属性无法转换为 IComparable
,它将发出错误(实际上,如果遵循 intellisense,将会阻止该错误)。 此外,它将确保该属性在 public
/private
可访问性方面是可以访问的。
这是 Compare
方法的实现
public int Compare(T x, T y)
{
foreach (var l in sortOrder)
{
int c = l(x).CompareTo(l(y));
if (c != 0) return c;
}
return 0;
}
请注意,我们只是按顺序遍历已编译的 lambda 表达式,并使用它们从要比较的两个操作数中检索属性值。 然后我们对返回的值调用 IComparable.CompareTo
,如果结果告诉我们这些值是不同的,我们返回差异,否则,我们继续寻找一个,如果没有找到则返回 0
。
SortOrder
方法还有另一个重载,它接受一系列字符串,这不是使用此类的推荐方式,但它适用于那些需要它的人,并且具有教育价值。 这是实现
public void SortOrder(params string[] sortProperties)
{
string err = sortProperties.FirstOrDefault(x => uncomparable.Contains(x));
if (err != null)
throw new InvalidOperationException
("Property '" + err + "' does not implement IComparable");
err = sortProperties.FirstOrDefault(x => !properties.ContainsKey(x));
if (err != null)
throw new InvalidOperationException("Property '" + err + "' does not exist");
sortOrder.Clear();
sortOrderNames.Clear();
sortOrder.AddRange(sortProperties.Select(x => properties[x]));
sortOrderNames.AddRange(sortProperties);
}
在这里,我们进行一些运行时检查,并在某些情况下抛出异常。 这就是为什么这是使用此类的“不安全”方式,因为编译器将无法及时警告我们这些情况。 请注意,一旦我们确定一个属性可用于排序,我们就会从一个神秘的 properties
字典中检索它的 lambda 函数。 此字典在构造函数中组成,在构造函数中,我们使用反射来遍历每个属性并准备一个 lambda 表达式来检索它。 这是构造函数的实现
public DynamicComparer()
{
var ps = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
foreach (var p in ps)
{
// Only consider properties whose type is comparable (implements IComparable).
if (!typeof(IComparable).IsAssignableFrom(p.PropertyType))
{
// Save the names of uncomparable properties for later reference
// in case an attempt is made to sort by them.
uncomparable.Add(p.Name);
continue;
}
ParameterExpression pe = Expression.Parameter(typeof(T), "x");
MemberExpression me = Expression.MakeMemberAccess(pe, p);
Expression<Func<T, IComparable>> le;
if (!p.PropertyType.IsValueType)
{
le = Expression.Lambda<Func<T, IComparable>>(me, pe);
}
else
{
UnaryExpression ue = Expression.Convert(me, typeof(IComparable));
le = Expression.Lambda<Func<T, IComparable>>(ue, pe);
}
var f = le.Compile();
properties[p.Name] = (Func<T, IComparable>)f;
}
}
请注意 Expression
对象的巧妙用法,以构造合适的 lambda 表达式来检索每个属性,并注意适应属性是值类型还是引用类型。 值类型属性需要显式转换为 IComparable
。
关注点
如果构成排序顺序的任何属性是值类型,则存在性能问题,因为它们将在比较期间进行装箱。 这可能对您来说无关紧要,但无论如何,它就在那里。
另一个性能问题是为检索每个属性而执行方法调用的开销; 每次比较都有两个这样的方法调用。 这大约使比较所需的时间增加了一倍,但这取决于比较的具体情况(排序顺序、属性的值以及它们是值类型还是引用类型)。
请注意,尽管存在上述性能问题,但此比较器仍然比 LINQ 的 OrderBy
运算符快大约 2 倍,因此如果性能是一个问题,但灵活性需要保持,那么使用它仍然比使用 LINQ 快两倍。
在本文的下载中,有基准测试方法,显示了硬编码比较器(最快的方式)、动态比较器和 LINQ 之间的性能差异。
历史
- 3月30日 - 初始版本