Equality might look like a simple concept at a first glance, but looking deeper it isn’t. In C# objects can be compared with the ==
operator, with the Equals(Object)
member, with the Object.Equals(Object, Object)
method or using custom comparators that implement one of or more of the IEquatable<T>
, IComparable
, IStructuralEquatable
or IStructuralComparable
interfaces. There’s also a Object.ReferenceEquals(Object, Object)
method that can be used. In this post, we’ll take a closer look at the basics: ==
and .Equals()
.
Plain Vanilla Operator ==
The most common way to compare objects in C# is to use the ==
operator.
For predefined value types, the equality operator (==) returns true if the values of its operands are equal, false otherwise. For reference types other than string, == returns true if its two operands refer to the same object. For the string type, == compares the values of the strings.
Looking first at simple value types, this makes sense and makes comparisons of e.g. integers behave logical. Looking at more complex value types such as DateTime
it also makes sense. If we put the current date in two variables we expect them to be equal.
var d1 = DateTime.Now.Date; var d2 = DateTime.Now.Date; Console.WriteLine(d1 == d2); // Writes True |
Reference types are handled differently; ==
by default compares if the two variable are references to the same object. The contents of the object doesn’t matter.
var sb1 = new StringBuilder("Blue"); var sb2 = new StringBuilder("Blue"); Console.WriteLine(sb1 == sb2); |
The string
type is an exception pointed out in the documentation. It is a reference type stored on the heap, but everything possible has been done to make it behave like a value type. It is immutable. ==
compares the contents of the strings.
But string
is not the only one; looking just in the System
namespace the classes Uri
and Version
compares the content instead of checking if the variables reference the same object. It’s possible to test by comparing the output of ==
to that of Object.ReferenceEquals(Object, Object)
. The latter checks if the two references are to the same object or to different objects.
// Strings are highly optimized to share storage space. Using a StringBuilder is // a way to get two different string instances with the same value. var s1 = "Blue"; var sb = new StringBuilder("Bl"); sb.Append("ue"); var s2 = sb.ToString(); Console.WriteLine(s1 == s2); // True Console.WriteLine(object.ReferenceEquals(s1, s2)); // False var u1 = new Uri("http://localhost"); var u2 = new Uri("http://localhost"); Console.WriteLine(u1 == u2); // True Console.WriteLine(object.ReferenceEquals(u1, u2)); // False var v1 = new Version(1, 2, 3); var v2 = new Version(1, 2, 3); Console.WriteLine(v1 == v2); // True Console.WriteLine(object.ReferenceEquals(v1, v2)); // False |
Overloaded Operator ==
For string
, Uri
and Version
the default implementation of ==
is obviously not used, but instead a more specific overload is provided by the framework. In fact, all of them override the ==
operator by implementing the public static bool operator ==
.
Note that the operator method is static
. It isn’t an instance member. It isn’t virtual
. The decision to use it or not will be done entirely at compile time. If the references are cast to another type, such as object
the custom operator won’t be used. It’s enough to cast one of the operands to object
to get the default reference comparison. Using the strings from the previous example, we’ll treat one of them as an object
object o1 = s1; Console.WriteLine(o1 == s2); // False Console.WriteLine(s2 == o1); // False |
Compiling the code will give a warning: Possible unintended reference comparison; to get a value comparison, cast the left hand side to type ‘string’.
The verdict for ==
is that it behaves consistent until inheritance is involved. Since it is resolved at compile time it simply can’t deal with inheritance. So ==
will be a reasonable default for the 90%+ of cases in a program where no inheritance is involved and the compile time type of the references is the same as the run time type. For the other few percent of comparisons, something more powerful is needed.
.Equals(Object)
When the dynamic type of the objects need to be taken into consideration, the .Equals(Object)
method can be used. It is virtual
and allows each class to define it’s own behaviour. Adjusting the code above to use Equals
shows the difference.
Console.WriteLine(s2.Equals(o1)); // True Console.WriteLine(o1.Equals(s2)); // True |
The method is virtual
so in both cases, an overload of .Equals()
on String
will be called. But, the overload resolution is done on the static (i.e. compile time) type. Which means that in one case String.Equals(Object)
will be called and in the second case String.Equals(String)
. The only difference between them is that the former has to cast the parameter, which is a small performance penalty. Providing a specialized overload with the right type can give some performance improvements, so for library code like String
it’s a good idea to provide that overload.
The IEquatable<T>
Interface
All types inherits the .Equals(Object)
method from Object
, so it can be used on any type in the .NET framework. In some cases it also makes sense to mark a type as implementing the a more specific version of .Equals()
, comparing to the right type. That is exactly what the IEquatable<T>
interface does.
public interface IEquatable<T> { bool Equals(T other); } |
With that, it might be tempting to wrap up this post and declare it done. But there are a few important details to add, regarding consitency.
Consistency
The first observation regarding consistency is that for non-virtual calls, the basic mathematical requirements of an equivalence relation should hold:
a == a
anda.Equals(a)
should always be true (Reflexivity).a == b
,b == a
,a.Equals(b)
andb.Equals(a)
should always give the same result. (Symmetry)- If
a == b
is true andb == c
is true, thena == c
should also be true (Transitivity). The same applies toa.Equals(b)
,b.Equals(c)
anda.Equals(c)
.
There is also one more important part of consistency that must be dealt with, at least if the class will ever be used in a Dictionary<TKey, TValue>
: GetHashCode()
. A dictionary works by first grouping item in buckets using the Object.GetHashCode()
virtual method. Then it ensures that it has found the right item by checking equality (by calling .Equals()
unless a custom comparer is provided). That means that if two objects are considered equal, but gives different hash codes, the Dictionary<TKey, TValue>
behave peculiar. Let’s have some fun and try!
struct Person { public int Age { get; set; } public string Name { get; set; } // A person is uniquely identified by name, so let's use it for equality. public override bool Equals(Object obj) { return (obj is Person) && ((Person)obj).Name == Name; } // For lazyness reasons we (incorrectly) use the age as the hash code. public override int GetHashCode() { return Age; } } |
The Person
class is clearly incorrectly implemented as Equals()
and GetHashCode
won’t behave consistently. If we use Person
as the key to a dictionary we can get some “fun” results.
var favColours = new Dictionary<Person, string>(); var p = new Person() { Age = 1, Name = "Alice" }; favColours[p] = "Blue"; // Happy birthday Alice! p.Age = 2; favColours[p] = "Green"; Console.WriteLine(favColours.Count); // 2 var keys = favColours.Keys.ToArray(); Console.WriteLine(object.ReferenceEquals(keys[0], keys[1])); // True |
The output of that snippet of code shows we have two person objects (being a struct, a copy is made when stored in the dictionary) that are used as keys. They have resulted in different entries in the dictionary – but comparing them they are equal. That’s confusing. Don’t go there.
When implementing custom equality, three different methods should always be implemented and behave consistently.
- Make an overload for the
==
operator. - Override
.Equals(Object)
and optionally provide an optimized.Equals(MyType)
. - Override
.GetHashCode()
and make sure that it returns the same hash code for all objects that compares are equal.
That’s all for now regarding equality; in my next post I’ll have a look at comparisons with IComparable
.
Hey, I like the post you mentioned the usage of .Equals(object) and == operator.
But I have a doubt that why there are two separate Equals methods in .Net
and why there is a need of .Equals(object) to be a member(property) of object class instead of a method
Thanks in Advance !!