No Base Call Extending Object
ID |
csharp.no_base_call_extending_object |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
CSharp |
Tags |
code_smell, equals, identity, inheritance |
Description
Reports a call to base.Equals or base.GetHashCode inside an Equals or GetHashCode
override of a class that extends object directly — one that declares no base class, or names
only object or interfaces in its base list.
Not reported: a base call used as an if condition, which is a legitimate reference-equality
fast path; plain delegation from an Equals override, which csharp.base_equals_reference
covers; classes with a real base class, whose inherited implementation may be structural; and
records and structs, whose inherited implementation is value-based.
Rationale
object.Equals and object.GetHashCode are defined on identity, not on content. The first
returns true only for the very same instance; the second derives its value from that instance.
Feeding either into an override cancels the override, because two distinct instances holding
equal values can never satisfy it:
public class Point
{
public int X;
public int Y;
public override bool Equals(object obj)
{
return base.Equals(obj) && ((Point)obj).X == X; // FLAW - false for two equal Points
}
public override int GetHashCode()
{
return base.GetHashCode() ^ X; // FLAW - identity leaks into the hash
}
}
public class Size
{
public int Width;
public override bool Equals(object obj)
{
if (base.Equals(obj)) return true; // OK - reference-equality fast path
return obj is Size other && other.Width == Width;
}
public override int GetHashCode() => Width; // OK - derived from state
}
The hash case is the more damaging one. Two objects that compare equal are required to return
the same hash, and an identity component breaks that contract, so a dictionary or set stops
finding entries it holds — usually not at the line that caused it, and not for every run. The
Equals case fails more visibly but just as completely: the override looks like it compares
values and does not.
Remediation
Drop the base call and compare the fields that define the type’s identity, combining their hashes
with HashCode.Combine. Keep the two members consistent: every field used in Equals should
contribute to GetHashCode, and no field should contribute to the hash unless it is stable for
the lifetime of the object. If the type really is compared by reference, delete the overrides and
let the inherited behaviour stand; if it is a value, consider declaring it a record, which
generates both members from its state.