Question:
What is the difference between the == operator and the object.Equals method call in C#?
Answer:
In fact, there is a whole zoo of object comparison methods in C#.
-
Comparison via
==
. This comparison is resolved at compile time according to the declared types of the left and right sides of the comparison. The comparison operator can be overloaded. It is static, the actual type of the object does not matter when choosing which of the overloaded comparison operators to use. Example:object o1 = 5, o2 = 5; bool eq = (o1 == o2); // false
The non-overloaded
==
operator tests reference types for reference equality. Anull
value is allowed,null
is only equal to anothernull
. String has an overloaded operator==
, which compares not references, but the contents of strings. In the overloaded operator for your object, you can, of course, implement any logic.The
==
operator for predefined value types tests the equality of these values. For numbers, for example, this can lead to type casting: if one of the operands isint
and the second isdouble
, then the first operand will also be cast to typedouble
(that is,5 == 5.0
givestrue
). The==
operator is not defined for custom structures, you must write it yourself.The presence of the static operator
==
for the current version of the language is not expressed with a generic constraint, so this operator is often useless in generic methods for generic type arguments. -
Comparison via virtual method
Equals(object)
. This comparison is resolved by calling theEquals
virtual function and is based on the dynamic type of the left argument. Example:object o1 = 5, o2 = 5; bool eq = o1.Equals(o2); // true
For an unoverridden function, for reference types, reference equality is also checked. Unlike the previous case, calling
a.Equals(b)
null
ina
, as in any method call, results in aNullReferenceException
.ValueType
, the base class of all value types, overrides theEquals
method . Thus, if you don't override this method, for value types it will behave like this:- if all non-static fields (including private ones) of the value type are also value types recursively down (that is, none of the fields and subfields is a reference type), then the values are compared bit by bit .
- otherwise, reflection is applied and the values of all fields are compared pairwise via
Equals(object)
.*
Then, comparison via reflection is slow, so if your value type will be compared frequently, it makes sense to override the
Equals(object)
method.Also, comparison via
Equals(object)
does not result in type conversions, so5.Equals(5.0)
will returnfalse
.If you are overriding
Equals(object)
, you will most likely need to overrideGetHashCode()
as well (which the compiler will kindly remind you of). -
Comparison via
object.Equals(object o1, object o2)
. This is essentially a convenient wrapper overEquals(object)
: the method checks if the same object ornull
has come to it, and only if both of these checks fail, callsEquals(object)
on the first object. It makes sense to use this method instead ofEquals(object)
to avoid wrapping code withnull
checks. -
The next method is an implementation of the
IEquatable<T>
interface. It's a typed interface, so you don't have to check what type your operand is. LikeEquals(object)
, this is a virtual method. However, it does not make much sense in complex class hierarchies, so it will most likely be called with a known runtime type of both arguments – after all, the type of the right side is set by the generic parameter!This method is preferred over the
Equals(object)
overloads because you avoid the overhead of type checking and [boxing] (for value types). But if you have already implemented this interface, it makes sense to implementEquals(object)
in a compatible way:// для класса (по контракту, Equals(null) должно возвращать false) public override bool Equals(object o) => Equals(o as T);
// для структуры public override bool Equals(object o) => o is T t && Equals(t);
-
Then, if you need a special object comparison method for a specific rather than a general situation (or want to compare objects that don't belong to you in a special way), you can delegate the comparison to a special object that implements the
IEqualityComparer
(untyped) orIEqualityComparer<T>
interface ( typed). Comparison using such comparison objects is used, for example,Hashtable
andDictionary<K, V>
, as well as some LINQ methods .In addition to it, there is a helper class
EqualityComparer<T>
.EqualityComparer<T>
provides anIEqualityComparer<T>
, which checks whether typeT
implements theIEquatable<T>
interface, and otherwise performs the comparison viaEqual(object)
(which is always present). (It is also advised to implementIEqualityComparer
to inherit fromEqualityComparer<T>
.) -
Finally, equality/inequality can be derived from the more general greater/less/equal comparison. For this, the
<
/>
operators are used (which are similar to the==
operator, but defined for a narrower group of types), theIComparable
interfaces (an analogue of theEquals(object)
method),IComparable<T>
(an analogue of theIEquatable<T>
interface),IComparer
(an analogue ofIEqualityComparer
) andIComparer<T>
(similar toIEqualityComparer<T>
). As well as theComparison<T>
delegate (for example, for theList<T>.Sort(Comparison<T>)
overload).
When overriding any of the object comparison methods, remember that your method must be reflexive, symmetric, and transitive, and, if possible, not throw exceptions (for example, if the operands are of incompatible types, just return false
).
*From the comparison rule for ValueType
follows, for example, the following subtlety. Bitwise equality and value equality are slightly different things. For example, minus zero is not bitwise equal to plus zero. Therefore, this code:
struct BitComparable
{
double d;
public BitComparable(double d) { this.d = d; }
}
struct NonBitComparable
{
double d; object o;
public NonBitComparable(double d, object o) { this.d = d; this.o = o; }
}
var x1 = new BitComparable(1 / double.PositiveInfinity); // +0
var y1 = new BitComparable(1 / double.NegativeInfinity); // -0
Console.WriteLine(x1.Equals(y1));
var x2 = new NonBitComparable(1 / double.PositiveInfinity, null);
var y2 = new NonBitComparable(1 / double.NegativeInfinity, null);
Console.WriteLine(x2.Equals(y2));
outputs False
and True
respectively.