IEqualityComparer that uses ReferenceEquals

2019-01-07 15:33发布

问题:

Is there a default IEqualityComparer<T> implementation that uses ReferenceEquals?

EqualityComparer<T>.Default uses ObjectComparer, which uses object.Equals(). In my case, the objects already implement IEquatable<T>, which I need to ignore and compare by object's reference only.

回答1:

Just in case there is no default implementation, this is my own:

Edit by 280Z28: Rationale for using RuntimeHelpers.GetHashCode(object), which many of you probably haven't seen before. :) This method has two effects that make it the correct call for this implementation:

  1. It returns 0 when the object is null. Since ReferenceEquals works for null parameters, so should the comparer's implementation of GetHashCode().
  2. It calls Object.GetHashCode() non-virtually. ReferenceEquals specifically ignores any overrides of Equals, so the implementation of GetHashCode() should use a special method that matches the effect of ReferenceEquals, which is exactly what RuntimeHelpers.GetHashCode is for.

[end 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
}


回答2:

I thought it was time to update the previous answers implementation to .Net4.0+ where it simplifies by becoming non-generic thanks to contravariance on the IEqualityComparer<in T> interface:

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

public sealed class ReferenceEqualityComparer
    : IEqualityComparer, IEqualityComparer<object>
{
    public static readonly ReferenceEqualityComparer Default
        = new ReferenceEqualityComparer(); // JIT-lazy is sufficiently lazy imo.

    private ReferenceEqualityComparer() { } // <-- A matter of opinion / style.

    public 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);
    }
}

Now there only needs to exist one instance for all your reference-equality checking instead of one for each type T as was the case before.

Also you save typing by not having to specify T every time you want to use this!


To clarify for those who are not familiar with the concepts of Covariance and Contravariance...

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

...will work just fine. This is not limited to e.g. HashSet<object> or similar (in .Net4.0).


Also for anyone wondering why x == y is reference equality, it is because the ==operator is a static method, which means it is resolved at compile-time, and at compile-time x and y are of type object so here it resolves to the ==operator of object - which is the real reference equality method. (In fact the Object.ReferenceEquals(object, object) method is simply a redirect to the object equals operator.)



回答3:

Here's a simple implementation for 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);
}

EDIT (You don't have to read this unless you're interested in the comments below)

@AnorZaken devoted many paragraphs to the three letters of the new modifier here. Let's summarise.

The single defined instance Equals(object,object) method implements the Equals method of the two declared interfaces for this type, IEqualityComparer and its generic counterpart IEqualityComparer<object>. The signatures are identical, so this definition satisfies both interfaces.

The instance method ReferenceEqualityComparer.Equals(object,object) hides the static object.Equals(object,object) method.

Without new the compiler warns about this. What does this actually mean?

It means that if you want to call the static object.Equals methods, you cannot call it on an instance of ReferenceEqualityComparer. Is this a big deal?

No. In fact it's desired behaviour. It means that if you want to call object.Equals(a,b) you cannot do it via code such as ReferenceEqualityComparer.Default.Equals(a,b). That code is clearly requesting reference equality -- no one would reasonably expect it to perform default/value equality. Why wouldn't you just code the more explicit object.Equals(a,b) anyway? So the use of new provides sensible and desirable behaviour, and allows compilation with no warnings.

How else could you suppress the warning? If you use a #pragma warning disable 108/#pragma warning restore 108 then the outcome is the same as using new, except you've added a bunch more noise to your code. new suffices and explains the intent more clearly to others.

Alternatively you could use explicit implementations for the two interface Equals methods, but then if you used ReferenceEqualityComparer.Default.Equals(a,b) you wouldn't have reference equality at all.

In reality, hiding static methods with instance methods is rarely a problem because static methods are dereferenced from a type specifier, not an instance specifier. That is, you use Foo.StaticMethod() not new Foo().StaticMethod(). Calling static methods from instances is unnecessary at best and misleading/incorrect at worst.

Further, for equality comparers, you rarely use their concrete types directly. Rather, you use them with APIs such as collections.

So whilst this was an interesting and at times confusing discussion, it was rather fruitless.