Possible Unintended Reference Compare
ID |
csharp.possible_unintended_ref_compare |
Severity |
low |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
CSharp |
Tags |
equality, reliability |
Description
Reports == or != comparisons between two operands whose types resolve to a class
declared in the same compilation unit that does not overload operator ==. In such a
case the operator falls back to reference identity, which is rarely the intent.
Rationale
C# uses reference identity for the default == between objects. When the developer
genuinely wants structural equality (compare field values), the type needs to overload
operator == (and Equals/GetHashCode). Without that overload, comparing two
instances with == returns true only when they are the very same allocation, which
makes the comparison silently wrong for value-like types.
public class Money
{
public decimal Amount { get; set; }
}
public class Wallet
{
public bool Same(Money a, Money b)
{
return a == b; // FLAW - reference equality
}
}
public class MoneyOk
{
public decimal Amount { get; set; }
public static bool operator ==(MoneyOk a, MoneyOk b) =>
a is null ? b is null : a.Amount == b.Amount;
public static bool operator !=(MoneyOk a, MoneyOk b) => !(a == b);
public override bool Equals(object obj) => obj is MoneyOk m && m.Amount == Amount;
public override int GetHashCode() => Amount.GetHashCode();
}
public class WalletOk
{
public bool Same(MoneyOk a, MoneyOk b)
{
return a == b; // OK - operator overload exists
}
public bool NullCheck(Money a) => a == null; // OK - null compare allowed
}
Remediation
Either overload operator == (and !=, Equals, GetHashCode) on the class so the
comparison expresses structural equality, or call Equals(a, b) / object.Equals(a, b)
explicitly. For records, the compiler does both automatically.