[英]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
並且將超過您的測試是非常可能和合法的。 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;
它可能會給出條件差的哈希(並非所有屬性在確定對象標識時都相同。)
如當前實現的,哈希計算可能溢出。
如果您的對象只有在所有屬性相等時才相等,那么繼續。 但我對此表示懷疑。 例如,員工的員工ID是唯一的。 如果執行此操作,您將無法比較員工數據的更改。
由於您不僅要實現Equals
還要實現GetHashCode
這意味着您手中的內容是不可變的struct
或class
。
您可能還知道反射比手動編寫的代碼要慢一些這一事實,並且您知道您的情況完全屬於一般 95% 的情況,性能不是問題。
在這種情況下,您絕對應該使用反射實現Equals
和GetHashCode
。
那是因為:
但是,這很棘手。
以下是如何以實際可行的方式進行操作。
查看代碼中的“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.