两年前,我写了篇文章《快速创建 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 年新年快乐!有更大的收获!