簡體   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