The Problem

By default, up to now I always used == to compare Objects and ValueTypes in C#. But this has led to some serious issues in my last project.

While I was well aware of the fact that this does an equality check on the reference in case of objects (when not overloaded) and a comparison of values in case of most ValueTypes1, I didn’t foresee that the unreflected usage of == would cause a quite exhaustive round of refactoring in my project.

To be able to better explain the problem, let’s first create a setting. Let’s assume we have a class Product that implements an IProduct interface.

public class Product : IProduct
{
    public string Id { get; set; }
    public double Price { get; set; }
}
public interface IProduct
{
    string Id { get; }
    double Price { get; set; }
}

Now instances of Product are passed around your application as IProduct, ensuring that the Id property is not changed. Now you could have another class like

public class ProductCatalogue
{
    public IProduct SelectedProduct { get; set; }

    public bool IsProductSelected(IProduct product)
    {
        return SelectedProduct == product;
    }
}

By default, the code in IsProductSelected will perform a check on reference equality2. But the catch is, since we compare interfaces and not the implementations, we have no means of overloading the operator. Why is that a problem? In my case it turned out after some significant development time that the default equality check for the class Product should no longer be a check upon the reference but a check if the Id property coincides.

Some Objects Are More .Equals Than Others

A more future-proof way of performing equality checks on objects is to use the Equals method or the ReferenceEquals method if you are sure that you will only want to compare references. Using these methods has two advantages:

  1. They make the intent very clear. ReferenceEquals tells any fellow developer that the author thought about it and was reasonably sure that he/she needed to do a comparison of references. Equals signals that there may be a comparison of values going on.
  2. The Equals method may be overriden3 and the overriden comparison will also be used when comparing implementations as their common interface. Thus, you can later on change your mind about how you want to compare two objects without having to refactor maybe hundreds of lines of code.

Let’s see this in action. Using the automatically generated implementation for our Product class we get the following code:

public class Product : IProduct
{
    public string Id { get; set; }
    public double Price { get; set; }

    public override bool Equals(object obj)
    {
        return obj is Product product &&
                Id == product.Id;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Id);
    }
}

Now the Equals method returns true for two instances of the Product class, if their Id coincides. Moreover, Equals still works as expected when used with IProduct Objects. Note that Lists, Dictionaries etc. and many LINQ methods also essentially use Equals when comparing entries. As an example you cannot have two different Product instances with the same Id as Keys in a Dictionary.

IProduct product1 = new Product { Id = "test" };
IProduct product2 = new Product { Id = "test" };

Console.WriteLine(product1 == product2); //Output: false
Console.WriteLine(product1.Equals(product2)); //Output: true 

Dictionary<IProduct, int> dict = new Dictionary<IProduct, int> 
{ 
    { product1, 1 } 
};
dict[product2] = 2;
Console.WriteLine(dict[product1]); //Output: 2

To emphasize on that again: We cannot change the behaviour of == when cmparing product1 and product2, it’s a public static operator defined for Object. But by overriding Equals we may achieve to compare two interfaces based on values.

In order to implement the requested behaviour into our ProductCatalogue from above, we simply change it to

public class ProductCatalogue
{
    public IProduct SelectedProduct { get; set; }

    public bool IsProductSelected(IProduct product)
    {
        return SelectedProduct?.Equals(product) ?? false;
    }
}

Here we use the null-conditional operator to make sure that no exception is thrown when SelectedProduct is null. Of course there are also scenarios where you would want that to throw an exception.

A Word About IEquatable<T>

If you are new to an existing codebase, it can be quite cumbersome to distinguish whether an Equals check compares references or values. To make the intent clearer I definitely recommend also implementing IEquatable<T> whenever you override Equals. You will also gain a slight performance increase in some situations. Again using the Visual Studio generated methods we end up with

public class Product : IProduct, IEquatable<Product>
{
    public string Id { get; set; }
    public double Price { get; set; }

    public override bool Equals(object obj)
    {
        return Equals(obj as Product);
    }

    public virtual bool Equals(Product other)
    {
        return other != null &&
                Id == other.Id;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Id);
    }
}

It is best practice to either mark Product as sealed or theEquals method as virtual. The background is that if you don’t do that, possible derived classes will default to use the comparison of their base class. Thus instances of two entirely different classes may be considered as equal and you probably don’t want that. See also this SonarSource Rule.

Footnotes

  1. Like int, double, char and also string albeit it’s not a ValueType. 

  2. In Production Code you would probably like to have a null check in your public methods accepting references. 

  3. See the guidelines for overwriting Equals() when you decide to do so.