【问题标题】:IEqualityComparer<T> that uses ReferenceEquals使用 ReferenceEquals 的 IEqualityComparer<T>
【发布时间】:2009-12-11 18:40:47
【问题描述】:

是否存在使用ReferenceEquals 的默认IEqualityComparer&lt;T&gt; 实现?

EqualityComparer&lt;T&gt;.Default 使用 ObjectComparer,后者使用 object.Equals()。在我的例子中,对象已经实现了IEquatable&lt;T&gt;,我需要忽略它并仅通过对象的引用进行比较。

【问题讨论】:

标签: c# .net iequalitycomparer referenceequals


【解决方案1】:

以防万一没有默认实现,这是我自己的:

由 280Z28 编辑:使用 RuntimeHelpers.GetHashCode(object) 的理由,你们中的许多人可能以前没有见过。 :) 这个方法有两个效果,使它成为这个实现的正确调用:

  1. 当对象为空时返回0。由于 ReferenceEquals 适用于空参数,因此比较器的 GetHashCode() 实现也应如此。
  2. 它以非虚拟方式调用Object.GetHashCode()ReferenceEquals 专门忽略了 Equals 的任何覆盖,因此 GetHashCode() 的实现应该使用与 ReferenceEquals 的效果相匹配的特殊方法,这正是 RuntimeHelpers.GetHashCode 的用途。

[结束 280Z28]

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

/// <summary>
/// A generic object comparerer that would only use object's reference, 
/// ignoring any <see cref="IEquatable{T}"/> or <see cref="object.Equals(object)"/>  overrides.
/// </summary>
public class ObjectReferenceEqualityComparer<T> : EqualityComparer<T>
    where T : class
{
    private static IEqualityComparer<T> _defaultComparer;

    public new static IEqualityComparer<T> Default
    {
        get { return _defaultComparer ?? (_defaultComparer = new ObjectReferenceEqualityComparer<T>()); }
    }

    #region IEqualityComparer<T> Members

    public override bool Equals(T x, T y)
    {
        return ReferenceEquals(x, y);
    }

    public override int GetHashCode(T obj)
    {
        return RuntimeHelpers.GetHashCode(obj);
    }

    #endregion
}

【讨论】:

  • 我唯一不喜欢的是 Default 属性违反了属性获取器的纯度假设。由于 CLR 在引用类的某些成员之前不会为类运行静态初始化程序,因此我添加的内联初始化具有与属性相同的有效延迟初始化效果,但不违反纯度约束。我也密封了类型。
  • 最后但同样重要的是,我从 EqualityComparer 派生了 IEqualityComparer(非泛型)的实现。附带说明一下,这个确切的类型是来自 .NET 4 的 System.Xaml 程序集中的 internal 类(在 System.Xaml.Schema 命名空间中)。
  • 我认为Default 标识符令人困惑。为什么无缘无故隐藏继承的成员。我会将Default 属性重命名为Instance(或完全不使用它,实例构造函数为public)。
  • EqualityComparer&lt;T&gt; 继承不是一个好主意。静态成员“继承”真的很混乱;所以像这样重用Default 不是一个好主意。此外,虚方法比普通方法慢;并且由于这可能是一种将在紧密循环中使用的类型,为什么要增加不必要的开销呢?最后,考虑到你已经拥有的东西,实现IEqualityComparer 是微不足道的,那么为什么不保持简单并避免依赖呢?
  • @EamonNerbonne 无论如何,这些方法都是虚拟调用,因为该类型几乎完全通过 IEqualityComparer 接口使用。一旦方法在 vtable 中,不管它在哪里实现,调用开销都是一样的。
【解决方案2】:

我认为是时候将之前的答案实现更新为 .Net4.0+,由于 IEqualityComparer&lt;in T&gt; 接口上的逆变性,不再需要泛型:

using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

public sealed class ReferenceEqualityComparer
    : IEqualityComparer, IEqualityComparer<object>
{
    private ReferenceEqualityComparer() { }

    public static readonly ReferenceEqualityComparer Default
        = new ReferenceEqualityComparer();

    public /*new*/ bool Equals(object x, object y)
    {
        return x == y; // This is reference equality! (See explanation below)
    }

    public int GetHashCode(object obj)
    {
        return RuntimeHelpers.GetHashCode(obj);
    }
}

现在只需要存在一个用于所有引用相等检查的实例,而不是像以前那样为每个类型T 存在一个。

