• 快速创建 IEqualityComparer 实例:改进


    两年前,我写了篇文章《快速创建 IEqualityComparer<T> 和 IComparer<T> 的实例》,文中给出了一个用于快速创建 IEqualityComparer<T> 实例的类 Equality<T>。

    在后来的使用中发现了一些不足,在此进行一些改进,以便更好的使用。原文中的 Equality<T> 实现如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public static class Equality<T>
    {
        public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector)
        {
            return new CommonEqualityComparer<V>(keySelector);
        }
        public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector, IEqualityComparer<V> comparer)
        {
            return new CommonEqualityComparer<V>(keySelector, comparer);
        }
    
        class CommonEqualityComparer<V> : IEqualityComparer<T>
        {
            private Func<T, V> keySelector;
            private IEqualityComparer<V> comparer;
    
            public CommonEqualityComparer(Func<T, V> keySelector, IEqualityComparer<V> comparer)
            {
                this.keySelector = keySelector;
                this.comparer = comparer;
            }
            public CommonEqualityComparer(Func<T, V> keySelector)
                : this(keySelector, EqualityComparer<V>.Default)
            { }
    
            public bool Equals(T x, T y)
            {
                // 此处未处理参数 x 和 y 为空的情况
                return comparer.Equals(keySelector(x), keySelector(y));
            }
            public int GetHashCode(T obj)
            {
                // 此处未处理参数 obj 为空的情况
                return comparer.GetHashCode(keySelector(obj));
            }
        }
    }

    代码中的问题使用红色粗体标出。

    在改进之前,我们需要先弄清两个关于 null 值的两个问题:

    关于 null 的两个问题

    将定有一个 Person 类:

    1
    2
    3
    4
    public class Peron
    {
        public string Name { get; set; }
    }

    问题一,两个 null 值是否相等?

    1
    2
    3
    4
    5
    6
    7
    8
    Peron p1 = new Peron { Name = null };
    Peron p2 = new Peron { Name = null };
    
    Peron p3 = null;
    Peron p4 = null;
    
    bool b1 = p1.Name == p2.Name;
    bool b2 = p3 == p4;

    请告诉我 b1 和 b2 的值。

    问题二,为 null 时 HashCode 应该是什么?

    1
    2
    var h1 = StringComparer.InvariantCulture.GetHashCode(p1.Name);
    var h2 = EqualityComparer<Peron>.Default.GetHashCode(p3);

    请告诉我 h1 和 h2 的值。

    建议大家想下这两个问题,答案就不给出了,自行调试吧。

    你的答案和调试得出的结果可能会有出入,如果这样你得好好思考下了。

    Equality<T> 改进后的代码

    改进后,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
        public static class Equality<T>
        {
            public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector)
            {
                return new CommonEqualityComparer<V>(keySelector);
            }
            public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector, IEqualityComparer<V> comparer)
            {
                return new CommonEqualityComparer<V>(keySelector, comparer);
            }
    
            class CommonEqualityComparer<V> : IEqualityComparer<T>
            {
                private Func<T, V> keySelector;
                private IEqualityComparer<V> comparer;
    
                public CommonEqualityComparer(Func<T, V> keySelector, IEqualityComparer<V> comparer)
                {
                    this.keySelector = keySelector;
                    this.comparer = comparer;
                }
                public CommonEqualityComparer(Func<T, V> keySelector)
                    : this(keySelector, EqualityComparer<V>.Default)
                { }
    
                public bool Equals(T x, T y)
                {
                    if (x == null || y == null) return false;
                    return comparer.Equals(keySelector(x), keySelector(y));
                }
                public int GetHashCode(T obj)
                {
                    if (obj == null) return 0; 
                    return comparer.GetHashCode(keySelector(obj));
                }
            }
        }

    以上代码黄色高亮部分为新加入代码。

    用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var personNameComparer = Equality<Peron>.CreateComparer(p => p.Name);
    //
    Peron p5 = new Peron { Name = "Bob" };
    Peron p6 = new Peron { Name = "Tom" };
    var b3 = personNameComparer.Equals(p5, p6); // false
    //
    Peron p7 = null;
    Peron p8 = null;
    var b4 = personNameComparer.Equals(p7, p8);  // false

    第 28 行代码

    此行代码会有很大争议,它会影响 p7 与 p8 比较的结果 b4。

    也许有的朋友认为应该将这行代码修改为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    if(x== null)
    {
        if (y == null) return true;
        else return false;
    }
    else
    {
        if (y == null) return false;
        else return comparer.Equals(keySelector(x), keySelector(y));
    }

    这样得出 b4 的值为 true.

    我不赞同这种方式,我的观点是:“p=>p.Name”指定使用 Person 的 Name 进行相等比较,Person若不存在(值为 null), Name 更不存在,也谈不上相等,所以应返回 false。

    当然还有另一种想法,Person 不存在,没法比,应该抛出异常。

    第 33 行代码

    也可以写成:

    1
    return RuntimeHelpers.GetHashCode(null);

    RuntimeHelpers 类在 System.Runtime.CompilerServices 命名空间下,我在反编译 Object 时,在 GetHashCode() 方法中发现了它。

    复杂情况下的使用

    一位园友问我这样一个问题,如下两个类:

    1
    2
    3
    4
    5
    6
    7
    8
    public class Employee
    {
        public School School { get; set; }
    }
    public class School
    {
        public string City { get; set; }
    }

    要创建 Employee 的相等比较器,根据其学校(School)的所在城市(City)。不考虑一个 Employee 多个 School 的情况,但要考虑 Employee 的 School 属性为 null 的情况(可能没上过学)。

    用以下方式创建:

    1
    var employeeComparer = Equality<Employee>.CreateComparer(i => i.School.City);

    运行时,可能会出错。执行比较时,遇到 Employee 的 School 属性为 null ,便会抛出 NullReferenceException。

    一种可行的写法是:

    1
    2
    var companylComparer = Equality<School>.CreateComparer(i => i.City);
    var employeeComparer = Equality<Employee>.CreateComparer(i => i.School, companylComparer);

    是的,分两步。也许是麻烦了些,不过试想下如果没有 Equality<T> 类的帮助,如果实现这个这个相等比较器?相当麻烦,不信可以试着写下。

    简单测试下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var v0 = new Employee { School = new School { City = "Beijing" } };
    var v1 = new Employee { School = new School { City = "Beijing" } };
    var v2 = new Employee { School = new School { City = "Shanghai" } };
    var v3 = new Employee { School = null };
    var v4 = new Employee { School = null };
    
    var b1 = employeeComparer.Equals(v0, v1);   // true
    var b2 = employeeComparer.Equals(v0, v2);   // false
    var b3 = employeeComparer.Equals(v0, v3);   // false
    var b4 = employeeComparer.Equals(v3, v4);   // false

    再搞复杂一点

    把前面的 City 变成一个类,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Employee
    {
        public School School { get; set; }
    }
    public class School
    {
        public City City { get; set; }
    }
    public class City
    {
        public string Name { get; set; }
        public string Country { get; set; }
    }

    还是要求创建 Employee 的相等比较器,根据 Employee 的 School 的 City 的 Country 来判断。要考虑各引用属性的为 null 时的情形。

    还不过瘾,就再加点难度! Country 比较时不考虑大小写。

    嘻嘻,有谁能告诉我如何创建,可以使用本文中的 Equality<T>,也可以不用。当然,越简洁越好。

    知道的话,请回复我,非常期待你的参与!

    后记

    终于赶在最后一分钟完成了2013年最后一篇文章。三更半夜,行文仓促,如有疏漏,请多包涵。

    祝大家 2014 年新年快乐!有更大的收获!

  • 相关阅读:
    团队项目01应用场景
    团队项目冲刺第一阶段03
    团队冲刺第一阶段02
    团队冲刺第一阶段01
    基于 Scrapy-redis 的分布式爬虫详细设计
    创建CrawlSpider爬虫简要步骤
    如何将redis中的数据导入到本地MongoDB和MySQL数据库
    远程访问ubuntu下mysql的问题
    vi命令汇总
    Chrome/FireFox处理JSON的插件
  • 原文地址:https://www.cnblogs.com/ldp615/p/quickly-create-instance-of-iequalitycomparer-improvement.html
Copyright © 2020-2023  润新知