How do you compare two objects in C#? (testing for equality)

It's common to need to compare two objects for equality, such as when detecting changes before a save operation. What does it mean to define equality in C#? What is given to you out of the box, and what can you define yourself? Let's take a look!

How do you compare two objects in C#? (testing for equality)

Hacktoberfest and the promise of free t-shirts had me looking for a project to help with this month. That's how I stumbled across GeneGenie.Gedcom, a genealogical library written in C# (you can read more about it here), and found myself reviewing everything I know about class equality.

I focused on implementing some logic to make sure changes were correctly detected, and in order to do that I had to define "equality" for each class that represented some facet of genealogical research. Why was that? Let's take two instances of a class – one with data that's fresh from the database, and another that's potentially been modified by the user. Any inequality between the two indicates one or more changes. That could be really important when, for example, it comes time to save changes.

So what does it mean to define equality in C#? And when do you have to define it yourself, versus using the default?


What do we get, out of the box?

Everything derives from the base object class in C#. In fact, you could define your own class like this.

public class Person : Object
{
    // properties go here
}

Doing that wouldn't make sense since it's implicit anyway, but it demonstrates that we get the object class and everything that comes along with it by default. And one of those things is the Equals method. What's it do for us?

[T]he Equals(Object) method tests for reference equality, and a call to the Equals(Object) method is equivalent to a call to the ReferenceEquals method. Reference equality means that the object variables that are compared refer to the same object. (MSDN)

In other words, the default test for "equality" is that two instances of a class are literally the same instance. This is true when two variables point to the same instance in memory. That's what you get out of the box, without doing anything else yourself, but as we'll see it's usually not what you want.

In the code below, p2 references the same instance as (is equal to) p1. Even though p3 has the same values for name and age, it's not equal. There are cases when you want this behavior, but not often.

using System;
 
public class Program
{
    public static void Main()
    {
        Person p1 = new Person { Name = "Jay", Age = 25 };
        Person p2 = p1;
        Person p3 = new Person { Name = "Jay", Age = 25 };
 
        Console.WriteLine(p1.Equals(p2));  // True
        Console.WriteLine(p1 == p2);       // True
 
        Console.WriteLine(p1.Equals(p3));  // False
        Console.WriteLine(p1 == p3);       // False
    }
}
 
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

The rules are different for a struct but I'm not even going to cover that... 8 years using C# professionally and I don't recall creating my own struct once. If you're interested though, you can find more info in the C# guide.


Defining Your Own Equality

Most of the time, you want to be in total control of exactly what makes two instances of your class equal to one another.

Overriding Object.Equals

Since the base Object.Equals method is marked as virtual you can override it in any class that derives from Object, which is... well, everything!

In the code below, the Person class now overrides the base Equals method and defines what "equal" means to it. Note how that changes the result of p1.Equals(p3) since it's now comparing the name and age instead of the reference.

using System;
 
public class Program
{
    public static void Main()
    {
        Person p1 = new Person { Name = "Jay", Age = 25 };
        Person p2 = p1;
        Person p3 = new Person { Name = "Jay", Age = 25 };
 
        Console.WriteLine(p1.Equals(p2));  // True
        Console.WriteLine(p1 == p2);       // True
 
        Console.WriteLine(p1.Equals(p3));  // True
        Console.WriteLine(p1 == p3);       // False
    }
}
 
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
 
    public override bool Equals(object obj)
    {
        var other = obj as Person;
 
        if (other == null)
            return false;
 
        if (Name != other.Name || Age != other.Age)
            return false;
 
        return true;
    }
}

Overloading the == operator

Once you override Object.Equals to define your own equality, most people are going to expect that the == operator will perform the same way and not produce different results.

In the code below, I've overloaded the == operator so that it follows the same logic as Equals. (Actually, it just calls Equals, which is even better. Whenever possible, stay DRY!)

Notice how p1 == p3 is true now.

using System;
 
public class Program
{
    public static void Main()
    {
        Person p1 = new Person { Name = "Jay", Age = 25 };
        Person p2 = p1;
        Person p3 = new Person { Name = "Jay", Age = 25 };
 
        Console.WriteLine(p1.Equals(p2));  // True
        Console.WriteLine(p1 == p2);       // True
 
        Console.WriteLine(p1.Equals(p3));  // True
        Console.WriteLine(p1 == p3);       // True
    }
}
 
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
 
    public override bool Equals(object obj)
    { 
        if (!(obj is Person))
            return false;
 
        var other = obj as Person;
 
        if (Name != other.Name || Age != other.Age)
            return false;
 
        return true;
    }
 
    public static bool operator ==(Person x, Person y)
    { 
        return x.Equals(y);
    }
 
    public static bool operator !=(Person x, Person y)
    {
        return !(x == y);
    }
}

Interfaces and Equality as a Contract

