Introduction
In the previous post, we discussed why we should implement equality for our value types in C# and we saw how it can help in writing more efficient code when writing our value types. The default implementation in the .NET framework for value types uses reflection to check for equality of two value types based on the values they contain so it is always a better option to override the equality behavior for our value types which, of course, will be a performance gain during execution.
We didn't discuss implementing equality for reference types so, in this post, we will be just focusing on overriding the equality behavior of reference types, as with reference types, there are other complications involved too that need to be addressed when implementing equality for reference types. This includes inheritance, as unlike value types, we have the inheritance feature available in the case of reference types and also in handling nulls, as reference types can be null but this is not the case for value types.
Another thing that we will see is the use cases where we should implement equality for reference types, we already saw the reasons why we need to do this in the case of value types.
We will be overriding equality for a reference type to demonstrate the concepts the same way as we did in the case of value types to have a clear understanding of the concepts here. We will see what are the necessary steps to do when implementing equality for a reference type and another important thing is that we will learn to implement it when we also have inheritance involved in reference types. For example, class A implements equality for itself but in the future, class B inherits from A, in this case, how would class B implement the equality for itself without breaking the equality behavior of class A?
Why Implement it?
The way we implement equality for reference types is a little different than what we do in the case of value types. As we discussed above in the case of reference types, we also have to deal with inheritance, which we never need to handle in the case of value types as they are by default marked as sealed in the .NET framework, so expecting the implementation of equality for reference types would be more complicated than for value types.
Now, let's create a reference type for which we can implement equality in this post later. So let’s start creating the class for which we will implement equality in this post.
We will use the Car class as an example as we want to understand it for Reference types. The following is the implementation of our Car class.
public class Car
{
public int MakeYear { get; private set; }
public Decimal Price { get; private set; }
public Car(int makeYear, Decimal price)
{
MakeYear = makeYear;
Price = price;
}
public override string ToString()
{
return $"Model: {MakeYear}, Price: {Price}";
}
}
We know that classes are Reference types and because of that we can use Object Orientation principles in it and an important part of that is Inheritance. We will be looking into how two objects are checked for equality when inheritance is also in play and for that, we will consider the Car type as a base class and we will extend it with a subclass to understand the behavior and how we can implement it.
Now, we will create a derived class that will have overridden implementation which will be of course different than the base class. Let’s say, we created a class called LeaseCar which will have an extra property that will hold the information of Lease options that are available for the lease car and the object will hold the specific options chosen at the time of object creation.
Our LeaseCar class will look like the following.
public class LeaseCar: Car
{
public LeaseOptions Options { get; set; }
public LeaseCar(int makeYear, Decimal price, LeaseOptions options) : base(makeYear, price)
{
Options = options;
}
public override string ToString()
{
return $"{nameof(DownPayment)}: {DownPayment}, {nameof(InstallmentPlan)}: {InstallmentPlan.ToString()}, {nameof(MonthlyInstallment)}: {MonthlyInstallment}";
}
Our LeaseCar contains one new property that will hold the options for the car to be leased and we have provided overridden implementation for the ToString method too which will return the name of down payment information, Installment Plan, and Monthly Installment for the car in comma separated form.
Our LeaseOptions implementation looks like this.
public class LeaseOptions
{
public Decimal DownPayment { get; }
public InstallmentPlan InstallmentPlan { get; }
public Decimal MonthlyInstallment { get; }
public LeaseOptions(Decimal downPayment, InstallmentPlan installmentPlan, Decimal monthlyInstallment)
{
DownPayment = downPayment;
InstallmentPlan = InstallmentPlan;
MonthlyInstallment = monthlyInstallment;
}
public override bool Equals(object obj)
{
if (obj == null)
return false;
if (ReferenceEquals(obj, this))
return false;
if (obj.GetType() != this.GetType())
return false;
LeaseOptions optionsToCheck = obj as LeaseOptions;
return this.DownPayment == optionsToCheck.DownPayment
&& this.InstallmentPlan == optionsToCheck.InstallmentPlan
&& this.MonthlyInstallment == optionsToCheck.MonthlyInstallment;
}
public override int GetHashCode()
{
return this.DownPayment.GetHashCode()
^ this.InstallmentPlan.GetHashCode()
^ this.MonthlyInstallment.GetHashCode();
}
public static bool operator ==(LeaseOptions optionsA, LeaseOptions optionsB)
{
return Object.Equals(optionsA, optionsB);
}
public static bool operator !=(LeaseOptions optionsA, LeaseOptions optionsB)
{
return !Object.Equals(optionsA, optionsB);
}
}
First, we will write some basic code to see the default behavior for comparing two reference types for equality when there is no implementation being provided specifically about how to compare the two. Following is the code for our Main Program.
static void Main(string[] args)
{
Car carA = new Car(2018, 100000);
Car carA2 = new Car(2018, 100000);
Car carB = new Car(2016, 2500000);
LeaseCar leasedCarA = new LeaseCar(2014, 2500000, new LeaseOptions(1000000,InstallmentPlan.FiveYears, 10000));
LeaseCar leasedCarA2 = new LeaseCar(2014, 2500000, new LeaseOptions(1000000, InstallmentPlan.FiveYears, 10000));
LeaseCar leasedCarB = new LeaseCar(2016, 2500000, new LeaseOptions(1000000, InstallmentPlan.FiveYears, 10000));
Console.WriteLine(carA == carA2);
Console.WriteLine(carA == carB);
Console.WriteLine(leasedCarA == leasedCarA);
Console.WriteLine(leasedCarB == carB);
Console.Read();
}
We can see that there are five different objects created all of which are either of type Car or their type inherits from Car due to which we can say that all objects are of type Car and we are comparing all of those with each other one by one for equality and the output is not any surprise, it is what we were expecting. All other comparison operations returned false except leaseCarA == leaseCarA which is quite obvious as we are comparing the leaseCarA object reference with itself. We are clear here that it is comparing the references for equality, not values in it as the class is a reference type. Following is the output of the above program:
We can also check using Object. Equals method instead of == operator in the above example program but the output will remain the same as both will check if the references of two objects are pointing to the same memory location or not.
One thing more to note is that we saw previously that for value types for the == operator to get working for doing comparisons, we had to define overloads for == and != in that value type. But for reference types, we don’t need to define those as it is a reference type and the base type object already takes care of doing reference comparison for any reference type except String, as its case is a little different which we have seen in one of the past posts.
Purpose of Overriding Equality for Reference Types
The motivation to provide the overridden implementation for value types is now pretty clear to all of us; i.e., the performance of the code. If we don’t provide our implementation for our value types the framework will use reflection to compare the field properties of both objects for equality, but some of us might be wondering why we need to implement for Reference types as well.
The answer is that there can be cases where we don’t want to just rely on the reference equality for two objects of a particular reference. We might want to consider two objects to be equal if two of the properties in the reference type have the same value in both objects. In this kind of situation, we would need to implement equality for those reference types ourselves instead of depending on the default behavior of the framework for reference types.
So we can say that when we want our objects to be checked for equality based on values instead of the reference of the objects, in general, we don't need to implement equality for reference types then value types.
Possible Use Cases for Overriding Reference Equality
One example of this kind of case can be a class named Marks which holds the marks of each subject for a student. Let’s say we frequently need to compare the marks of students with one another, in that case, we can override the implementation for the Marks class to compare the marks of each subject between two objects. Another cause can be that we might have a class holding the location details as Latitude and Longitude and we would want to check for equality based on those values to be equal.
Another good example can be of a Model class or any class which holds string properties and we want the equality check to be done by comparing the string values. Let’s take the following class as an example.
public class User
{
public string FirstName {get;set;}
public string LastName {get;set;}
public string EmailAddress {get;set;}
}
Let’s write the following code in the Main method.
void Main()
{
User user1 = new User()
{
FirstName="ehsan",
LastName="sajjad",
EmailAddress="[email protected]"
};
User user2 = new User()
{
FirstName="ehsan",
LastName="sajjad",
EmailAddress="[email protected]"
};
Console.WriteLine(user1 == user2);
}
We will see that these two objects are not considered equal though both are equal if we consider the values in the properties. They are all the same and equal but as the user is a reference type it is doing reference equality here not value.
Think Before Overriding Equality
We should watch closely before implementing value equality in a reference type as most of the developers would be expecting the equality comparison to do a reference equality check, not a value-based one, so there should be a good reason to override the default behavior of the framework for reference types.
The cases that we discussed above might also not be a good reason to override equality for reference types sometimes. If we are considering overriding equality for reference types we should first think about the usage of the class that we are writing. We used to take into account whether or not overriding the equality for it will make it easier for the consumers of it or not, the result can be either one depending on what is the actual purpose of that reference type and how the consumer code will be utilizing it.
Alternate Approach to Override Equality for Reference Type
The .NET framework also provided another way to provide other developers the ability to compare the objects of your class using value equality to check if they contain the same values, so we can do that without overriding the equality for the reference type.
We need to write an Equality Comparer class which will inherit from IEqualityComparer<T> and we would write the logic there to compare the reference type objects as value-based. This allows developers to the plugin for value equality checks when needed otherwise the default behavior would be the reference equality check which is also the default behavior in the .NET framework for reference types. So, using this approach gives more flexibility when doing an equality check, but one thing to note is that when using this approach we will not be able to do a value equality check using the == operator and we will need to call the Equals method on the instance of equality comparer and pass both objects as a parameter in for comparing them.
Overriding Equals in Base Class
So now we will first define the equality behavior for our base class Car in which the first thing that we will include is overriding the Object. Equals method so that it will compare two car objects using the values of their properties.
The following will be the code for our base class Employee as the overridden implementation of the Equals method of Object.
public override bool Equals(object obj)
{
if (obj == null)
return false;
if (ReferenceEquals(obj, this))
return false;
if (obj.GetType() != this.GetType())
return false;
Car carToCheck = obj as Car;
return this.MakeYear == carToCheck.MakeYear
&& this.Price == carToCheck.Price;
}
The code is pretty simple, first of all, it makes sure the value passed in the parameter is not null and if it's null we simply tell the caller that both objects are not equal and we will compare it with the object called this instance method so it's obvious that this wouldn't be null.
Then we check if when using the ReferenceEquals method both instances are the reference to the same object which means we are comparing the object to itself and that gets us to the result that both objects are equal, so we return true in that case. This check is useful in giving a little performance benefit as eventually, our latter check will also evaluate true so it's good if we get the result early within the method call and save ourselves from comparing multiple property values of our type.
Another thing that we need to make sure of before performing the values equality is that the type of the value passed in the parameter object is the same and that's what we are trying to do. If they both have different types that simply means that they are not the same object so we just return false, as instances of two reference types wouldn't be equal normally.
Lastly, we have a code that would check if both objects have the same values for the properties that we are checking then we can say that both objects are the same and we return true as a result.
We also need to provide the implementation of the GetHashCode method so that it aligns with our implementation of the Equals method, it would be quite the same the way we did for value types before, here is the code for it.
public override int GetHashCode()
{
return this.MakeYear.GetHashCode() ^ this.Price.GetHashCode();
}
We are just getting the values of the three properties and doing XOR of them. We will not go into the details in this post about why we do this and other details, we will see details about this in some later post.
We have discussed this in one of the previous posts about what are the essential steps to do when overriding equality for a type, so here we have implemented two methods, we also need to overload the == operator and != operator methods, as if we don't it would result in inconsistent and contradictory results for devs using this type.
So, let’s write the overloads.
public static bool operator ==(Car carA, Car carB)
{
return Object.Equals(carA, carB);
}
public static bool operator !=(Car carA, Car carB)
{
return !Object.Equals(carA, carB);
}
We are just calling the static Equals method which will first check both the parameters to make sure that either of them is not null and then eventually will call the virtual Equals method of the Object class and in return, the overridden implementation of our type will get called which is what we want here and for != operator we are just inverting the result of Object. Equals method.
So, we are almost done with all the things that need to be implemented when overriding equality for a Reference type.
Overriding Equality in Child Class
Now, we will implement equality in the LeaseCar class which inherits from the Car class. First of all, like we did for the Parent class, we will provide the Equals method override, and here are the implementations of it.
public override bool Equals(object obj)
{
if (!base.Equals(obj))
return false;
LeaseCar objectToCompare = (LeaseCar)obj;
return this.Options == object to compare.Options;
}
For the derived type, we need to do it differently than for the base reference type. In Base, we were checking the equality of all the properties that we consider to be checked when comparing two objects of Car so here we will be reusing the base class method to do the initial checking. So first we call the Equals implementation of Base and see what it returns. If the base implementation call results show that both instances are not equal, we do not proceed for further derived class properties checking, as the base class already told us that the two objects are not the same.
But if the base class implementation tells that that both objects are equal then we proceed further for derived class checks to be executed for which the code starts from Line 5 in the above method. What base class Equals is doing is we already saw above that it makes sure both instances are of the same type and have the same values in them and then we check the derived class properties for value equality to decide if the objects are equal or not.
But if we look at the property Options here it is also a reference type and object of type LeaseOptions class. So, we will need to implement the Equality for that as well so that we can use it in the LeaseCar in a better manner. The following is the implementation for lease options.
public class LeaseOptions
{
public Decimal DownPayment { get; }
public InstallmentPlan InstallmentPlan { get;}
public Decimal MonthlyInstallment { get; }
public LeaseOptions(Decimal downPayment,InstallmentPlan installmentPlan,Decimal monthlyInstallment)
{
DownPayment = downPayment;
InstallmentPlan = InstallmentPlan;
MonthlyInstallment = monthlyInstallment;
}
public override bool Equals(object obj)
{
if (obj == null)
return false;
if (ReferenceEquals(obj, this))
return false;
if (obj.GetType() != this.GetType())
return false;
LeaseOptions optionsToCheck = obj as LeaseOptions;
return this.DownPayment == optionsToCheck.DownPayment
&& this.InstallmentPlan == optionsToCheck.InstallmentPlan
&& this.MonthlyInstallment == optionsToCheck.MonthlyInstallment;
}
public override int GetHashCode()
{
return this.DownPayment.GetHashCode()
^ this.InstallmentPlan.GetHashCode()
^ this.MonthlyInstallment.GetHashCode();
}
public static bool operator ==(LeaseOptions optionsA, LeaseOptions optionsB)
{
return Object.Equals(optionsA, optionsB);
}
public static bool operator !=(LeaseOptions optionsA, LeaseOptions optionsB)
{
return !Object.Equals(optionsA, optionsB);
}
public override string ToString()
{
return $"{nameof(DownPayment)}: {DownPayment}, {nameof(InstallmentPlan)}: {InstallmentPlan.ToString()}, {nameof(MonthlyInstallment)}: {MonthlyInstallment}";
}
}
So, now when we call equality check for LeaseOptions it will be checked using the implementation provided and we will be reusing it in the other overloads that we need to write for the Derived class, and also it will be used in the GetHashCode implementation of LeaseCar.
We don't need to take care of null handling as we first call the base. Equals and in the implementation of that we are already taking care of nulls. Now, let’s implement the GetHashCode method for LeaseCar.
public override int GetHashCode()
{
return base.GetHashCode() ^ this.Options.GetHashCode();
}
So, what we are doing here is calling the base implementation to get the hash code of it and then XOR it with the field Options of LeaseCar.
Now we also need to provide the == and != operator overloads as well which would be similar to what we did in the base class to call the static object. Equals method which will eventually call the overridden implementation of LeaseCar via a call to the virtual Equals method of the Object class.
The methods implemented would look like this.
public static bool operator ==(LeaseCar carA, LeaseCar carB)
{
return object.Equals(carA, carB);
}
public static bool operator !=(LeaseCar carA, LeaseCar carB)
{
return object.Equals(carA, carB);
}
If we notice our above overloads are having the same implementation that we have in the base class and if we want to skip implementing these two, we can; and the result would be the same. But for the sake of giving the whole implementation for the derived class too, we are adding it to this post.
Now, as we are done with the implementation of both of our classes, let's write some code to test for the correctness of our equality implementations. Following is the code to add in the Main method.
class Program
{
static void Main(string[] args)
{
Car carA = new Car(2018, 100000);
Car carA2 = new Car(2018, 100000);
Car carB = new Car(2016, 2500000);
LeaseCar leasedCarA = new LeaseCar(2014, 2500000, new LeaseOptions(1000000,InstallmentPlan.FiveYears, 10000));
LeaseCar leasedCarA2 = new LeaseCar(2014, 2500000, new LeaseOptions(1000000, InstallmentPlan.FiveYears, 10000));
LeaseCar leasedCarB = new LeaseCar(2016, 2500000, new LeaseOptions(1000000, InstallmentPlan.FiveYears, 10000));
Console.WriteLine(carA == carA2);
Console.WriteLine(carA == carB);
Console.WriteLine(leasedCarA == leasedCarA);
Console.WriteLine(leasedCarB == carB);
}
}
Now, we can observe a form console output that our objects are getting evaluated not based on reference but using the values contained in the Field/Properties that we have dictated to use when comparing for equality.
Summary
In this post, we learned the following things
- We saw how we can implement equality behavior for reference types so that they can act as value types when comparing two objects.
- Due to the inheritance feature that is supported by Reference Types, it's more complex than Value Types to implement our Equality check behavior.
- Another thing to remember is that it is not always a good idea to implement Equality for Reference types as by default, they are compared using the object reference but in the case of Value Types we should implement it as that will give us a performance hit as we will be able to eliminate the default behavior which uses reflection for custom Value Types.
- For implementing equality, the following are the things to be done.
- Override the Equals method of Object class in the Reference Type
- Override the GetHashCode method of the Object class in the Reference Type
- Implement overloads for == and != operator for the type