Implementing IEquatable<T> in C# for Efficient Equality Checks
c# iequatable: Learn how to implement IEquatable<T> in C# to improve equality comparison performance and correctness for custom types.
c# iequatable requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you use a custom type as a dictionary key or call Contains on a list, the runtime must decide whether two instances are equal. The default behavior uses object.Equals, which for value types causes boxing and for reference types uses reference equality unless overridden. IEquatable<T> gives you a typed, faster, and more correct way to define equality for your own types. This article explains how to implement it, why it matters, and where it fits alongside other equality mechanisms.
What IEquatable<T> Does and Why It Matters
IEquatable<T> is an interface that defines a single method: bool Equals(T other). When a type implements this interface, generic collection methods like List<T>.Contains, Dictionary<TKey, TValue>.ContainsKey, and HashSet<T>.Contains can call the strongly typed Equals(T) directly instead of falling back to object.Equals(object). This avoids boxing for value types and eliminates the need for a runtime type check for reference types.
The performance benefit is most visible with value types. Without IEquatable<T>, comparing two struct instances through object.Equals requires boxing each operand, allocating heap objects, and then performing a virtual dispatch. With IEquatable<T>, the comparison is direct and allocation-free. For reference types, the gain is smaller but still relevant: the caller does not need to cast other to object and back, and the compiler can often devirtualize the call.
Beyond performance, implementing IEquatable<T> forces you to define equality explicitly. This is especially important for domain objects where value equality—not reference identity—determines whether two instances represent the same thing.
Implementing IEquatable<T> on a Value Type
Consider a simple struct that represents a point in two-dimensional space. Without IEquatable<T>, two points with the same coordinates would not compare equal by default because ValueType.Equals performs a field-by-field comparison using reflection, which is slow. Implementing the interface gives you direct control.
public readonly struct Point : IEquatable<Point> { public int X { get; } public int Y { get; } public Point(int x, int y) { X = x; Y = y; } public bool Equals(Point other) { return X == other.X && Y == other.Y; } public override bool Equals(object? obj) { return obj is Point other && Equals(other); } public override int GetHashCode() { return HashCode.Combine(X, Y); } }
The Equals(Point other) method contains the actual comparison logic. The Equals(object?) override calls into it after a type check, and GetHashCode is overridden to keep the contract that equal objects have equal hash codes. Without GetHashCode, the type would still compile, but it would misbehave in hash-based collections.
When you implement IEquatable<T> on a value type, you should also override Equals(object) and GetHashCode. The runtime and the compiler expect these to be consistent. If you omit the object override, callers that use the non-generic interface will still get the reflection-based behavior, defeating the purpose.
Implementing IEquatable<T> on a Reference Type
Reference types default to reference equality, meaning two distinct instances are never equal unless they are the same object. For many domain entities, you want value equality based on an identifier or a set of properties. IEquatable<T> lets you define that without affecting the rest of the type's behavior.
public class Person : IEquatable<Person> { public string FirstName { get; } public string LastName { get; } public int Id { get; } public Person(int id, string firstName, string lastName) { Id = id; FirstName = firstName; LastName = lastName; } public bool Equals(Person? other) { if (other is null) return false; return Id == other.Id; } public override bool Equals(object? obj) { return Equals(obj as Person); } public override int GetHashCode() { return Id.GetHashCode(); } }
Here, two Person instances are equal if their Id values match, regardless of name fields. The Equals(Person?) method handles the null check. The object override uses as Person, which returns null for non-Person objects, and then delegates to the typed method.
For reference types, the performance benefit of IEquatable<T> is modest because there is no boxing. The main advantage is clarity and avoiding a double cast. However, you must still override GetHashCode; otherwise, two equal persons will produce different hash codes, breaking dictionary lookups.
Overriding Equals and GetHashCode Alongside IEquatable<T>
The .NET documentation is explicit: if you implement IEquatable<T>, you should also override Equals(object) and GetHashCode. The reason is that many APIs, including Array.IndexOf, ArrayList.Contains, and object.Equals calls, do not use the generic interface. If the two Equals implementations disagree, you get inconsistent behavior.
The Equals(object) override should delegate to the typed method whenever possible. This keeps the logic in one place and avoids duplication. For value types, use a pattern like obj is Point other && Equals(other). For reference types, use obj is Person other && Equals(other) or Equals(obj as Person) with a null check inside.
GetHashCode must be consistent with Equals. If two objects are equal, they must produce the same hash code. The reverse is not required—unequal objects may share a hash code—but a good hash function minimizes collisions. Use HashCode.Combine for multiple fields, or a single field if that field is the equality key.
A common mistake is to override Equals but forget GetHashCode. The compiler does not warn you, and the code may work in simple tests. But as soon as you place the objects in a HashSet or use them as dictionary keys, you will see incorrect lookups or duplicate entries.
Performance and Runtime Behavior
The primary reason to implement IEquatable<T> is to avoid boxing and virtual dispatch overhead for value types. Consider a List<Point> with thousands of points and a call to Contains. Without IEquatable<T>, each comparison boxes both the current item and the argument, allocates temporary objects, and then performs a virtual call to ValueType.Equals. With IEquatable<T>, the call is direct and the compiler can inline it, making the loop significantly faster.
For reference types, the difference is smaller but still measurable in hot paths. The generic method avoids the type check and cast that object.Equals requires. In tight loops, this can reduce CPU time, though the effect is usually minor compared to the cost of the actual comparison logic.
There is also a correctness dimension. The default ValueType.Equals uses reflection to compare all fields. If a field is a reference type, it compares references, not values. This often leads to surprising results. Implementing IEquatable<T> lets you define exactly which fields participate in equality, giving you predictable behavior.
One caveat: IEquatable<T> does not change the behavior of == or != operators. Those operators are statically bound and must be overloaded separately if you want them to use your equality logic. If you do overload them, keep them consistent with Equals and GetHashCode.
Common Pitfalls and How to Avoid Them
A frequent mistake is implementing IEquatable<T> but not overriding GetHashCode. This breaks hash-based collections. Another issue is making Equals and GetHashCode depend on mutable fields. If an object's equality key changes after it is inserted into a dictionary, the hash code changes, and the object can no longer be found. Use immutable properties or fields for equality keys.
For value types, forgetting to override Equals(object) means that non-generic callers still use the slow reflection path. The interface alone is not enough. Similarly, for reference types, forgetting to handle null in Equals(T) can cause NullReferenceException when comparing against a null argument.
A subtle problem arises when you implement IEquatable<T> on a class and also inherit from a base class. The derived class may need its own equality logic. If you do not override Equals and GetHashCode in the derived class, it inherits the base's implementation, which may consider two derived instances equal even when they differ in derived fields. Decide whether equality should include derived fields and implement accordingly.
Another pitfall is implementing IEquatable<T> but leaving the object override inconsistent. For example, if Equals(object) uses reference equality while Equals(T) uses value equality, collections that call the non-generic version will behave differently. Always delegate from the object override to the typed method.
When to Use IEquatable<T> vs Other Equality Options
IEquatable<T> is not the only way to control equality. You can also use IEqualityComparer<T> to provide a separate comparer object, or override Equals and GetHashCode without implementing the interface. The choice depends on the scenario.
Use IEquatable<T> when the type itself should define its natural equality. This is the right approach for domain entities, value types, and any type where value equality is intrinsic. It is also the expected pattern for types used as dictionary keys or in sets.
Use IEqualityComparer<T> when you need multiple equality definitions for the same type. For example, a Person might have one comparer based on Id and another based on Email. The comparer can be passed to dictionary or set constructors, leaving the type's own Equals untouched.
If you only need to override equality for a single use case and do not want to modify the class, an IEqualityComparer<T> is a cleaner choice. But if the equality is a property of the type itself, IEquatable<T> is more idiomatic and integrates with LINQ methods like Distinct and Contains without extra arguments.
The following table summarizes the key differences:
| Aspect | IEquatable<T> | IEqualityComparer<T> | Overriding Equals only |
|---|---|---|---|
| Where defined | On the type itself | Separate class or struct | On the type itself |
| Multiple definitions | Not possible | Possible, one per comparer | Not possible |
| Boxing for value types | Avoided | Avoided if comparer is generic | Avoided if using generic collection |
| Integration with LINQ | Automatic | Requires passing comparer | Automatic if using generic methods |
| Typical use case | Natural equality of the type | Context-specific equality | Simple cases where interface is overkill |
In practice, start with IEquatable<T> when you control the type and equality is unambiguous. If you later need a different comparison for a specific collection, add an IEqualityComparer<T> without changing the type's behavior.
One final consideration: IEquatable<T> does not affect serialization, database mapping, or any other framework that uses reflection. It only influences equality checks in code. Keep that in mind when designing types that are persisted or transmitted; the equality contract is separate from the data contract.