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,
- var numList = new List<int>(){ 4,5 };
- var numbers = new List<int>() { 3,4 };
- var commonItems = numList.Intersect(numbers);
- foreach(var item in commonItems)
- {
- Console.WriteLine(item);
- }
-
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.
-
- var students = new List<student>();
- var girl = new Student() { Name = "Simran", StudentId = 4 };
- var sameGirl = new Student() { Name = "Simran", StudentId = 4 };
- students.Add(girl);
- Console.WriteLine("Default equality : {0}",students.Contains(sameGirl));
-
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,
- public class Employee
- {
- public int Id { get; set; }
- public string Name { get; set; }
-
-
- public override bool Equals(Object obj)
- {
- if (obj == null)
- return false;
- var emp = (Employee)obj;
- return emp.Id == Id && emp.Name == Name;
- }
-
- public override int GetHashCode() => new { Id, Name }.GetHashCode();
- }
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.
-
- var emp1 = new Employee() { Id = 1, Name = "John" };
- var emp2 = new Employee() { Id = 1, Name = "John" };
- Console.WriteLine(emp1.Equals(emp2));
-
- var empList = new List<Employee>();
- empList.Add(emp1);
- Console.WriteLine(empList.Contains(emp2));
-
- var employees = new HashSet<Employee>();
- employees.Add(emp2);
- Console.WriteLine(employees.Contains(emp1));
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.
- public class Person : IEquatable<Person>
- {
- public int Age { get; set; }
- public string Name { get; set; }
-
-
- public bool Equals(Person otherPerson)
- {
- if (otherPerson!= null && otherPerson.Age == Age && otherPerson.Name == Name)
- {
- return true;
- }
- return false;
- }
-
- public override int GetHashCode() => new { Age, Name }.GetHashCode();
- }
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,
- var person1 = new Person() { Age = 21, Name = "Alice" };
- var person2 = new Person() { Age = 21, Name = "Alice" };
- Console.WriteLine(person1.Equals(person2));
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.
- public class Student
- {
- public int StudentId { get; set; }
- public string Name { get; set; }
- }
-
-
- {
- public bool Equals(Student x, Student y)
- {
- if(x != null && y != null)
- {
- if(x.StudentId == y.StudentId && x.Name == y.Name)
- {
- return true;
- }
- }
- return false;
- }
- public int GetHashCode(Student obj) => new { obj.StudentId, obj.Name }.GetHashCode();
- }
Code to test equality using this technique,
- var studentList = new List<Student>();
- var girl = new Student() { Name = "Simran", StudentId = 4 };
- var sameGirl = new Student() { Name = "Simran", StudentId = 4 };
- studentList.Add(girl);
- var stuList = new List<Student>();
- stuList.Add(sameGirl);
- var commonStudents = studentList.Intersect(stuList,new StudentComparer()).ToList();
- foreach(var student in commonStudents)
- {
- Console.WriteLine("Id: {0} , Name: {1}",student.StudentId,student.Name);
- }
- Console.WriteLine(studentList.Contains(sameGirl, new StudentComparer()));
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.
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.