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

Configuration

This detector does not need any configuration.