您不再需要在每次想要使用它时都指定T,并且还避免了不必要的通用运行时类型的污染。


至于x == y为什么是引用相等,是因为==operator是一个静态方法,也就是说它在编译时被解析,而在编译时xy参数类型为object

事实上这就是Object.ReferenceEquals(object, object)方法sourcecode的样子:

public static bool ReferenceEquals(object objA, object objB) {
    return objA == objB;
}

为不熟悉Covariance and Contravariance概念的人澄清一下...

class MyClass
{
    ISet<MyClass> setOfMyClass = new HashSet<MyClass>(ReferenceEqualityComparer.Default);
}

...上面的代码编译;请注意,它确实HashSet&lt;object&gt;

【讨论】:

  • 引用:“thanks to contravariance on the IEqualityComparer&lt;in T&gt; interface”不,IEqualityComparer&lt;T&gt;T 中是协变的。
  • @TysonWilliams 不,我做对了。见MSDN: Variance in Generic Interfaces
  • 这会给出警告 CS0108:'ReferenceEqualityComparer.Equals(object, object)' 隐藏继承的成员 'object.Equals(object, object)'。如果打算隐藏,请使用 new 关键字。
  • 显式实现接口,完全解决隐藏问题。我想没有人会直接使用ReferenceEqualityComparer.Default.Equals(a, b) 而不是ReferenceEquals(a, b)。如果传递到一个期待 IEqualityComparer&lt;T&gt; 的地方 - 一切都会好起来的。
  • 否,因为它使用了可能被覆盖的object.Equalsobject.GetHashCode
【解决方案3】:
【解决方案4】:

这是 C# 6 及更高版本的简单实现:

public sealed class ReferenceEqualityComparer : IEqualityComparer, IEqualityComparer<object>
{
    public static ReferenceEqualityComparer Default { get; } = new ReferenceEqualityComparer();

    public new bool Equals(object x, object y) => ReferenceEquals(x, y);
    public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
}

或确保它仅可用于引用类型的通用版本:

public sealed class ReferenceEqualityComparer<T> : IEqualityComparer<T> where T : class
{
    public static IEqualityComparer<T> Default { get; } = new ReferenceEqualityComparer<T>();

    public bool Equals(T x, T y) => ReferenceEquals(x, y);
    public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj);
}

【讨论】:

  • new 关键字实际上应该被使用。正如警告所说的"Use the new keyword if hiding was intended.",但这里不打算隐藏,所以它会向维护者传达错误的意图。这是一个方法名称冲突,导致 un 有意隐藏,幸运的是,这恰好是无害的。正确的做法是使用编译指示关闭该行的警告并用注释记录它。除此之外,我喜欢 C#6 的简洁性。 :)
  • 我不同意。在这种情况下,new 会阻止警告。添加new,它会在没有警告的情况下编译,更重要的是,它可以工作。如果有的话,这是编译器诊断的问题。
  • 编译器不是问题——警告应该在那里——这就是我的意思。可能使用编译指示禁用警告,因为警告是应得的。为方便起见,您可以按照自己的方式进行操作,可能没有人会被打扰,但这在技术上是错误的。我们可以同意或不同意在这里不正确地使用new,因为它是无害的。但事实上我是正确的,事实不是基于意见的。澄清这是语言的限制。我们可以使用显式接口实现来绕过它,但这也可能是不可取的。无害!= 错误警告。
  • 它的工作原理是给定的,因为使用 new 关键字纯粹是语言希望您用来确认您的意图的装饰属性。它对生成的代码的影响为零。它只为人类而存在。我们这里的意图是实现IEqualityComparer&lt;object&gt;.Equals not 来隐藏Object.Equals。使用new 表示后者,这是错误的。
  • 方法没有隐藏Object.Equals。问题是具有相同签名的IEqualityComparer.EqualsIEqualityComparer&lt;object&gt;.Equals 之间的冲突。选项是:重复成员(具有显式实现),添加#pragmas,或者只使用new。如有疑问,我通常倾向于使用更少代码的解决方案,因为更少的代码往往会导致更少的错误。各有各的。但是,正如您所说,此实现在技术上没有任何错误。检查the MSIL 确定。
【解决方案5】:

Microsoft 在System.Data.Entity.Infrastructure 中提供ObjectReferenceEqualityComparer。 只需使用ObjectReferenceEqualityComparer.Default 作为比较器。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-08-20
    • 1970-01-01
    • 2013-09-14
    • 2011-10-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多