快速创建 IEqualityComparer 实例:改进

时间:2023-12-26 16:21:25

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