C# Equals vs ==: How They Differ and When to Use Each
c# equals vs ==: Understand the difference between the == operator and the Equals method in C#, including reference vs value types, operator overloading, and practical...
In C#, the choice between == and Equals often leads to subtle bugs because the two behave differently depending on whether you are dealing with a value type, a reference type, or a type that overrides these members. Understanding the distinction is essential for writing correct comparisons. This article explains how c# equals vs == differ under the hood, when each is appropriate, and how to avoid common pitfalls.
The Core Difference Between == and Equals
At first glance, == and Equals seem interchangeable, but they are not. The == operator is a static, non-virtual operator that is resolved at compile time based on the declared types of the operands. Equals is a virtual method defined on System.Object, so its behavior is determined at runtime by the actual type of the object. This fundamental difference drives everything else.
For a class that does not override either, == and Equals both compare reference identity: they return true only if two references point to the same object. For a struct, the default behavior differs: == is not even defined unless the struct explicitly overloads it, while Equals performs a field-by-field comparison (but with a performance penalty we will discuss later).
How == Behaves for Reference Types
When you write a == b where both variables are of a reference type, the compiler emits a call to the op_Equality method if the type defines one, or falls back to reference comparison. Because == is not virtual, the decision is made at compile time based on the static type of the operands.
public class Person { public string Name { get; set; } } var p1 = new Person { Name = "Alice" }; var p2 = new Person { Name = "Alice" }; Console.WriteLine(p1 == p2); // False, different references
Even though both objects have the same Name value, == returns false because Person does not overload the operator. If you want value-based comparison, you must either overload == or use Equals after overriding it.
How Equals Behaves by Default
The Equals method is virtual, so the runtime dispatches to the most derived override. The default implementation in Object performs reference equality for reference types. For value types, ValueType.Equals performs a field-by-field comparison using reflection, which is correct but slow.
public struct Point { public int X; public int Y; } var pt1 = new Point { X = 1, Y = 2 }; var pt2 = new Point { X = 1, Y = 2 }; Console.WriteLine(pt1.Equals(pt2)); // True Console.WriteLine(pt1 == pt2); // Compile error: operator '==' cannot be applied to operands of type 'Point' and 'Point'
Because Point is a struct, == is not available unless you define it. The default Equals works, but it uses reflection internally, which is expensive for hot paths.
Operator Overloading Changes the Game
When you overload ==, you provide a custom implementation that is called at compile time based on the static types. Overloading == does not automatically affect Equals; you must override Equals separately to keep the two consistent.
public class Money { public decimal Amount { get; } public string Currency { get; } public Money(decimal amount, string currency) { Amount = amount; Currency = currency; } public override bool Equals(object obj) { return obj is Money other && Amount == other.Amount && Currency == other.Currency; } public override int GetHashCode() { return HashCode.Combine(Amount, Currency); } public static bool operator ==(Money left, Money right) { if (ReferenceEquals(left, right)) return true; if (left is null || right is null) return false; return left.Equals(right); } public static bool operator !=(Money left, Money right) { return !(left == right); } }
Here, == delegates to Equals, ensuring consistent behavior. Note the null checks: ReferenceEquals handles the case where both are the same reference, and the null checks prevent a NullReferenceException when one operand is null. This pattern is common for immutable value-like classes.
The Special Case of Strings
Strings are a reference type, but both == and Equals are overridden to compare the character sequence, not the reference. This is why string1 == string2 works as expected for value equality.
string a = "hello"; string b = "hello"; Console.WriteLine(a == b); // True Console.WriteLine(a.Equals(b)); // True
However, Equals has overloads that accept a StringComparison enum, giving you control over case sensitivity and culture. The == operator does not offer that flexibility; it always uses ordinal comparison. For example:
string s1 = "Hello"; string s2 = "hello"; Console.WriteLine(s1 == s2); // False Console.WriteLine(s1.Equals(s2, StringComparison.OrdinalIgnoreCase)); // True
If you need culture-aware or case-insensitive comparison, use the Equals overload. If you simply need to check for exact character equality, == is fine and slightly more readable.
Performance and Runtime Costs
Performance is a key consideration when choosing between == and Equals. For value types, Equals is a virtual call, which means the runtime must look up the method table. More importantly, when you call Equals on a value type without overriding it, the default ValueType.Equals uses reflection to enumerate fields, which is significantly slower than a direct field comparison.
public struct Coordinate { public int X; public int Y; // Override Equals and GetHashCode for better performance public override bool Equals(object obj) { return obj is Coordinate other && X == other.X && Y == other.Y; } public override int GetHashCode() => HashCode.Combine(X, Y); }
By overriding Equals for a struct, you avoid the reflection overhead and get a direct, JIT-compiled comparison. For reference types, Equals is a virtual call, which has a small dispatch cost compared to a non-virtual == call. However, the difference is negligible in most applications unless you are comparing millions of objects in a tight loop.
Another subtle performance issue is boxing. When you call Equals on a value type through an interface or as object, the value is boxed, allocating an object on the heap. The == operator, when defined for a struct, avoids boxing because it is a static method that takes the struct by value.
int x = 42; object boxed = x; // boxing bool eq = boxed.Equals(x); // no additional boxing for x? Actually x is boxed again? No, x is passed as object? Wait, Equals(object) receives x boxed.
To avoid boxing, use the generic IEquatable<T> interface and override Equals(T) as well. This is a common performance optimization for value types in generic collections.
Practical Guidance: When to Use Which
The choice between == and Equals depends on the type and the context. Here are concrete rules:
- Use
==for primitive types (int,double,bool, etc.) andstringwhen you need ordinal comparison. The operator is fast and readable. - Use
Equalswhen you need polymorphic comparison or when you are calling a method that takes anobjectparameter. For example,List<T>.ContainsusesEqualityComparer<T>.Default, which callsEquals. - Override
EqualsandGetHashCodefor your own types when you need value equality. Always overrideGetHashCodeso that the type can be used in hash-based collections. - If you overload
==, also overrideEqualsand make them consistent. Inconsistent behavior leads to subtle bugs when code mixes both. - For structs, always override
Equalsand consider overloading==if you need the operator. The defaultEqualsis slow, and==is not available unless you define it. - Be careful with nullable types. For
Nullable<T>,==andEqualsbehave similarly, butEqualsreturnsfalsewhen both are null? ActuallyNullable<T>.Equalsreturnstrueif both have no value. The==operator also returnstruefor two null nullable values. So they are consistent. - For floating-point types, remember that
NaNis not equal to itself. Both==andEqualsreturnfalsefordouble.NaN == double.NaN. If you need to treatNaNas equal, you must implement custom logic.
A common mistake is to use == on two variables of type object that happen to hold strings. Because the static type is object, == performs reference comparison, not string value comparison. The correct approach is to cast or use Equals.
object obj1 = "test"; object obj2 = "test"; Console.WriteLine(obj1 == obj2); // False (reference comparison) Console.WriteLine(obj1.Equals(obj2)); // True (string override)
This is a classic pitfall that can be avoided by understanding the static type resolution of ==.
When designing your own types, the safest approach is to implement IEquatable<T> and override Equals and GetHashCode. Then, if you also want to support the == operator, implement it in terms of Equals. This ensures that all comparison paths lead to the same logic, making your type predictable and maintainable.