C# gives every type two kinds of equality and several places to state which one it means. These are the defaults, the members that have to move together, and the comparisons that go wrong quietly: hashed keys, ordering mistaken for equality, and floating point.
Equality means what the type decides it meansCA1046
A class compares references: two variables are equal only when they hold the same object, so two instances built from the same data are not equal. A struct or a record compares values. Overloading == on a class commits it to keeping !=, Equals, and GetHashCode in step from then on, which is why CA1046 asks a reference type to keep the operator it inherited and a data type to become a record.
Preferred
Order a = new(42);
Order b = a;
bool same = a == b;Avoid
public static bool operator ==(Order left, Order right) => left.Id == right.Id;
public static bool operator !=(Order left, Order right) => left.Id != right.Id;
public override bool Equals(object? obj) => obj is Order other && Id == other.Id;
public override int GetHashCode() => Id;Corpus: C# Equality comparisons · Objects - create instances of types · CA1046: Do not overload operator equals on reference types
Let a record generate the equality members
The record modifier makes the compiler emit Equals, GetHashCode, and == with != over the declared data, and the generated code compares the runtime type as well, so a base record and a derived record with equal values stay unequal. A record class is still a reference type and a record struct gets operator equality that a plain struct never had.
Preferred
public record Order(int Id, string Sku);
bool same = new Order(42, "A-1") == new Order(42, "A-1");Avoid
public class Order
{
public int Id { get; init; }
public string Sku { get; init; } = "";
}Corpus: C# record types · Records
A record compares each member with the equality that member has
Synthesized record equality calls Equals on every member, and a List<T> or an array compares by reference. Two records holding equal contents in different instances are not equal, so compare the contents where the contents are what matters, or give the record the equality it needs.
Preferred
public record Order(int Id, string[] Lines);
bool sameLines = first.Lines.SequenceEqual(second.Lines);Avoid
public record Order(int Id, string[] Lines);
bool same = first == second;Corpus: C# Equality comparisons
Override `GetHashCode` in the same change as `Equals`CA2218
A dictionary or a set looks at the hash code before it looks at anything else, so two equal objects must return the same code. Build the hash from the members Equals reads, and let HashCode.Combine mix them instead of an arithmetic sequence assembled by hand.
Preferred
public override bool Equals(object? obj) =>
obj is Order other && Id == other.Id;
public override int GetHashCode() => HashCode.Combine(Id, Sku);Avoid
public override bool Equals(object? obj) =>
obj is Order other && Id == other.Id;Corpus: CA2218: Override GetHashCode on overriding Equals · IDE0070: Use 'System.HashCode.Combine' · C# Equality comparisons
Implement `IEquatable<T>` and route `Equals(object)` through it
The typed Equals(T) overload is what a caller holding the concrete type reaches, and it compares without boxing the argument. Equals(object) stays because the framework calls it, and it delegates rather than repeating the comparison. CA1066 and CA1067 check the two directions of that pairing.
Preferred
public sealed class Order : IEquatable<Order>
{
public bool Equals(Order? other) => other is not null && Id == other.Id;
public override bool Equals(object? obj) => Equals(obj as Order);
public override int GetHashCode() => Id;
}Avoid
public struct Money : IEquatable<Money>
{
public bool Equals(Money other) => Amount == other.Amount;
}Corpus: CA1066: Implement IEquatable when overriding Equals · CA1067: Override Equals when implementing IEquatable · Comparisons and Sorts Within Collections
Override `Equals` and `==` on a value typeCA1815
The inherited ValueType.Equals compares fields through reflection, which costs more than a field-by-field check and reads members that are not part of the value. A struct that will be compared, sorted, or used as a key implements IEquatable<T>, overrides Equals(object) and GetHashCode, and declares == together with !=.
Preferred
public bool Equals(Money other) => _amount == other._amount;
public override bool Equals(object? obj) => obj is Money m && Equals(m);
public override int GetHashCode() => _amount.GetHashCode();
public static bool operator ==(Money l, Money r) => l.Equals(r);
public static bool operator !=(Money l, Money r) => !l.Equals(r);Avoid
public struct Money
{
public decimal Amount;
}Corpus: CA1815: Override equals and operator equals on value types · CA2231: Overload operator equals on overriding ValueType.Equals
Keep `==`, `!=`, and `Equals` consistentCA2226
The compiler rejects a lone == without its !=, which is what CA2226 restates, so the remaining hazard is an operator that answers differently from Equals, the case CA2224 flags for the types that get the warning. Equality also carries a contract: reflexive, symmetric, transitive, consistent, and false rather than an exception for a null or unrelated argument.
Preferred
public static bool operator ==(Money l, Money r) => l.Equals(r);
public static bool operator !=(Money l, Money r) => !l.Equals(r);
public override bool Equals(object? obj) => obj is Money m && Equals(m);
public override int GetHashCode() => _amount.GetHashCode();Avoid
public static bool operator ==(Order l, Order r) => l.Id == r.Id;
public static bool operator !=(Order l, Order r) => l.Id != r.Id;
public override bool Equals(object? obj) => ReferenceEquals(this, obj);
public override int GetHashCode() => Id;Corpus: CA2226: Operators should have symmetrical overloads · CA2224: Override Equals() on overloading operator equals · Equality operators - test if two objects are equal or not equal
Use `ReferenceEquals` when identity is the question
ReferenceEquals always tests identity, whatever a type does with Equals or ==, so it is the dependable check once an operator has been overloaded and the natural short circuit at the top of an Equals override. It returns false for value types, because each argument is boxed separately.
Preferred
if (ReferenceEquals(this, other)) return true;
return other is Order o && Equals(o);Avoid
public static bool operator ==(Order l, Order r) => l.Id == r.Id;
public static bool operator !=(Order l, Order r) => l.Id != r.Id;
bool identity = first == second;Corpus: C# Equality comparisons · Equality operators - test if two objects are equal or not equal
Keep the hash code of a key stable while it is stored
A dictionary picks a bucket from the hash code a key has at insertion and recomputes that hash on lookup, and a hash function returns the same code for the same key every time. A key that changes while it sits in the collection is looked for in a bucket it no longer occupies. Value keys are safer as immutable records or structs.
Preferred
public record OrderKey(int Id, string Sku);
var byKey = new Dictionary<OrderKey, Order>();
byKey[new OrderKey(42, "A-1")] = order;Avoid
var key = new MutableKey { Id = 42 };
lookup[key] = order;
key.Id = 43;Corpus: Hashtable and Dictionary Collection Types · CA2218: Override GetHashCode on overriding Equals
Write `Equals` and `GetHashCode` as a pair on a comparer too
An IEqualityComparer<T> carries its own two methods and they owe each other the same agreement: hashing the object rather than the members Equals compares loses entries. A dictionary or a set takes the comparer as a constructor argument, and comparers are contravariant, so one written for a base type also serves its derived types.
Preferred
public bool Equals(Person? x, Person? y) =>
ReferenceEquals(x, y) || (x is not null && y is not null && x.Name == y.Name);
public int GetHashCode(Person p) => HashCode.Combine(p.Name);Avoid
public bool Equals(Person? x, Person? y) => x?.Name == y?.Name;
public int GetHashCode(Person p) => p.GetHashCode();Corpus: Using Variance in Interfaces for Generic Collections · Comparisons and Sorts Within Collections
Give a type one sort order, and keep `Equals` in stepCA1036
Sorting and BinarySearch ask for an ordering comparer, which a collection takes from IComparable<T>.CompareTo or from an IComparer<T> handed to the call. A sort order is a claim about position rather than equality, so CA1036 asks a comparable type to keep Equals in step and to supply ==, !=, <, and >, and a zero from CompareTo stays a statement about position.
Preferred
public int CompareTo(Order? other) => other is null ? 1 : Id.CompareTo(other.Id);
public override bool Equals(object? obj) => obj is Order o && Id == o.Id;
public override int GetHashCode() => Id;Avoid
if (left.CompareTo(right) == 0)
Merge(left, right);Corpus: CA1036: Override methods on comparable types · Comparisons and Sorts Within Collections · Comparing Strings in .NET
`NaN` equals nothing, so test for it with `IsNaN`CA2242
Every comparison against NaN is false, including NaN == NaN, so a value that never became a number cannot be found through equality. Floating-point == also compares the exact binary value, which is why results reached by different arithmetic paths are compared with a tolerance.
Preferred
bool isNaN = double.IsNaN(value);
bool close = Math.Abs(a - b) < 1e-9;Avoid
bool isNaN = value == double.NaN;
bool same = total == 0.3;Corpus: CA2242: Test for NaN correctly · Floating-point numeric types · Comparison operators - order items using the greater than and less than operators
Tuple equality is element-wise and ignores field names
== on two tuples compares the elements in order, and field names are a compile-time convenience the comparison never sees. The types have to line up position by position, so a tuple with elements of a different shape than the other does not compile at all. That structural equality is what makes a tuple usable as a composite key.
Preferred
var key = (region: "eu", id: 42);
var seen = new HashSet<(string Region, int Id)>();
seen.Add(key);Avoid
var left = (id: 42, region: 1);
var right = (region: 42, id: 1);
bool same = left == right;Corpus: Tuple types · Tuples and deconstruction