Implement Equality for Reference Types in C#
Introduction
In the last post we discussed about why to implement equality for our value types in c# and we saw how it can help writing more efficient code when writing our own value types as the default implementation in .NET framework for value types uses reflection to check for equality of two value types on the basis of 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.
What we will learn ?
We didn't discussed about implementing equality for reference types so in this post we will be just focusing on overriding the equality behavior for reference types, as with references types there are other complications involved too which are needed to be addressed when implementing equality for reference types which includes inheritance as unlike value types we have inheritance feature available in case of reference types and another thing is handling nulls to as reference types can be null but is not the case for value types.
Another thing that we will see is what are the use cases where we should implement equality for reference types, we already saw the reasons why we need to do this in case of value types.
We will be overriding equality for a reference type to demonstrate the concepts the same way as we did in case of value types to have clear understanding of the concepts here. We will see what the necessary steps are to do when implementing equality for a reference type and another important thing we will learn is how to implement it when we have inheritance involved too in reference types. For example, class A implements equality for itself but in future a class B inherits from A, in the case how the class B would implement the equality for itself without breaking the equality behavior of class A.
Let's get Started
The way we implement equality for reference types is little different than what we do in case of value types. As we discussed above in case reference types we also have to deal with inheritance too, which we never need to handle in 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 of value types.
Now let's create a reference type for which we will implement equality in this post next. So let’s start creating the class for which we will implement equality in this post.
We will use the Car class as 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 the ability to use Object Orientation principles in it and one of the important part of which is Inheritance and we will be looking in to how two objects are checked for equality when inheritance is also in play and for that we will consider the Car type as base class and we will extend it with a subclass to understand the behavior how it behaves and how we can implement it.
Now we will create a derived class which will have overridden implementation which will be of course different than the base class. Let’s say we create a class called LeaseCar which will have an extra property which will hold the information of Lease options that are available for the lease car and the object will hold the specific options chosen at time of object creation.
Our LeaseCar class will look like 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 which will hold the options select for the car to be leased and we have provided overridden implementation for ToString() method too which will return the name of down payment information, Installment Plan and Monthly Installment for the car in comma separated form.
And our LeaseOptions implementation looks like :
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; } }
First, we will write some basic code to see what the default behavior for is comparing two reference types for equality when there is no implementation being provided specifically about how to compare 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 actually 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 and we are clear here that it is comparing the references for equality not values in it as 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 same as both will check for the references of two objects are pointing to same memory location or not.
One thing more to note is that we saw previously that for value types for == operator to get working for doing comparison 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 it’s case is little different which we have seen in one of the past post.
Motivation for 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 as if we don’t provide own implementation for our value-types the framework will use reflection to compare the fields, properties of both objects for equality, but some of us might be having question in mind that Why do we need to implement for Reference types as well?
Here's the answer
The answer to it 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 let’s say two of the properties in the reference type are having same value in both objects, in these kind of situations we would need to implement equality for those reference types ourselves instead of depending on the default behavior of framework for reference types.
So, we can say it this way as well that when we want our objects to be checked for equality on the basis of values in it instead of the reference of the objects. In general, it is not commonly needed to implement equality for reference types than value types.
Possible Use Cases for Overriding Reference Equality
One example for this kind of case can be a class named Marks which holds the marks of each subject for a student and let’s say we frequently need to compare the marks of students with each other for any reason, in that case we can override the implementation for Marks class to compare the marks of each subject between two objects. Another case can be that we might have a class holding the location details as Latitude Longitude and we would want to check for equality on the basis of those values to be equal.
Examples
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 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 Main method:
void Main() { User user1 = new User() { FirstName="ehsan", LastName="sajjad", EmailAddress="ehsansajjad@yahoo.com" }; User user2 = new User() { FirstName="ehsan", LastName="sajjad", EmailAddress="ehsansajjad@yahoo.com" }; 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 exactly same and equal but as the User is a reference type so it is doing reference equality here not value.
Expected Consequences of 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 reference equality check not value based, so there should be a good reason to override the default behavior of 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 and we use take into account that does overriding the equality for it will make it more easy for the consumers of it or not, the result can be either one depending on what is the actual purpose of the that reference type and how the consumer code would be utilizing it.
Alternative Approach for Overriding Reference Type Equality
.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 would need to write a EqualityComparer class which would inherit from IEqualityComparer<T> and we would write the logic there to compare the reference type objects as value based. This actually allows developers to plug in for value equality check when needed otherwise the default behavior would be the reference equality check which is also default behavior in .NET framework for reference types. So, using this approach gives more flexibility when doing equality check, but one thing to note is that using this approach we would not be able to do value equality check using == operator and we will need to call the Equals method on the instance of equality comparer and passing both objects as 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 will include is overriding the Object.Equals method so that it would compare two car objects using the values of its properties.
The following will be the code for our base class Employee as overridden implementation of Equals method of Object.
public override bool Equals(object obj)
{
if (obj == null)
return false;
if (ReferenceEquals(obj, this))
return true;
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 passed in parameter is not null and if it's null we simply tell the caller that both objects are not equal and we would be comparing it with the object which actually called this instance method so it's obvious that this wouldn't be null.
Then we check if using ReferenceEquals() method that if both instances are reference to same object which means we are comparing the object to itself and that obviously get us to the result that both objects are equal of course, so we return true in that case. This check is useful in giving 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 properties values of our type.
Another thing that we need to make sure before performing the values equality is that that type of passed in parameter object is same that of the this and that's what we are trying if they both have different type that simply means that they are not same object so we just return false as instances of two reference types wouldn't be equal normally.
Lastly, we have code which would check that if both objects have same values for the properties that we are checking then we can say that both objects are same and we return true as result.
We also need to provide the implementation of GetHashCode() method so that it aligns with our implementation of Equals() method, it would be quite 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 in to 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 previous posts too about what are the essential steps to do when overriding equality for a type, so here we have implemented two method, 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 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 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 needed to be implemented when overriding equality for a Reference type.
Overriding Derived Class Equality
Now, we will implement the equality in LeaseCar class which inherits from Car class. First of all like we did for Parent class we will provide the Equals() method override and here is the implementation for it:
public override bool Equals(object obj) { if (!base.Equals(obj)) return false; LeaseCar objectToCompare = (LeaseCar)obj; return this.Options == objectToCompare.Options; }
For the derived type we need to do it differently than for the base reference type. In Base we were checking 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 initial checking. So first we call the Equals implementation of Base and see what it returns. If the base implementation call results shows 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 same so, they are different it means.
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 and what base class Equals is doing is we already saw that above that it makes sure both instances are of same type and have 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 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 LeaseOptions:
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 and what base class Equals is doing is we already saw that above that it makes sure both instances are of same type and have 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 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 LeaseOptions:
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 true; 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 will 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 Derived class and also would be used in 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 and as we have also provided the implementation for LeaseOptions class too to generated the GetHashCode() the same way using all the fields that we want.
Now we also need to provide the == and != operator overloads as well which would be similar to what we did in base class to call the static object.Equals method which will eventually call the overridden implementation of LeaseCar via call to virtual Equals method of Object class.
The methods implemented would be like:
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 same implementation what we have in the base class, so if we want to skip implementing these two we can and the result would be same what we will get using these. But for the sake of giving the whole implementation for derived class too we are adding it for 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); } }
In this post, we learned the following things:
- We saw that how we can implement equality behavior for reference types so that they can act as value-types when comparing two objects.
- Due to inheritance feature that is supported in 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 case of Value Types we should be implementing as that will give us performance hit as we will be able to eliminate the default behavior which uses reflection for custom Value Types.
- For implementing equality following are the thing to be done:
- Override the Equals() method of Object class in the Reference Type
- Override the GetHashCode() method of Object class in the Reference Type
- Implement overloads for == and != operator for the type