Equals and GetHashCode are not overridden together
ID |
vbnet.correctness.equals_hashcode_override |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
low |
Resource |
Type Design |
Language |
VB.NET |
Description
Reports a type that overrides Equals without also overriding GetHashCode, and a type that
overrides GetHashCode without also overriding Equals. Both directions are reported, and
the pair has to be declared on the same type: an override is accepted only when its
counterpart appears in the same class or structure body, so inheriting one half from a base
type does not satisfy the check. Any Equals declared with Overrides counts as the
counterpart, whatever its parameter list.
Rationale
Equals and GetHashCode are two halves of one contract, and the compiler does not enforce
that they are written together. When only Equals is overridden, the type keeps the inherited
hash: identity-based for a class, derived from the field layout for a structure. Two instances
the new Equals calls equal then almost always produce different hash codes, and every
hash-based container consults the hash first - so a Dictionary cannot find the key that was
used to store the value, a HashSet accepts what is logically the same item twice, and
Distinct, GroupBy and Except return the wrong number of rows. Overriding only
GetHashCode fails just as quietly from the other side: equal objects now land in the same
bucket, but the comparison that resolves the bucket still uses reference equality, so the
lookup misses anyway and the custom hash buys nothing. Neither form raises an exception or
logs anything. The collection simply behaves as though the items were never equal, and the
symptom - a duplicated record, a cache that never hits, a total counted twice - surfaces far
from the type that caused it.
The following code illustrates the pattern detected by this rule:
Public Property Name As String
' FLAGGED: Equals and GetHashCode are not overridden together
Public Overrides Function Equals(obj As Object) As Boolean
Dim other = TryCast(obj, Customer)
If other Is Nothing Then Return False
Return Id = other.Id
End Function
Remediation
Override both, and derive the hash from exactly the members the equality comparison reads.
Prefer HashCode.Combine where it is available, and make sure the members involved are stable
for the lifetime of the object: a hash computed from a mutable property changes while the
object sits in a container, which loses the entry just as effectively. Implementing
IEquatable(Of T) alongside the overrides avoids boxing and gives callers a typed comparison.
' Before: equal customers hash differently, so dictionary lookups miss
Public Overrides Function Equals(obj As Object) As Boolean
Dim other = TryCast(obj, Customer)
Return other IsNot Nothing AndAlso Id = other.Id
End Function
' After: the same member drives both
Public Overrides Function Equals(obj As Object) As Boolean
Dim other = TryCast(obj, Customer)
Return other IsNot Nothing AndAlso Id = other.Id
End Function
Public Overrides Function GetHashCode() As Integer
Return Id.GetHashCode()
End Function