Equality in C#
5
(2)

I had an interesting discussion with one of my colleagues regarding equality in C# recently. It all started from a simple question: “how can I check if two objects are equal?”. Answering the question brought up some additional questions and all ended up in a crash course on equality. Therefore, I thought it might be useful to share a summary in this blog post.

Types of Equality in C#

First of all, the whole idea of equality in C# is not necessarily very straight forward since we can have different types of equality. Why? Simply because when we ask the question about equality (also called equivalence) we often mean total different things. In certain situations when we compare variables for equality we just want to know if they hold a reference to the same memory location, therefore to the object that is stored there. This is called reference equality.

Some other times when we compare for equality in C# we want to find out if all the properties (or a subset of them) of the objects in question have the same values or not. This is called value equality or structural equality.

Further, there might be cases when we just want to know if the identifiers of two different objects have the same value. This is what I would call identifier equality and even if it is not mentioned in the official Microsoft documentation, this is a type of equality that we deal with almost every day. For instance when we want to find a certain record in the database, or an element in a list and so on. In DDD the concept of identifier equality is very important since it is one of the key differences between entities and value objects.

Testing for Reference Equality

The good news is that you don’t actually have to implement any custom logic to support reference equality comparisons in your types. This functionality is provided for all types by the static ReferenceEquals() method as well as by the Equals() method. The latter actually calls the former when it is invoked. These are defined on the Object class and therefore all types will have them.

The following example shows how to determine whether two variables have reference equality, which means that they refer to the same object in memory.

static void Main(string[] args)
{
//Student class is omitted for brevity
var stud1 = new Student() {Name = "Dan", Age = 15};
var stud2 = new Student() {Name = "Dan", Age = 15};
//returns False
Console.WriteLine(stud1.Equals(stud2));

//Now the variable stud2 will have the same reference as value
stud2 = stud1;

//Returns True
Console.WriteLine(stud1.Equals(stud2));
}

However, testing value types for reference equality will always result in “False” and that’s where all the “fun” starts.

static void Main(string[] args)
{
//Student struct is omitted for brevity
var stud1 = new StudentStruct() {Name = "Dan", Age = 15};
var stud2 = new StudentStruct() {Name = "Dan", Age = 15};
//Displays: False
Console.WriteLine(object.ReferenceEquals(stud1, stud2));

//Displays False
stud2 = stud1;
Console.WriteLine(object.ReferenceEquals(stud1, stud2));
}

Not Everything is What It Seems

You probably noticed that I used the ReferenceEquals() method when testing two structs for equality. The result corresponds, of course, to the earlier stated principles that testing value types for reference equality will always return false. This makes a lot of sense since value types hold values not references to memory locations.

But what if we use the Equals() method?

static void Main(string[] args)
{
//Student struct is omitted for brevity
var stud1 = new StudentStruct() {Name = "Dan", Age = 15};
var stud2 = new StudentStruct() {Name = "Dan", Age = 15};
//Displays: True
Console.WriteLine(stud1.Equals(stud2));

//Displays True
stud2 = stud1;
Console.WriteLine(stud1.Equals(stud2));

}

Bummer! Equality check returns “True” when using the Equals method! But why?

Well, things are fairly simple. The struct class overrides the Equals() method to check for value equality instead of reference equality. And since all properties on our struct objects have the same values the value equality check returns “True”.

The same holds true for C# record types. Under the hood records are usually classes (tough we also have record structs in C#) and therefore we’d expect to have the default reference equality check. However, the good thing about records is that they were created with the idea of structural equality in mind, and that’s why they offer some clear advantages for modeling DDD value objects. Therefore, it’s only logical that the Equals() method was overridden to check for structural equality instead of reference equality.

Why is This a Problem?

If you think about the applications that you write, you’ll sure realize that there are a lot of classes that are not written by you. Moreover, you probably use a lot of classes from different libraries. This means that wherever the class author wrote an override for the Equals() method that tests for value equality instead of the default check for reference equality, you’ll probably get unexpected results when comparing two objects. It gets even worse if we think that anybody could write any type of custom logic in the override, so you don’t always get what you’d expect.

That’s why it is wise that whenever you want to test for reference equality, you should use the ReferenceEquals() method on the object class instead of the Equals() method (because this one might be overridden without your knowledge). If you want to remember only one thing from this blog posts, then that thing should be this advice! I obviously came to it the hard way.

Happy coding!

P.S: If you enjoyed this article, you might also want to check my YouTube channel for a lot more and visual .NET content. I also run a newsletter that’s different to any other newsletter as it focuses more on things that will help you become a better engineer, not just a better coder. You might want to subscribe to it as well!

How useful was this post?

Click on a star to rate it!

Average rating 5 / 5. Vote count: 2

No votes so far! Be the first to rate this post.

We are sorry that this post was not useful for you!

Let us improve this post!

Tell us how we can improve this post?

By Dan Patrascu

I solve business problems through quality code, smart architecture, clever design and cloud sorcery. Over more than 8 years I've worked and led projects of all shapes and sizes: from regular monoliths to complex microservices and everything in between. I'm also running the Codewrinkles channel and a newsletter.