C# Equality — Quick Revision Sheet
== vs Equals in C#
Equals(object)
- From
System.Object. - Virtual → can override.
- Default: reference equality.
- Overridden by many types for value equality, e.g.:
stringDateTimedecimal- All primitive value types (
int,double,bool, etc.) - Collections (
List<T>,Dictionary<K,V>, etc. — they check contained values in some operations)
==
- Operator.
- Default:
- Reference types → reference equality
- Value types → value equality
- Overloaded for value equality in:
stringDateTimedecimal- Most primitives (
int,double,bool, etc.)
Best practice for custom classes
- Override
Equals - Override
GetHashCode - Overload
==/!=for consistency
⚡ Rule of thumb:
If not overridden → both check reference equality.
If overridden (e.g., string, DateTime) → compare contents/values.
What are we comparing?
- Reference equality: are the two variables pointing to the same object instance?
- Value equality: do the two variables represent the same value (same contents)?
By default, classes compare by reference; structs compare by value. Records are designed for value equality.
Operators & Methods
== vs Equals
Equals(object): virtual onSystem.Object. Default is reference equality; many types override to compare values.==: operator. Default is reference for reference types, value for value types. Can be overloaded.
// String overrides both:
object o = "Hello";
object o1 = o;
Console.WriteLine(o == o1); // True (same ref)
Console.WriteLine(o.Equals(o1)); // True (string value equality)
Defaults
Classes, Structs, Records
- class Default: reference equality.
- struct Default: value equality (field-by-field).
- record Default: value equality (compiler generates
Equals,GetHashCode,==,!=).
public record Person(string Name, int Age);
var a = new Person("Alice", 30);
var b = new Person("Alice", 30);
Console.WriteLine(a == b); // True
Console.WriteLine(a.Equals(b)); // True
Common overrides
Types that compare by value
- All primitives:
int,double,bool, etc. string(ordinal by default, culture options via APIs).DateTime,DateOnly,TimeOnly,TimeSpan.decimal,Guid.- Most enums (underlying numeric equality).
Collections like List<T> don’t override ==; they use Equals from object (reference). Many collection operations use IEqualityComparer<T>/IEquatable<T> of the elements.
Template
Recommended pattern for custom classes
public sealed class Person : IEquatable<Person>
{
public string Name { get; }
public int Age { get; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
// Value equality on Name + Age
public bool Equals(Person? other)
=> other is not null && Name == other.Name && Age == other.Age;
public override bool Equals(object? obj) => obj is Person p && Equals(p);
public override int GetHashCode() => HashCode.Combine(Name, Age);
public static bool operator ==(Person? left, Person? right)
=> Equals(left, right);
public static bool operator !=(Person? left, Person? right)
=> !Equals(left, right);
}
Always keep Equals, GetHashCode, and ==/!= consistent. If instances can be used as dictionary keys or in a HashSet, GetHashCode must align with Equals.
Gotchas
Things that trip people up
- Override only one (e.g., just
Equals) → inconsistent behavior with==. - Null handling: prefer implementing
IEquatable<T>and static operators that tolerate nulls. - Floating point: don’t compare with
==if precision matters — use tolerances. - Strings: default equality is ordinal, case-sensitive. For culture-insensitive comparisons use
StringComparer.OrdinalIgnoreCase.
Quick rules
Memorise these
- Classes: default ref equality. Override for value semantics.
- Structs: value equality out of the box.
- Records: value equality generated by the compiler.
Equalsis the framework’s contract;==is sugar — make them agree.

















