Handling Equality Operator On Complex Types In C#

Comparing two objects for equality is common in C#. In some cases, equality is tested explicitly (direct comparison) and implicitly (in operations like union, except, intersect etc) in other cases. When it comes to the indirect or implicit test for equality, is where we need to take extra care of object comparison.
 

Background

 
There are quite a few extension methods for the IEnumerable<T> and IQueryable<T> types. The methods which perform set operations are very handy. For example, Intersect method produces the set intersection of two sequences. The intersection of two sets A and B is defined as the set that contains all the elements of A that also appear in B.

For more information on set operations, you can refer link,

  1. var numList = new List<int>(){ 4,5 };  
  2. var numbers = new List<int>() { 3,4 };  
  3. var commonItems = numList.Intersect(numbers);  
  4. foreach(var item in commonItems)   
  5. {  
  6.    Console.WriteLine(item);  
  7. }   
  8. //Output : 4  

Here is the result as expected, i.e. commonItems contains element 4 which is common to both the lists. But here we are comparing the values by internally using the default Equals method.

But while doing these operations for complex types or objects, the default implementation doesn’t give the generally expected result. If the current instance is a reference type, the 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.

  1. //Problem with object comparison  
  2. var students = new List<student>();  
  3. var girl = new Student() { Name = "Simran", StudentId = 4 };  
  4. var sameGirl = new Student() { Name = "Simran", StudentId = 4 };  
  5. students.Add(girl);  
  6. Console.WriteLine("Default equality : {0}",students.Contains(sameGirl));  
  7. //Output : False  
To overcome this problem, we have various techniques but with the same core idea to compare the contents of the object rather than the object/reference itself.
 
Ways to overcome the Equality problem with complex types  
 
By overriding the Equals and GetHashCode of Object base class,
  1. public class Employee  
  2. {  
  3.    public int Id { getset; }  
  4.    public string Name { getset; }  
  5.      
  6.    //Allows to override method with argument type as Object only  
  7.    public override bool Equals(Object obj)  
  8.    {  
  9.       if (obj == null)  
  10.          return false;  
  11.       var emp = (Employee)obj;  
  12.       return emp.Id == Id && emp.Name == Name;  
  13.    }  
  14.    //For hash based comparison  
  15.    public override int GetHashCode() => new { Id, Name }.GetHashCode();  
  16. }  

Here we modify the Equals method to compare the contents/properties(Id and Name) of Employee object. A thing to be noted here is that Equals method will always have argument type as Object only so it needs casting to the required type.

GetHashCode generates a hash code which is same for objects having same values of properties. This method is useful for operations with Hash based collections like HashSet<T> etc.
  1. //Equality using overridden methods - Equal and GetHashCode of Object  
  2. var emp1 = new Employee() { Id = 1, Name = "John" };  
  3. var emp2 = new Employee() { Id = 1, Name = "John" };  
  4. Console.WriteLine(emp1.Equals(emp2)); //True   
  5.   
  6. var empList = new List<Employee>();  
  7. empList.Add(emp1);  
  8. Console.WriteLine(empList.Contains(emp2)); //True  
  9.   
  10. var employees = new HashSet<Employee>();  
  11. employees.Add(emp2);  
  12. Console.WriteLine(employees.Contains(emp1)); //True  
Here the result will be True for both as the overridden Equals and GetHashCode are effectively implemented as per our need.
 

By implementing IEquatable<T> interface 

 
By implementing IEquatable interface and a corresponding Equals method and overriding GetHashCode of Object class, we can get the expected outcome. The class to be compared (Person in this case) implements this interface.
  1. public class Person : IEquatable<Person>  
  2. {  
  3.    public int Age { getset; }  
  4.    public string Name { getset; }  
  5.      
  6.    //Flexibility to use argument type other than Object  
  7.    public bool Equals(Person otherPerson)  
  8.    {  
  9.       if (otherPerson!= null && otherPerson.Age == Age && otherPerson.Name == Name)  
  10.       {  
  11.          return true;  
  12.       }  
  13.       return false;  
  14.    }  
  15.    //GetHashCode() method of Object base class is implemented for hash based comparison  
  16.    public override int GetHashCode() => new { Age, Name }.GetHashCode();  
  17. }  

Here the Equals method does not have the restriction (as evident in overridden Equals method of Object class) that the type of argument should be Object. So we use the argument of type of the class we want to compare (Person in this case). Here it compares the object otherPerson with the current instance of the class.

GetHashCode is overridden as earlier for usefulness with hash based collections.

Following is the code to test the equality using this technique,

  1. var person1 = new Person() { Age = 21, Name = "Alice" };  
  2. var person2 = new Person() { Age = 21, Name = "Alice" };  
  3. Console.WriteLine(person1.Equals(person2)); //True  

By implementing the IEqualityComparer<T> interface

 
IEqualityComparer<T> allows the implementation of customized equality comparison for collections. This interface needs to be implemented by a class that performs the comparison on two objects of class type T. This interface is implemented by a separate class like StudentComparer in the below example and not by the class (Student) itself which is being compared.
  1. public class Student  
  2. {  
  3.    public int StudentId { getset; }  
  4.    public string Name { getset; }  
  5. }  
  6. /// <summary>  
  7. /// External Class which implements IEqualityComparer to compare equality of two objects type /// </summary> public class StudentComparer : IEqualityComparer<Student>   
  8. {   
  9.    public bool Equals(Student x, Student y)  
  10.    {  
  11.       if(x != null && y != null)  
  12.       {  
  13.          if(x.StudentId == y.StudentId && x.Name == y.Name)  
  14.          {   
  15.             return true;  
  16.          }  
  17.       }  
  18.       return false;  
  19.    }  
  20.    public int GetHashCode(Student obj) => new { obj.StudentId, obj.Name }.GetHashCode();  
  21. }  
Code to test equality using this technique,
  1. var studentList = new List<Student>();  
  2. var girl = new Student() { Name = "Simran", StudentId = 4 };  
  3. var sameGirl = new Student() { Name = "Simran", StudentId = 4 };  
  4. studentList.Add(girl);  
  5. var stuList = new List<Student>();  
  6. stuList.Add(sameGirl);  
  7. var commonStudents = studentList.Intersect(stuList,new StudentComparer()).ToList();  
  8. foreach(var student in commonStudents)  
  9. {  
  10.    Console.WriteLine("Id: {0} , Name: {1}",student.StudentId,student.Name);  
  11. }  
  12. Console.WriteLine(studentList.Contains(sameGirl, new StudentComparer())); // True  
Note that an instance of StudentComparer is passed as a second argument in methods like Contains , Intersect etc for the internal equality check to use the Equals and GetHashCode method as implemented in StudentComparer class. 
 

Final Code

 
The final code is demonstrated in the form of a console application compressed in EqualityProject.zip.
 

Conclusion

 
So we have seen different ways to tackle the problem of object equality in various situations. We can now perform operations like Intersect, Except, Contains and many more and expect them to give the expected results by following the techniques explained above.