.NET == and .Equals() – Passion for Coding (2024)

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().

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.

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); // TrueConsole.WriteLine(object.ReferenceEquals(s1, s2)); // Falsevar u1 = new Uri("http://localhost");var u2 = new Uri("http://localhost");Console.WriteLine(u1 == u2); // TrueConsole.WriteLine(object.ReferenceEquals(u1, u2)); // Falsevar v1 = new Version(1, 2, 3);var v2 = new Version(1, 2, 3);Console.WriteLine(v1 == v2); // TrueConsole.WriteLine(object.ReferenceEquals(v1, v2)); // False

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); // FalseConsole.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.

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)); // TrueConsole.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.

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.

The first observation regarding consistency is that for non-virtual calls, the basic mathematical requirements of an equivalence relation should hold:

  • a == a and a.Equals(a) should always be true (Reflexivity).
  • a == b, b == a, a.Equals(b) and b.Equals(a) should always give the same result. (Symmetry)
  • If a == b is true and b == c is true, then a == c should also be true (Transitivity). The same applies to a.Equals(b), b.Equals(c) and a.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); // 2var 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.

.NET == and .Equals() – Passion for Coding (2024)
Top Articles
Latest Posts
Article information

Author: Jerrold Considine

Last Updated:

Views: 6375

Rating: 4.8 / 5 (78 voted)

Reviews: 93% of readers found this page helpful

Author information

Name: Jerrold Considine

Birthday: 1993-11-03

Address: Suite 447 3463 Marybelle Circles, New Marlin, AL 20765

Phone: +5816749283868

Job: Sales Executive

Hobby: Air sports, Sand art, Electronics, LARPing, Baseball, Book restoration, Puzzles

Introduction: My name is Jerrold Considine, I am a combative, cheerful, encouraging, happy, enthusiastic, funny, kind person who loves writing and wants to share my knowledge and understanding with you.