65.9K
CodeProject 正在变化。 阅读更多。
Home

一个动态、通用、类型安全的比较器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (12投票s)

2010年3月30日

CPOL

4分钟阅读

viewsIcon

26815

downloadIcon

246

一个 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 - 初始版本
© . All rights reserved.