Equality in C#

Table of Contents

Introduction
Operator ==()
Equals()
Equality of Built-in Value Types
Equality of User-Defined Value Types
Equality of Reference Types
Equality and Boxing
Equality of Strings
Conclusion

Introduction

In languages that deal with obects and object references, a question of equality may become quite confusing. For starters, C# provides not one, but two ways to compare things: operator==() and method Object.Equals(). Since everything in C# derives from Object, method Equals() can be universally applied to any value.

operator==() and Equals() can mean different things in different contexts. Furthermore, result of x==y may be different from x.Equals(y)!

First of all, one must realize, that operator==() and Equals() are two different methods, and they are not formally related. It is not guaranteed that expressions x==y and x.Equals(y) will yield the same result, even if both x and y belong to system-defined types such as int and double.

Operator ==()

In C# operator==() must be static member of a class (or struct), have return value of type bool, and two parameters, at least one of which has the same type as the enclosing class.

When expression x==y occurs in a program, the compiler looks for a matching operator==() inside the classes to which x and y belong, and their base classes. If there is no matching operator==(), or there is more than one matching operator==(), the program will not compile. Version of operator==() that will be called is always known at compile time.

Example:

class Base
{
    public static bool operator==(Base x, Base y)
    {
       ...
    }
}

class Derived : Base
{
    public static bool operator==(Derived x, Derived y)
    {
       ...
    }
}


Base a = new Derived(); Base b = new Derived(); bool result = (a==b);

Here, Base::operator==() will be called when calculating a==b, despite the fact that both a and b actually reference instances of type Derived.

Equals()

Equals() is defined as a virtual method in class Object. Since everything in C# derives from Object, this method can be called on value of any type. This is in contrast with operator==() that may or may not be defined for particular type.

Since call to Equals() is virtual, exact version of the method that will be called by x.Equals(y) is determined by dynamic type of x, that usually is not known at compile time. Note also, that unlike a==b, expression x.Equals(y) is inherently asymmetrical. Only x dictates what version of Equals() will be called. y has absolutely no say in the matter.

Example:

class Base
{
    public override bool Equals(object obj)
    {
       ...
    }
}

class Derived : Base
{
    public override bool Equals(object obj)
    {
       ...
    }
}


Base a = new Derived(); Base b = new Derived(); bool result = a.Equals(b);

Here a.Equals(b) will call Derived.Equals(), because variable a references isntance of class Derived.

Equality of Built-in Value Types

operator==() compares built-in value types by value, performing type conversion if necessary. In particular, all of the following is true:
1==1
1==1.0
1==1L
'1'==49
The latter is true, because 49 is ASCII code for character '1'.

Equals() always returns false if operands are of different types. If operands are of the same type, they are compared by value:

ExpressionValueCommenct
1.Equals(1)truecomparing int to int
1.Equals(1.0)falsecomparing int to double
1.Equals(1L)falsecomparing int to long
'1'.Equals(49)falsecomparing char to int

Note: string is not a value type. Things like 1=="1" are not allowed and will lead to compilation error. 1.Equals("1") is, of course, legal, and it returns false.

Equality of User-defined Value Types (Structs)

By default operator==() is not implemented for structs. If you want to compare structs using ==, you must define an operator==() in your struct. Note, that you may define versions of the operator, that compare your struct to other types (built-in types, other structs, or even object types).

Default implementation of Equals() comes from class ValueType, which is implicit base class of all value types. You may override this implementation by defining your own Equals() method in your struct. ValueType.Equals() always returns false when one compares objects of different (dynamic) types. If objects are of the same type, it compares them by calling Equals() for each field. If any of these returns false, the whole process is stopped, and final result is false. If all field-by-field comparisons return true, final result is true.

Equality of Reference Types

By default, operator==() on object types means reference equality. That is, a==b is true if and only if a and b reference the same object. Static method Object.ReferenceEquals() has the same effect.

You may override default implementation and provide your own logic for operator==(). However, keep in mind that operator==() is tied to the static types of the operands. If your objects are passed to code that does not know their static type, this code will not use your version of operator==().

Example:

class Orange
{
    int Weight;
    
    public Orange( int weight )
    {
        Weight = weight;
    }

    public static bool operator ==(Orange x, Orange y)
    {
       return (x.Weight == y.Weight);
    }

    public static bool operator !=(Orange x, Orange y)
    {
       return (x.Weight != y.Weight);
    }

}


Orange a = new Orange(10); Orange b = new Orange(10); bool result = (a==b); // true, Orange.operator==() used object a_obj = a; object b_obj = b; result = (a_obj == b_obj); // false, reference equality used

Object.Equals() also uses reference equality. However, Equals() is a virtual method, and can be overridden. Object also defines static version of Equals() that takes two parameters. Object.Equals(a,b) is essentially the same as a.Equals(b), but it will not blow up if a is null.

Equality and Boxing

Boxing is a conversion of value type to reference type. Let's consider this example:

class Test
{
    private static bool MyEquals( object a, object b )
    {
        return a==b; // Reference equality
    }

    public static void DoTest()
    {
        bool result = MyEquals(1,1); 
    }
}

In this example, value of result will be false. Let's analyze why it is so.

A literal of type int (which is actually an alias for Int32) must be passed to a method that takes parameter of type object. Internally variables of type Int32 are represented as 32-bit values. They are not object references. At the same time, Test.MyEquals() expects an object reference to be passed to it. C# solves this problem by boxing the integer value. It creates a temporary object of unnamed type and passes this object to MyEquals(). When asked about its type, the object reports Int32. When converted back to Int32, the object yields the original value (in this case 1).

A crucial point is that each boxing action creates new temporary object. Although both parameters of MyEquals are 1, separate temporary object will be created for each parameter. operator==() inside MyEquals() works on variables of static type object, so it is in fact Object.operator==(). As we mentioned earlier, Object.operator==() implements reference equality only. Since a and b don't reference the same object, result will be false.

Equality of Strings

In C# strings are reference types. However, operator==() is overloaded for strings and compares string contents, not reference equality. String.Equals() also compares string contents. What makes strings unique is that string literals are pooled. For instance, if we have the following code:

string s1 = "abc";
string s2 = "abc";
bool result = Object.ReferenceEquals(s1,s2);

the value of result will be true. ReferenceEquals() is not lying here. s1 and s2 indeed reference the same object. The system recognized that we have two equivalent string literals, and merged them into one. It can get away with it, because string objects are immutable. This check for duplicate string literals occurs when you code is JIT-compiled, i.e. converted from IL (intermediate language) to native (x86) machine codes. Thus, string literals are merged even when they are from different assemblies.

Note, however, that merging process affects only string literals, not calculated strings. E.g., in the following code:

string s1 = "abc";
string s2t = "ab";
string s2 = s2t + "c";
bool result = Object.ReferenceEquals(s1,s2);
bool result2 = (s1==s2);

value of result will be false. s1 and s2 are not merged, since s2 is not a literal. Nevertheless, the value of result2 is true, because for strings operator==() compares string contents, not object references.

Conclusion

A matter of equality in C# is not a simple one. Results of equality comparisons are not always intuitive. Understanding the theory behind equality operations helps to prevent surprises and hard-to-find bugs. Hopefully, this article removed a shroud of mystery from C# equality and made it your friend.