繁体   English   中英

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

[英]Why should I not implement Equals and GetHashCode using reflection?

我有一些带有一堆字段的对象,我发现自己必须实现 GetHashCode 和 Equals。 尽管每个字段都是手动的,但 go 很痛苦,所以我这样写:

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

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

以下是我避免这条路线的几个原因

  • 比较字段而不是属性更可靠
  • 如果两个对象是相同的引用(您正在使用==),那么您的代码会假设两个对象被认为是相等的。 情况并非如此,因为许多类型通过.Equals实现值相等。 将两个不同的参考文献视为Equals并且将超过您的测试是非常可能和合法的。
  • 如果通过代码库以广泛的方式使用这种形式的Equality,当对象图具有循环时,它将很容易导致无限递归。
  • GetHashCode方法忽略属性可以为null

下面是一个类型的具体示例,它将在您的应用程序中导致无限递归

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

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

任何值类型属性都将被GetValue调用装箱,这意味着即使它们具有相同的值,它们也永远不会相等。

您可以通过调用静态Equals(x,y)方法来避免这种情况 - 如果需要,它将x.Equals(y)虚拟x.Equals(y)方法 - 而不是使用非虚拟==运算符,它将始终测试在这种情况下引用相等。

if (!object.Equals(p.GetValue(obj, null), p.GetValue(this, null)))
    return false;
  1. 它可能会给出条件差的哈希(并非所有属性在确定对象标识时都相同。)

  2. 如当前实现的,哈希计算可能溢出。

如果您的对象只有在所有属性相等时才相等,那么继续。 但我对此表示怀疑。 例如,员工的员工ID是唯一的。 如果执行此操作,您将无法比较员工数据的更改。

由于您不仅要实现Equals还要实现GetHashCode这意味着您手中的内容是不可变的structclass

您可能还知道反射比手动编写的代码要慢一些这一事实,并且您知道您的情况完全属于一般 95% 的情况,性能不是问题。

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

那是因为:

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

但是,这很棘手。

以下是如何以实际可行的方式进行操作。

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

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

按如下方式使用它:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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