Let's say we have a person class that is functioning as an entity object as in the following:
- public class Person
- {
- private readonly String _Identifier;
- public Person(String identifier)
- {
- _Identifier = identifier;
- }
- public String Identifier
- {
- get
- {
- return _Identifier;
- }
- }
- }
- Person firstPerson = new Person("123-45-6789");
- // a bunch of logic here
- Person secondPerson = new Person("123-45-6789");
- if (firstPerson.Equals(secondPerson)) // will evaluate to 'false'
- {
- // do something important here
- }
This is a common modeling problem. To address it, we will first implement the IEquatable interface.
- public class Person: IEquatable
- {
- private readonly String _Identifier;
- public Person(String identifier)
- {
- _Identifier = identifier;
- }
- public String Identifier
- {
- get
- {
- return _Identifier;
- }
- }
- public bool Equals(Person other)
- {
- throw new NotImplementedException();
- }
- }
- public class Person: IEquatable
- {
- private readonly String _Identifier;
- public Person(String identifier)
- {
- _Identifier = identifier;
- }
- public String Identifier
- {
- get
- {
- return _Identifier;
- }
- }
- public bool Equals(Person other)
- {
- throw new NotImplementedException();
- }
- public override bool Equals(object obj)
- {
- return base.Equals(obj);
- }
- public override int GetHashCode()
- {
- return base.GetHashCode();
- }
- }
- Person firstPerson = new Person("123-45-6789");
- // a bunch of logic here
- Person secondPerson = new Person("123-45-6789");
- if (firstPerson == secondPerson)
- {
- // do something
- }
Since assumptions will eventually be made either way, it is far less risky to err on the side of consistent behavior. If we override the "Equals()" method and do not implement the equality operator, false assumptions will lead to defects in the code base. If we override the "Equals()" method and also implement the equality operator, the impact of the false assumption that the equality operator is reference checking is much smaller. Besides, if we want to check for reference equality, we should explicitly use the "ReferenceEquals()" method.
- var test1 = firstPerson == secondPerson;
- var test2 = firstPerson.Equals(secondPerson);
- Here is the surface of what we will have to implement to implement the equality operator.
- public class Person: IEquatable
- {
- private readonly String _Identifier;
- public Person(String identifier)
- {
- _Identifier = identifier;
- }
- public String Identifier
- {
- get
- {
- return _Identifier;
- }
- }
- public bool Equals(Person other)
- {
- throw new NotImplementedException();
- }
- public override bool Equals(object obj)
- {
- return base.Equals(obj);
- }
- public override int GetHashCode()
- {
- return base.GetHashCode();
- }
- public static Boolean operator == (Person first, Person second)
- {
- throw new NotImplementedException();
- }
- public static Boolean operator != (Person first, Person second)
- {
- throw new NotImplementedException();
- }
- }
The first problem we run into when implementing the equality operator "==" is that it is easy to inadvertently add infinite recursion.
- public static Boolean operator == (Person first, Person second) {
- if (first == null && second == null) // infinite recursive loop here
- {
- return true;
- }
- }
- public static Boolean operator == (Person first, Person second)
- {
- if (ReferenceEquals(first, second))
- {
- return true;
- }
- if (ReferenceEquals(first, null))
- {
- return false;
- }
- return first.Equals(second);
- }
The infinite recursion problem can span multiple methods in this more subtle. If we have implemented the equality operator "==" as above, but then use the equality operator to check for referential equality in the "Equals()" method, we will end up with another infinite recursion.
- public bool Equals(Person other)
- {
- if (other == null)
- {
- return false;
- }
- // evaluate
- }
- public bool Equals(Person other)
- {
- if (ReferenceEquals(other, null))
- {
- return false;
- }
- // check identifiers
- var result = _Identifier.Equals(other._Identifier);
- return result;
- }
If you value your weekends like I do and do not like being called up at 3am on Saturday to put out fires in production, then we are safer betting on using "ReferenceEquals()" rather than the equality operator "==" to check referential equality.
Trap #3
Another thing we have to be careful of is making sure the identifier is not null. This can be accomplished through guard clauses in the constructor.
- public Person(String identifier)
- {
- if (ReferenceEquals(identifier, null))
- {
- throw new ArgumentNullException(identifier);
- }
- _Identifier = identifier;
- }
- if(String.IsNullOrEmpty(identifier)){}
- if(String.IsNullOrWhiteSpace(identifier)){}
Since we are overriding the "Equals()" method, we should also override the "GetHashcode()" method. If we don't have a good understanding of the implications of overriding "GetHashcode()" it is easy to introduce a bunch of subtle defects into the system. "GetHashcode()" is used to get an identifier that is used to keep track of your instance. By default, "GetHashcode()" is based off the instance. Here are some of the defects that are easy to introduce.
Let's say we have a "User" class where we have implemented equality:
- c class User: IEquatable
- {
- private Int32 _ID;
- public User(Int32 id)
- {
- _ID = id;
- }
- public override bool Equals(object obj)
- {
- return Equals(obj as User);
- }
- public bool Equals(User other)
- {
- return _ID == other._ID;
- }
- }
- User a = new User(1);
- User b = new User(2);
- var friends = new Dictionary<User, User>() { {a , b} };
- User c = new User(1);
- var areEqual = a.Equals(c); // true
- var containsA = friends.ContainsKey(a); // true
- var containsC = friends.ContainsKey(c); // false
- public class User: IEquatable
- {
- private Int32 _ID;
- public User(Int32 id)
- {
- _ID = id;
- }
- public override bool Equals(object obj)
- {
- return Equals(obj as User);
- }
- public bool Equals(User other)
- {
- return _ID == other._ID;
- }
- public override int GetHashCode()
- {
- return _ID.GetHashCode();
- }
- }
- User a = new User(1);
- User b = new User(2);
- var friends = new Dictionary<User, User>() { {a , b} };
- User c = new User(1);
- var areEqual = a.Equals(c); // true
- var containsA = friends.ContainsKey(a); // true
- var containsC = friends.ContainsKey(c); // true
A problem that can arrive when implementing "GetHashcode()" is when the identifier changes.
- public class User: IEquatable
- {
- private Int32 _ID;
- public User(Int32 id)
- {
- _ID = id;
- }
- public override bool Equals(object obj)
- {
- return Equals(obj as User);
- }
- public bool Equals(User other)
- {
- return _ID == other._ID;
- }
- public override int GetHashCode()
- {
- return _ID.GetHashCode();
- }
- public Int32 ID
- {
- get
- {
- return _ID;
- }
- set
- {
- _ID = value;
- }
- }
- }
- User a = new User(1);
- User b = new User(2);
- var friends = new Dictionary<User, User>() { {a , b} };
- var contains1 = friends.ContainsKey(a); // true
- a.ID = 3;
- var contains2 = friends.ContainsKey(a); // false
- public class User: IEquatable
- {
- private readonly Int32 _ID; // immutable
- public User(Int32 id)
- {
- _ID = id;
- }
- public override bool Equals(object obj)
- {
- return Equals(obj as User);
- }
- public bool Equals(User other)
- {
- return _ID == other._ID;
- }
- public override int GetHashCode()
- {
- return _ID.GetHashCode();
- }
- public Int32 ID
- {
- get {
- return _ID;
- }
- } // immutable
- }
To finish our implementation, we just need to implement our overridden "Equals" method by casting the type to a "Person" class and then call the "IEquality.Equals()" method. This way, if we are passed a type that is anything other than a "Person" instance, the soft cast ("as") will return null.
- public override bool Equals(object obj)
- {
- return Equals(obj as Person);
- }
- Finally, we implement the inequality‘ != ’operator which is required when we implement the equality‘ == ’operator
- public static Boolean operator != (Person first, Person second)
- {
- return !(first == second);
- }
[Original article]
Until next time
Happy Coding
Santhakumar MunuswamyPosted May 4, 2015, 2:39 PM
Thanks for nice article
Gowtham RajamanickamPosted Apr 7, 2015, 7:33 AM
great