EqualityComparer<T>.Default与T.Equals的区别

38
假设我有一个通用的 MyClass<T>,需要比较两个类型为 <T> 的对象。通常我会这样做...
void DoSomething(T o1, T o2)
{
  if(o1.Equals(o2))
  {
    ...
  }
}

现在假设我的MyClass<T>有一个支持传入自定义IEqualityComparer<T>的构造函数,类似于Dictionary<T>。这种情况下,我需要执行...

private IEqualityComparer<T> _comparer;
public MyClass() {}
public MyClass(IEqualityComparer<T> comparer)
{
  _comparer = comparer;
}
void DoSomething(T o1, T o2)
{
  if((_comparer != null && _comparer.Equals(o1, o2)) || (o1.Equals(o2)))
  {
    ...
  }
}

为了去掉这个冗长的if语句,如果我可以让_comparer在使用常规构造函数时默认为“默认比较器”,那就太好了。我搜索了类似于typeof(T).GetDefaultComparer()的东西,但没有找到任何类似的内容。
我确实发现了EqualityComparer<T>.Default,我能用它吗?然后这段代码会怎样……
public MyClass()
{
  _comparer = EqualityComparer<T>.Default;
}
void DoSomething(T o1, T o2)
{
  if(_comparer.Equals(o1, o2))
  {
    ...
  }
}

使用o1 == o2是否可以在所有情况下提供与使用o1.Equals(o2)相同的结果?

(顺带一提,这是否意味着我还需要使用任何特殊的泛型约束来限制<T>?)


2
注意:如果 o1 是 null,o1.Equals(o2) 将会失败并抛出普遍存在的 NullReferenceException,而 EqualityComparer<T>.Default.Equals(o1, o2) 则可以愉快地返回 false。 - M.Stramm
@M.Stramm 这就是为什么我说“我会做类似于...的事情”,而不是“这是我的生产代码...”;-) - takrl
5个回答

52
它应该是相同的,但不能保证,因为它取决于类型T的实现细节。
解释:
如果没有对T进行约束,则o1.Equals(o2)将调用Object.Equals,即使T实现了IEquatable。
但是EqualityComparer.Default仅在T不实现IEquatable时才使用Object.Equals。如果它实现了该接口,则使用IEquatable.Equals。
只要T的Object.Equals实现只调用IEquatable.Equals,结果就是相同的。但在下面的示例中,结果不同:
public class MyObject : IEquatable<MyObject>
{
    public int ID {get;set;}
    public string Name {get;set;}

    public override bool Equals(object o)
    {
        var other = o as MyObject;
        return other == null ? false : other.ID == ID;
    }    

    public bool Equals(MyObject o)
    {
        return o.Name == Name;
    } 
}

现在,实现这样一个类就没有任何意义。但是,如果MyObject的实现者忘记重写Object.Equals,你将遇到同样的问题。
结论: 使用EqualityComparer.Default是一个好方法,因为您不需要支持有缺陷的对象!

23
任何同时实现了IEquatable.Equals和object.Equals方法且两者行为不同的类型,已经注定会出问题 :) - Marc Gravell
谢谢。根据我的用例,我甚至可以指定像 public MyClass<T> where T : IEquatable<T> 这样的约束条件。很好,你们都非常有帮助。 - takrl
2
那个返回语句看起来更好,像这样:return other != null && other.ID == ID; - nawfal
4
如果你重写了 Equals 方法,不要忘记也要重写 GetHashCode 方法。参考链接:http://stackoverflow.com/questions/23757210/c-sharp-dictionary-file-path-custom-equalitycomparer [编译器警告(级别3)CS0659](http://msdn.microsoft.com/en-us/library/xxhbfytk%28v=vs.90%29.aspx)。 - Philip Pittle
6
如果你继承了一个类(比如DependencyObject),而这个类已经使用sealed关键字封闭了它的object.Equals实现,那么你将无法避免这种情况。在这种情况下,重要的是要使用EqualityComparer<T>.Default来确保调用你自己实现的Equals方法。请注意,不要改变原始意思。 - Tim Pohlmann

4

默认情况下,在类中不被覆盖的情况下,Object.Equals(a,b) / a.Equals(b)会通过引用比较。

EqualityComparer<T>.Default将返回何种比较器取决于T。例如,如果T : IEquatable<>,则将创建相应的EqualityComparer<T>


使用IEquatable<T>而不是IComparable。 - Paul Creasey

3

是的,我认为使用EqualityComparer<T>.Default是明智的,因为它会根据类型T是否实现了IEquatable<T>接口来选择相应的实现,否则就使用Object.Equals方法的重载。您可以按照以下方式执行:

private IEqualityComparer<T> _comparer;
public IEqualityComparer<T> Comparer
{
    get { return _comparer?? EqualityComparer<T>.Default;}
    set { _comparer=value;}
}
public MyClass(IEqualityComparer<T> comparer)
{  
    _comparer = comparer;
}
void DoSomething(T o1, T o2)
{  
    if(Comparer.Equals(o1, o2))
    {
     ...
    }
}

2
您可以使用空值合并运算符 ?? 来缩短 if 语句,如果确实有必要的话。
  if ((_comparer ?? EqualityComparer<T>.Default).Equals(o1, o2))
  {

  }

9
这如何回答问题? - M.Stramm

2

如果您在构造对象时没有指定比较器,那么BCL中的Dictionary<>和其他泛型集合正是这样做的。这样做的好处是EqualityComparer<T>.Default会为IEquatable<T>类型、可空类型和枚举类型返回正确的比较器。如果T不是这些类型之一,则它将执行类似于您旧代码所做的简单Equals比较。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接