In some cases, certain components of the .NET framework use interfaces to define equality. Instead of relying on the virtual Equals method, they use an interface as a contract to let you know what methods they expect your class to implement.

To explain that a little better, let’s look at an example using LINQ's DISTINCT method. According to the docs, it expects your class to implement the IEquatable<T> interface (where T is your class type), which forces you to include a bool Equals(T other) method in your class.

Similar to SQL, the DISTINCT method is used to remove duplicates from a collection. When it comes to primitive types the work is done for you, because those classes already implement the interface like this:

public struct Int32 :
    IComparable, IFormattable, IConvertible , IComparable<Int32>, IEquatable<Int32>
{
    internal int m_value;
    ...
    ...
 
    public bool Equals(Int32 obj)
    {
        return m_value == obj;
    }
}
 
var distinctNumbers = new[] {4,3,5,4,3,7,3}.Distinct();
Console.WriteLine(string.Join(",", distinctNumbers));
 
// Output:
// 4,3,5,7

To do the same thing in your own class, you can implement the same interface. After you implement Equals(T) you need to define GetHashCode as well – per the docs, the hash codes of two instances need to be equal too.

using System;
using System.Collections.Generic;
using System.Linq;
 
public class Program
{
    public static void Main()
    {
        Person p1 = new Person { Name = "Jay", Age = 25 };
        Person p2 = p1;
        Person p3 = new Person { Name = "Jay", Age = 25 };
 
        var people = new List<Person> { p1, p3 };
 
        Console.WriteLine(p1.Equals(p3));
 
        Console.WriteLine(people.Distinct().Count());
    }
}
 
public class Person : IEquatable<Person> 
{
    public string Name { get; set; }
    public int Age { get; set; }
 
    public bool Equals(Person other)
    {
        if (ReferenceEquals(other, null))
            return false;
 
        if (ReferenceEquals(this, other))
            return true;
 
        return Name.Equals(other.Name) && Age.Equals(other.Age);
    }
 
    public override int GetHashCode()
    {
        int hashName = Name == null ? 0 : Name.GetHashCode();
        int hashAge = Age.GetHashCode();
 
        return hashName ^ hashAge;
    }
}

It'll actually work if you just override Equals(object) and leave out IEquatable<T>, but I like that the interface defines a method that uses generics so you don't have to unbox the object and check the type, etc. It saves a few steps.


Defining Your Own Contracts

You can take this all a step further by defining your own interfaces that extend IEquatable<T>, or by creating abstract classes that force your class to implement certain functions.

Interfaces that extend interfaces

Let's look at an example of extending an interface first. I'm tired of the "person" example, so let's switch to the Legend of Zelda. :)

Here's an example of two items that implement our own IWeapon<T> interface, which in turn extends the IEquatable<T> interface. Implement the one, and you get the other for free.

public class Sword : IWeapon<Sword>
{
    public enum SwordLevel { Fighter, Master, Tempered, Golden }
    public SwordLevel Strength { get; set; }
 
    public bool Equals(Sword other)
    {
        return Strength == other?.Strength;
    }
 
    public bool IsEquipped { get; set; }
}
 
public class Shield : IWeapon<Shield>
{
    public enum ShieldLevel { Fighter, Red, Mirror }
    public ShieldLevel Strength { get; set; }
 
    public bool Equals(Shield other)
    {
        return Strength == other?.Strength;
    }
 
    public bool IsEquipped { get; set; }
}
 
public interface IWeapon<T> : IEquatable<T>
{
    bool IsEquipped { get; set; }
}

Abstract classes that implement interfaces

Here’s one more similar example, using an abstract class that implements the IEquatable<T> interface. Now we can declare GetHashCode as a member of the abstract class, forcing our other classes to implement it... less error-prone than using the interface directly.

public class Sword : Weapon<Sword>
{
    public enum SwordLevel { Fighter, Master, Tempered, Golden }
    public SwordLevel Strength { get; set; }
 
    public override bool Equals(Sword other)
    {
        return Strength == other?.Strength;
    }
 
    public override int GetHashCode()
    {
        return Strength.GetHashCode();
    }
 
    public override void Use()
    {
        // attack!
    }
}
 
public class Shield : Weapon<Shield>
{
    public enum ShieldLevel { Fighter, Red, Mirror }
    public ShieldLevel Strength { get; set; }
 
    public override bool Equals(Shield other)
    {
        return Strength == other?.Strength;
    }
 
    public override int GetHashCode()
    {
        return Strength.GetHashCode();
    }
 
    public override void Use()
    {
        // block!
    }
}
 
public abstract class Weapon<T> : IEquatable<T>
{
    public abstract bool Equals(T other);
 
    public abstract override int GetHashCode();
 
    public bool IsEquipped { get; set; }
 
    public abstract void Use();
}