What is the difference between the == operator and the object.Equals method call in C#?

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#.

  1. 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. A null value is allowed, null is only equal to another null . 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 is int and the second is double , then the first operand will also be cast to type double (that is, 5 == 5.0 gives true ). 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.

  2. Comparison via virtual method Equals(object) . This comparison is resolved by calling the Equals 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 in a , as in any method call, results in a NullReferenceException .

    ValueType , the base class of all value types, overrides the Equals 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, so 5.Equals(5.0) will return false .

    If you are overriding Equals(object) , you will most likely need to override GetHashCode() as well (which the compiler will kindly remind you of).

  3. Comparison via object.Equals(object o1, object o2) . This is essentially a convenient wrapper over Equals(object) : the method checks if the same object or null has come to it, and only if both of these checks fail, calls Equals(object) on the first object. It makes sense to use this method instead of Equals(object) to avoid wrapping code with null checks.

  4. 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. Like Equals(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 implement Equals(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);
  5. 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) or IEqualityComparer<T> interface ( typed). Comparison using such comparison objects is used, for example, Hashtable and Dictionary<K, V> , as well as some LINQ methods .

    In addition to it, there is a helper class EqualityComparer<T> . EqualityComparer<T> provides an IEqualityComparer<T> , which checks whether type T implements the IEquatable<T> interface, and otherwise performs the comparison via Equal(object) (which is always present). (It is also advised to implement IEqualityComparer to inherit from EqualityComparer<T> .)

  6. 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), the IComparable interfaces (an analogue of the Equals(object) method), IComparable<T> (an analogue of the IEquatable<T> interface), IComparer (an analogue of IEqualityComparer ) and IComparer<T> (similar to IEqualityComparer<T> ). As well as the Comparison<T> delegate (for example, for the List<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.

Scroll to Top