为什么不应该使用反射来实现Equals和GetHashCode方法?

9

我有一些带有许多字段的对象,我发现自己不得不实现GetHashCode和Equals。手动处理每个字段很痛苦,所以我像这样编写它们:

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

除了速度考虑之外,为什么我不应该像这样实现它们?

除了速度问题外,请注意并非所有正确实现 GetHashCodeEquals 的算法都等同于上述算法。另外,发布的代码存在几个问题。您可能会在多个位置取消引用 null。此外,您的 Equals 版本在相应属性之间使用引用相等性,这不是最常见的习惯用法。 - Ani
1
另一个 bug - 你正在检查此.GetType() 的属性,因此继承等价性可能会引入奇怪的行为(例如,aManager.Equals(anEmployee) 将测试不同于 anEmployee.Equals(aManager) 的属性)。此外,当你执行 anApple.Equals(anOrange) 时会发生什么?我没有测试过,但似乎在使用不相关类型调用 Equals 时会出现一些奇怪的行为。 - Chris Shaffer
哈希计算中存在非质数。我猜你的意思是要使用33。 :-) - Christian Hayter
@Christian 我实际上是指23,33也不是质数。 - stimms
哎呀,这会教我在评论前先想一想 :) - Christian Hayter
显示剩余2条评论
5个回答

8
以下是我避免使用此路线的几个原因:
  • 比较字段而不是属性更可靠
  • 您的代码错误地假设如果两个对象是同一引用,则它们被视为相等(您正在使用==)。但事实并非如此,因为许多类型通过.Equals实现值相等。两个不同的引用很可能被认为是相等的,这将使您的测试失败。
  • 如果在代码库中广泛使用此形式的相等性,当对象图具有循环时,它很容易导致无限递归。
  • GetHashCode方法忽略了属性可能为null
以下是一个具体示例,该类型会在您的应用程序中导致无限递归:
class C1 {
  public object Prop1 { get; set; }
};

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

5
任何值类型的属性都将被GetValue调用装箱,这意味着即使它们具有相同的值,它们也永远不会比较相等。
您可以通过调用静态的Equals(x,y)方法来避免这种情况--如果必要,它将推迟到虚拟的x.Equals(y)方法--而不是使用非虚拟的==运算符,在这种情况下始终测试引用相等性。
if (!object.Equals(p.GetValue(obj, null), p.GetValue(this, null)))
    return false;

错别字已经修正,但是 != 保留以确保你的答案有效。 - stimms
如果 anull,则 a.Equals(b) 将会抛出异常;而 Object.Equals(a, b) 则会返回 false - Tim Robinson

2
  1. 它可能会产生一个不良条件的哈希值(并非所有属性都相等以确定对象的身份)。

  2. 目前实现的哈希计算可能会溢出。


2
如果您的对象仅在所有属性相等时才相等,那么就继续吧。但我怀疑这一点。例如,员工由其员工ID唯一确定。如果您这样做,将无法比较员工数据中的更改。

0

由于您提到了不仅要实现 Equals,还要实现 GetHashCode,这意味着您手上的是不可变的 structs 或 classes。

您可能也知道反射比手写代码慢一点,并且您知道您的情况完全符合 95% 的不需要考虑性能的情况。

在这种情况下,您绝对应该使用反射实现 EqualsGetHashCode

原因如下:

  • 这将使您免于编写大量愚蠢重复的代码,并且
  • 它保证处理所有字段,而手动编写的代码可能会无意中省略某些字段,从而导致非常隐蔽的错误。

但是,这很棘手。

以下是确实有效的方法。

查看代码中的“PEARL”注释,以了解为什么它很棘手。

注意:您可以自己进行哈希码计算,但为了方便起见,我使用了System.HashCode。为了使这种便利对您可用,请使用NuGet添加对包Microsoft.Bcl.HashCode的引用。
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();
}

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

请按以下方式使用:

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

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