C# == vs Equals: How They Differ and When to Use Each
c# == vs equals: Learn how C# == and Equals differ for reference types, value types, and strings, and when to override or overload them.
c# == vs equals requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
In C#, the comparison operators == and the Equals method are often used interchangeably, but they do not always produce the same result. The difference comes down to how each behaves by default for reference types and value types, and how they can be customized. Understanding this distinction is essential when writing equality checks that behave correctly across different types.
What the == Operator Does by Default
For reference types, the == operator performs reference equality by default. That means it compares whether two variables point to the same object in memory, not whether the objects have the same content.
class Person { public string Name { get; set; } } Person a = new Person { Name = "Alice" }; Person b = new Person { Name = "Alice" }; Person c = a; Console.WriteLine(a == b); // False Console.WriteLine(a == c); // True
Here, a and b are separate instances, so == returns False. a and c reference the same object, so the comparison returns True. This behavior is the default for all classes unless the operator is overloaded.
For value types, the default behavior of == is different. Most built-in value types such as int, double, and bool have the operator defined to compare the underlying values. However, if you define your own struct, == is not available unless you explicitly overload it.
struct Point { public int X; public int Y; } Point p1 = new Point { X = 1, Y = 2 }; Point p2 = new Point { X = 1, Y = 2 }; // Compile error: Operator '==' cannot be applied to operands of type 'Point' and 'Point' // Console.WriteLine(p1 == p2);
To use == with a custom struct, you must define the operator yourself. This is a key distinction: == is not automatically available for every type.
What Equals() Does by Default
The Equals method is defined on System.Object and is virtual, so any type can override it. The default implementation in object also performs reference equality for reference types, matching the default behavior of ==. For value types, however, Equals is overridden by System.ValueType to compare the fields of the struct.
Person a = new Person { Name = "Alice" }; Person b = new Person { Name = "Alice" }; Console.WriteLine(a.Equals(b)); // False (reference equality)
Point p1 = new Point { X = 1, Y = 2 }; Point p2 = new Point { X = 1, Y = 2 }; Console.WriteLine(p1.Equals(p2)); // True (field-by-field comparison)
For value types, Equals performs a value comparison by default, but it uses reflection to inspect the fields, which is slower than a manually implemented comparison. For this reason, overriding Equals in your own structs is recommended when you need frequent equality checks.
Why Strings Behave Differently
Strings are reference types, but they have a special behavior. The == operator and the Equals method both compare the string content, not the reference. This is because the string class overloads == and overrides Equals to perform ordinal comparison.
string s1 = "hello"; string s2 = string.Copy(s1); // Creates a new string instance Console.WriteLine(s1 == s2); // True Console.WriteLine(s1.Equals(s2)); // True
Even though s1 and s2 are different objects, both comparisons return True because the content is identical. This can lead to confusion when you expect reference equality, but for strings, the content-based comparison is almost always what you want.
One subtle difference is that == for strings can be overloaded further by the compiler in some contexts, but the practical result is the same. The Equals method also offers overloads that accept StringComparison enum values, allowing culture-sensitive or case-insensitive comparisons, while == does not.
string a = "Straße"; string b = "STRASSE"; Console.WriteLine(a.Equals(b, StringComparison.OrdinalIgnoreCase)); // False Console.WriteLine(a.Equals(b, StringComparison.CurrentCultureIgnoreCase)); // True (in German culture)
If you need culture-aware comparison, you must use Equals with the appropriate StringComparison argument.
Overloading == and Overriding Equals Together
When you define a custom type and want it to have value equality, you should override Equals and overload == so that they behave consistently. The C# compiler and many libraries expect these two to agree. If they diverge, you can introduce subtle bugs.
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) { if (obj is Money other) { return Amount == other.Amount && Currency == other.Currency; } return false; } public override int GetHashCode() { return HashCode.Combine(Amount, Currency); } public static bool operator ==(Money left, Money right) { if (left is null) return right is null; return left.Equals(right); } public static bool operator !=(Money left, Money right) { return !(left == right); } }
In this example, == delegates to Equals, ensuring consistent behavior. The != operator is also overloaded to complement ==. Overriding GetHashCode is necessary because types that override Equals must also provide a consistent hash code for use in collections like Dictionary or HashSet.
Note that the == operator handles null correctly. If left is null, it returns right is null. This avoids calling Equals on a null reference, which would throw a NullReferenceException.
ReferenceEquals for Reference Identity
Sometimes you genuinely need to know whether two variables point to the same object, regardless of any overridden Equals or overloaded ==. The ReferenceEquals method provides this guarantee.
Person a = new Person { Name = "Alice" }; Person b = a; Person c = new Person { Name = "Alice" }; Console.WriteLine(object.ReferenceEquals(a, b)); // True Console.WriteLine(object.ReferenceEquals(a, c)); // False
ReferenceEquals always compares object identity. It works on reference types, and for value types it always returns False because boxing creates a new object. If you need to compare value types by reference, you must box them first, but that is rarely useful.
Use ReferenceEquals when you are implementing custom equality and want to short-circuit the comparison if both references are the same. This is a common optimization in Equals overrides.
public override bool Equals(object obj) { if (ReferenceEquals(this, obj)) return true; if (obj is null) return false; // ... }
This pattern avoids expensive field comparisons when the same object is passed to Equals.
Performance and Allocation Considerations
The default Equals implementation for value types uses reflection to compare fields, which is significantly slower than a manually implemented comparison. If you have a struct that is compared frequently, overriding Equals and GetHashCode can reduce overhead. Similarly, overloading == for a struct avoids the boxing that occurs when calling Equals through the object interface.
struct Temperature { public double Celsius { get; } public Temperature(double celsius) => Celsius = celsius; public override bool Equals(object obj) { return obj is Temperature other && Celsius.Equals(other.Celsius); } public override int GetHashCode() => Celsius.GetHashCode(); public static bool operator ==(Temperature left, Temperature right) => left.Celsius == right.Celsius; public static bool operator !=(Temperature left, Temperature right) => !(left == right); }
With this implementation, == performs a direct double comparison without boxing. The Equals method still boxes the argument when called through the object overload, but you can also provide a strongly typed Equals(Temperature) method to avoid that cost.
For reference types, the performance difference between == and Equals is usually negligible. The main concern is correctness: ensure that both are consistent and that you do not accidentally use reference equality when value equality is intended.
Choosing Between == and Equals in Practice
The decision depends on the type you are comparing and the semantics you need.
For primitive value types like int, bool, and double, == is the natural choice. It is concise, performs value comparison, and does not box. For string, both == and Equals work, but use Equals with a StringComparison when you need culture-aware or case-insensitive comparisons.
For custom reference types, the default == and Equals both do reference equality. If you want value equality, override Equals and overload == together. If you only override Equals but not ==, code that uses == will still perform reference comparison, which can cause inconsistent behavior.
For custom structs, you must overload == if you want to use it. The default Equals performs field-by-field comparison but is slow. Override both for performance and consistency.
When implementing generic algorithms, prefer EqualityComparer<T>.Default over direct == or Equals calls. This static class handles nulls, uses the type's IEquatable<T> implementation if available, and falls back to object.Equals otherwise. It avoids the pitfalls of calling == on a generic type, which is not always defined.
public static bool AreEqual<T>(T a, T b) { return EqualityComparer<T>.Default.Equals(a, b); }
This approach works for any type that implements IEquatable<T> or overrides Equals, and it is the recommended way to compare values in generic code.
Finally, be aware of the null handling difference. The == operator can be overloaded to handle nulls gracefully, but Equals throws a NullReferenceException if called on a null instance. Always check for null before calling Equals, or use the EqualityComparer<T>.Default which handles nulls internally.
By understanding these behaviors, you can write equality checks that are correct, efficient, and consistent across your codebase.