Introduction
This post will focus on implementing Equality in Value Types, i.e., overriding Equality behavior for our own implemented Value Types which basically defines how to decide whether two objects of a type are equal or not.
Background
As we are now aware of the fact that the technique for checking Equality for Value Types and Reference Types is different, it would be better if we discuss both of them separately. That’s why we will be focusing on each of them in separate posts to have clear understanding on how we can override the Equality for both of these.
This post will particularly focus on the Value Types while for Reference Types, we will see in some future post.
Why do we need it?
The first question that might come in mind is “Why should we override the Equality for Value Type?”. We will see this with a simple example, which will help you understand the fact that overriding the Equality behavior for Value Types is always a good idea indeed. We will define a struct later down the road and will implement the Equality of it.
Steps to implement Equality for Value Type
For overriding the Equality for a Value Type, there are some necessary steps as mentioned below.
We will need to,
- Override the virtual Equals method of Object
- Implement IEquatable<T> interface and provide the implementation of Equals method of it
- Provide the overloaded implementation for == and != operator method
- Override the GetHashCode method of Object class
Possible Reasons for implementing Equality
So, before we start discussing how we can implement Equality for a Value Type, let’s think a little on what are the possible reasons that would make us think that we should define our own Equality behavior for a Value Type instead of using the default one that framework already provides.
So, why would you want to override it, the following are the main reasons for that,
- We want to be able use == operator for comparing the objects of our Value Type. If you remember we discussed previously that == operator does not work for value types and to make it work we need to do some implementation (i.e. overload == operator) in our particular Value Type
- We have also seen previously that the framework provided implementation uses Reflection for checking the values of each field of that type which is obviously affect the performance as Reflection is slow which results in poor performance of code.
- We also need sometimes different behavior for comparison of a particular type, though this is not usually required but can be needed in some cases, as the default behavior for Value Types considers two objects to be equal if all of their fields also have the same content which is absolutely fine most of the time.
It is a recommended approach to implement Equality for your own defined Value Types that would be used further in your code base. Other developers using your defined Value Type would not get surprised or frustrated when trying to compare two objects of it by using == operator and came to know that they can’t do it.
We can skip overriding the equality for those types that we know we will be using internally in the code base and won’t be exposed to complete code base for usage and we know what we wouldn’t be needing to compare the objects frequently, so implementing equality for those types is not that much needed and would be a waste of time as a result.
Example
We will create an example struct named Employee to illustrate about implementing Equality for a Value Type. This is how our Employee struct looks like now,
- public struct Employee
- {
- public string Name { get; }
-
- public Gender Gender { get; set; }
-
- public Department Department { get; set; }
-
- public Employee(string name, Gender gender, Department department)
- {
- Name = name;
- Gender = gender;
- Department = department;
- }
-
- public override string ToString()
- {
- return Name;
- }
-
- }
-
-
- public enum Department
- {
- HumanRecource,
- QualityAssurance,
- SoftwareDevelopment,
- ProjectManagement,
- ITOperations
- }
-
- public enum Gender
- {
- Male,
- Female
- }
We have a very simple Employee type as example which has a string property which will contain the name of employee and one enumeration property to hold the gender of employee and another enumeration for Department which will be used to track the Department of the employee. We will define our own implementation later to dictate that how to decide if two employee instances are equal or not.
There are multiple things that we need to take care of and implement in our Employee type for implementing Equality. The first thing that we need to do is override the Object.Equals method so that instead of default implementation getting invoked which is obviously slow because of Reflection involved, by providing implementation for this method it will enhance the performance of the Type, so overriding Equals method will eliminate the equality check using Reflection and will make it efficient and we will be able to see significant performance improvement.
But wait, Object.Equals method takes parameter of type Object which means that boxing will happen and we know it also hurts the performance of our code, as boxing and unboxing has its own cost involved, so here we would want to avoid that one as well some way.
For avoiding both Reflection and boxing we will need to implement the IEquatable<Employee> interface for our type Employee. Now we will have performant implementation of Equals method in comparison with the framework provided one and another plus point of doing this is that now we have a much better implementation which is also type safe.
We will need to provide the overloaded implementation for == and != operators as it is normally considered a good practice to do all of the following things when overriding Equality:
- Equals method overriding
- Implementing IEquatable<T> interface for that type
- Implementing == and != overloaded methods
- GetHashCode method overriding
This is important to do because it will make sure that checking for equality for two objects of that type will give the same result, otherwise it can be confusing for the calling code who will be utilizing our type and can cause problems in future.
For Example, if we just implement the overloads for == and != operator and we don’t provide the override for Object.Equals method then what will happen is that the result of Equals method can be different from the result of == operator which will be troublesome for the code that will be utilizing our types.
There is also another method in Object class called GetHashCode, whenever we override the Object.Equals method for a type then another thing that must be done is overriding the GetHashCode method as well.
Step1 - Implementing IEquatable<T> Interface
Now let’s jump in to Visual Studio and let’s start implementing the IEquatable<Employee> interface for out Person type. We will need to inherit our struct from IEquatable<Employee> and using the code refactoring feature of Visual Studio.
It will add the Equals method without any implementation for the IEquatable<Employee> which would look like,
- public bool Equals(Employee other)
- {
- throw new NotImplementedException();
- }
Now we just need to provide the logic which would be used to identify that the two objects of Employee are equal or not which for this particular example we will do by checking the name and department of the employee is same, or we can also add the Gender if we like as this is just for getting understanding how we can implement it so it doesn’t matter much.
After implementing the Equals method, here is how the method now looks like,
- public bool Equals(Employee other)
- {
- var IsEqual = this.Name == other.Name && this.Department == other.Department;
-
- return IsEqual;
- }
So the two objects of Employee will be considered Equal if they both contain in Name field and Department field same value. Our one field is of type String which does the value equality for == operator and the other is Enum which is a primitive type in C# so we know that in case of primitive type also == operator checks for value Equality, so the both operations will check for value equality which is what we are trying to do here.
Step 2 - Overriding Object.Equals Method
As we are done with the IEquatable<Employee> part, let’s now override the Equals method so that it also returns the same result that we would get when checking for equality via IEquatable<Employee>, the implementation for which would be,
- public override bool Equals(object obj)
- {
- var IsEqual = false;
-
- if(obj is Employee)
- {
- IsEqual = Equals((Employee)obj);
- }
-
- return IsEqual;
- }
What we are doing here is that first we need to make sure that the object that is passed in as parameter is of type Employee which totally makes sense as Object.Equals is not type safe and it is possible to pass an object of type Person instead of Employee and the compiler will not give any compile time error but the code would fail at run-time in that case, but adding that if block would save us from breaking the code at run-time and the method will simply return false which is the correct result as object of type Person and Employee can never be equal.
The thing to note here is the this Equals override will be less efficient than Equals method implementation of IEquatable<T> because the former one has cost of Boxing when it will get called and then we are unboxing it back to Employee which IEquatable<Employee> implementation saves us from these.
What we can do is try to always call using the IEquatable<T> method, if we are concerned about performance and memory cost.
Step 3 - Overloading == and != Operator
Now let’s also implement the == and != operator for Employee which would make sure that checking for Equality using these will return consistent result what we were getting using Object.Equals or IEquatable<Employee> Equals method. So let’s add the implementation for == operator first which is,
- public static bool operator ==(Employee employee,Employee otherEmployee)
- {
- var IsEqual = employee.Equals(otherEmployee);
-
- return IsEqual;
- }
The method is pretty simple, it is also reusing the Equals method implementation that we did for IEquatable<Employee>, if we build the code it would not build and will give error saying that it hasn’t found the implementation of != operator, in C# if we overload == operator for a type it is mandatory to provide the implementation for inverse of it i.e. != operator implementation as well, otherwise we will get the following compile time error,
Error CS0216
The operator 'Employee.operator ==(Employee, Employee)' requires a matching operator '!=' to also be defined.
If we have overloaded any one of the operator either == or != we would need to implement the other one as well, so let’s implement the other one as well,
- public static bool operator !=(Employee employee, Employee otherEmployee)
- {
- var IsNotEqual = !employee.Equals(otherEmployee);
-
- return IsNotEqual;
- }
We can see that it is just inverting the result returned the Equals method of IEquatable and passing it back to the caller. Now if we again build our Solution we will be able to build it successfully without any errors.
Step 4 - Implementing GetHashCode
It is a practice and considered mandatory that if we override the Equals method for a type then we must also provide the overridden implementation for GetHashCode.
If we peek in to the implementation of Object we will see that there is a virtual method present in it named GetHashCode, its purpose is to return the 32-bit hash of the value which is contained in the object itself.
You might be wondering what is the purpose of this code, the GetHashCode method is used by types that internally use HashTables to store the objects which are normally Collections so they consume this method and get the hash value of the object to be used in the hash table as HashTables utilize the hash codes of the objects. In Framework Class Libraries the Dictionary<TKey,TValue> also uses HashTable internally.
Hash Codes is a complete topic on which a full article can be written to cover its aspects and we will not not be focusing in details on it. How Hash Tables work is that if two objects return true when called Equals method on them, then the Hash Codes for both objects should also be returned same when we call GetHashCode method on both which in our case would mean that the following line should return true as well.
So what it actually means is that If Employee.Equals(OtherEmployee) results in true then Employee.GetHashCode() == OtherEmployee.GetHasCode() should also return true as result. If the GetHashCode method is not implemented how it is required then the using Dictionary on our type wouldn’t work properly and can cause problems.
Now let’s add the implementation for our Employee type GetHashCode method which would be,
- public override int GetHashCode()
- {
- return Name.GetHashCode() ^ Department.GetHashCode();
- }
So it is quite simple to understand that what we are doing above, we are just taking the HashCode for our String field Name and Department Enumeration and combining them using XOR as both the framework provided types so there is already implementation presentation for generating the Hash Code for these types, so we are just reusing the Hash Codes that are already available and provided by framework.
Now let’s add some code in the Main method to verify what we have implemented is working as expected, add the following code in the Main and run it,
- static void Main(string[] args)
- {
- Employee ehsan = new Employee("Ehsan Sajjad", Gender.Male, Department.SoftwareDevelopment);
- Employee ehsan2 = new Employee("Ehsan Sajjad", Gender.Female, Department.SoftwareDevelopment);
-
- Employee bilal = new Employee("Bilal Asghar", Gender.Male, Department.QualityAssurance);
-
- object ehsanObj = ehsan;
-
- Console.WriteLine(ehsan.Equals(ehsan2));
- Console.WriteLine(ehsanObj.Equals(ehsan2));
- Console.WriteLine(ehsan == ehsan2);
-
- Console.WriteLine(ehsan.Equals(bilal));
- Console.WriteLine(ehsanObj.Equals(bilal));
- Console.WriteLine(ehsan == bilal);
-
- Console.ReadKey();
The following is the result which was printed on the Console,
We can observe the for ehsan and ehsan2 objects are the three has evaluated to return true which of course means that both are equal which was expected as this is how we defined to check for equality in our type, though we defined Gender field different in both objects but it has evaluated that both are equal as we are not considering Gender field when deciding that if two are equal or not.
Summary
Following are the points that we learned in this post,
- How to provide Custom implementation for Equality checking of Value Types.
- Implementing Equality for Value Types is normally good to do as it would improve performance by eliminating boxing/unboxing and reflection cost which can make our code inefficient.
- There are multiple things to take care of when overriding Equality for Value Types which includes,
- Implementing IEquatable<T> interface of that Value Type which will have type safe Equals method for checking two object of a type T
- Overriding the Object.Equals method which in turn calls the Equals method which we implemented for IEquatable<T>
- Implementing the == and != operator overloads which also call the Equals method on IEqutable<T>
- Implementing GetHashCode of Object for our value type T
- This is the recommended approach for implementing Equality for Value Types and this is not the recommended way in case of Reference Types.