简体   繁体   中英

Why should I not implement Equals and GetHashCode using reflection?

I have some objects with a bunch of fields and I find myself having to implement GetHashCode and Equals. It is painful to go though each field manually so I wrote them like this:

public override int GetHashCode()
{
    int hash = 17;
    foreach (PropertyInfo p in GetType().GetProperties())
    {
        hash = hash * 23 + p.GetValue(this, null).GetHashCode();
    }
    return hash;
}

public override bool Equals(object obj)
{
    foreach (PropertyInfo p in GetType().GetProperties())
    {
        if (p.GetValue(obj, null) != p.GetValue(this, null))
            return false;
    }
    return true;
}

Other than speed considerations why shouldn't I implement them like this?

Here are a few reasons I would avoid this route

  • It's much more reliable to compare fields instead of properties
  • Your code makes the incorrect assumption that two objects are considered to be equal if they are the same reference (you are using ==). This is not the case as many types implement value equality via .Equals . It is very possible and legal for two different references to be considered Equals and would beat your test.
  • If this form of Equality is used in a wide spread manner through your code base it will very easily lead to infinite recursion when the object graph has cycles.
  • The GetHashCode method ignores that a property could be null

Below is a concrete example of a type which would cause infinite recursion in your application

class C1 {
  public object Prop1 { get; set; }
};

var local = new C1();
local.Prop1 = local;
var x = local.GetHashCode();  // Infinite recursion

Any value-type properties will be boxed by the GetValue calls, which means that they'll never compare as equal even if they have the same value.

You can avoid this by calling the static Equals(x,y) method -- which will then defer to the virtual x.Equals(y) method if necessary -- rather than using the non-virtual == operator, which will always test reference equality in this case.

if (!object.Equals(p.GetValue(obj, null), p.GetValue(this, null)))
    return false;
  1. It may give a poorly conditioned hash (not all properties are equal in determining object identity.)

  2. As currently implemented, the hash computation may overflow.

If your object is equal only if all properties are equal then go ahead. But I doubt it. Eg an employee is unique by it's employee Id. You will not be able to compare changes in employee data if you do this.

Since you are speaking of implementing not only Equals but also GetHashCode this means that what you have in your hands is immutable struct s or class es.

You are probably also aware of the fact that reflection is a bit slower than manually written code, and you know that your case falls squarely into the general 95% of cases where performance is not a concern.

In this case, you definitely should implement Equals and GetHashCode using reflection.

That's because:

  • it will save you from writing copious amounts of silly repetitive code, and
  • it is guaranteed to handle all fields, whereas manually written code might inadvertently omit some field, causing a very insidious bug.

However, it is tricky.

Here is how to do it in a way that actually works.

Look at the "PEARL" comments in the code to see why it is tricky.

Note: You can do the hash-code calculation by yourself, but I am using System.HashCode for convenience. To make this convenience available to you, use NuGet to add a reference to package Microsoft.Bcl.HashCode .

class ReflectionHelpers

#nullable enable
using Sys = System;
using SysReflect = System.Reflection;

public static bool MemberwiseEquals<T>( T a, object? b ) where T : notnull
{
    if( b == null )
        return false;
    if( ReferenceEquals( a, b ) )
        return true;
    Sys.Type type = typeof(T);
    Assert( a.GetType() == type );
    Assert( b.GetType() == type );
    foreach( SysReflect.FieldInfo fieldInfo in type.GetFields( //
            SysReflect.BindingFlags.Instance | //
            SysReflect.BindingFlags.Public | //
            SysReflect.BindingFlags.NonPublic ) )
    {
        object? value1 = fieldInfo.GetValue( a );
        object? value2 = fieldInfo.GetValue( b );
        if( fieldInfo.FieldType.IsPrimitive )
        {
            if( !value1.Equals( value2 ) )
                return false;
        }
        else
        {
            if( !DotNetHelpers.Equals( value1, value2 ) )
                return false;
        }
    }
    return true;
}

public static int MemberwiseGetHashCode<T>( T obj ) where T : notnull
{
    Sys.Type type = typeof(T);
    Assert( obj.GetType() == type );
    Sys.HashCode hashCodeBuilder = new Sys.HashCode();
    foreach( SysReflect.FieldInfo fieldInfo in type.GetFields( //
            SysReflect.BindingFlags.Instance | //
            SysReflect.BindingFlags.Public | //
            SysReflect.BindingFlags.NonPublic ) )
    {
        Assert( fieldInfo.IsInitOnly );
        object? fieldValue = fieldInfo.GetValue( obj );
        hashCodeBuilder.Add( fieldValue );
    }
    return hashCodeBuilder.ToHashCode();
}

class DotNetHelpers

using Sys = System;
using Legacy = System.Collections;

// PEARL: Arrays in C# implement `IEnumerable` but provide no implementation for `Equals()`!
//        This means that `object.Equals( array1, array2 )` will always return false, even if the arrays have identical contents!
//        This is especially sinister since arrays are often treated as `IEnumerable`, so you may have two instances of `IEnumerable`
//        which yield identical elements and yet the instances fail to return `true` when checked using `object.Equals()`.
//        The standing advice is to use `a.SequenceEqual( b )` to compare `IEnumerable`, which is retarded, due to the following reasons:
//          1. This will only work when you know the exact types of the objects being compared; it might suit application programmers who are perfectly
//             accustomed writing copious amounts of mindless application-specific code to accomplish standard tasks, but it does not work when you are
//             writing framework-level code, which operates on data without needing to know (nor wanting to know) the exact type of the data.
//          2. This will not work when the `IEnumerable`s in turn contain other `IEnumerable`s (or arrays) because guess what `SequenceEqual()` uses
//             internally to compare each pair of elements of the `IEnumerable`s? It uses `object.Equals()`, which miserably fails when comparing
//             instances of `IEnumerable`! Again, this might be fine for application programmers who will happily write thousands of lines of
//             application-specific code to compare application data having intimate knowledge of the structure of the data, but it does not work when
//             writing framework-level code.
//        This method fixes this insanity. It is meant to be used as a replacement for `object.Equals()` under all circumstances.
public new static bool Equals( object? a, object? b )
{
    if( ReferenceEquals( a, b ) )
        return true;
    if( a == null || b == null )
        return false;
    if( a.Equals( b ) )
        return true;
    if( a is Legacy.IEnumerable enumerableA && b is Legacy.IEnumerable enumerableB )
        return legacyEnumerablesEqual( enumerableA, enumerableB );
    return false;

    static bool legacyEnumerablesEqual( Legacy.IEnumerable a, Legacy.IEnumerable b )
    {
        Legacy.IEnumerator enumerator1 = a.GetEnumerator();
        Legacy.IEnumerator enumerator2 = b.GetEnumerator();
        try
        {
            while( enumerator1.MoveNext() )
            {
                if( !enumerator2.MoveNext() )
                    return false;
                if( !Equals( enumerator1.Current, enumerator2.Current ) )
                    return false;
            }
            if( enumerator2.MoveNext() )
                return false;
            return true;
        }
        finally
        {
            (enumerator1 as Sys.IDisposable)?.Dispose();
            (enumerator2 as Sys.IDisposable)?.Dispose();
        }
    }
}

Use it as follows:

class MyClass

public override bool Equals( object other ) => other is MyClass kin && Equals( kin );
public bool Equals( MyClass other ) => ReflectionHelpers.MemberwiseEquals( this, other );
public override int GetHashCode() => ReflectionHelpers.MemberwiseGetHashCode( this );

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM