📜 ⬆️ ⬇️

Comparing objects by value - 4, or Inheritance & Equality operators

In the previous publication, we obtained an implementation variant of comparing objects by value for the .NET platform, using the example of the Person class, which includes:



Each of the comparison methods for any one and the same pair of objects returns the same result:


Code example
Person p1 = new Person("John", "Smith", new DateTime(1990, 1, 1)); Person p2 = new Person("John", "Smith", new DateTime(1990, 1, 1)); //Person p2 = new Person("Robert", "Smith", new DateTime(1991, 1, 1)); object o1 = p1; object o2 = p2; bool isSamePerson; isSamePerson = o1.Equals(o2); isSamePerson = p1.Equals(p2); isSamePerson = object.Equals(o1, o2); isSamePerson = Person.Equals(p1, p2); isSamePerson = p1 == p2; isSamePerson = !(p1 == p2); 

At the same time, each of the comparison methods is commutative:
x.Equals (y) returns the same result as y.Equals (x), etc.


Thus, the client code can compare objects in any way - the result of the comparison will be determined.


However, the issue requires disclosure:


How exactly is the determinism of the result ensured when implementing static methods and comparison operators in the case of inheritance - taking into account the fact that static methods and operators do not have polymorphic behavior.


For clarity, we present the Person class from the previous publication :


class Person
 using System; namespace HelloEquatable { public class Person : IEquatable<Person> { protected static int GetHashCodeHelper(int[] subCodes) { if ((object)subCodes == null || subCodes.Length == 0) return 0; int result = subCodes[0]; for (int i = 1; i < subCodes.Length; i++) result = unchecked(result * 397) ^ subCodes[i]; return result; } protected static string NormalizeName(string name) => name?.Trim() ?? string.Empty; protected static DateTime? NormalizeDate(DateTime? date) => date?.Date; public string FirstName { get; } public string LastName { get; } public DateTime? BirthDate { get; } public Person(string firstName, string lastName, DateTime? birthDate) { this.FirstName = NormalizeName(firstName); this.LastName = NormalizeName(lastName); this.BirthDate = NormalizeDate(birthDate); } public override int GetHashCode() => GetHashCodeHelper( new int[] { this.FirstName.GetHashCode(), this.LastName.GetHashCode(), this.BirthDate.GetHashCode() } ); protected static bool EqualsHelper(Person first, Person second) => first.BirthDate == second.BirthDate && first.FirstName == second.FirstName && first.LastName == second.LastName; public virtual bool Equals(Person other) { //if ((object)this == null) // throw new InvalidOperationException("This is null."); if ((object)this == (object)other) return true; if ((object)other == null) return false; if (this.GetType() != other.GetType()) return false; return EqualsHelper(this, other); } public override bool Equals(object obj) => this.Equals(obj as Person); public static bool Equals(Person first, Person second) => first?.Equals(second) ?? (object)first == (object)second; public static bool operator ==(Person first, Person second) => Equals(first, second); public static bool operator !=(Person first, Person second) => !Equals(first, second); } } 

And create a class-successor PersonEx:


class PersonEx
 using System; namespace HelloEquatable { public class PersonEx : Person, IEquatable<PersonEx> { public string MiddleName { get; } public PersonEx( string firstName, string middleName, string lastName, DateTime? birthDate ) : base(firstName, lastName, birthDate) { this.MiddleName = NormalizeName(middleName); } public override int GetHashCode() => GetHashCodeHelper( new int[] { base.GetHashCode(), this.MiddleName.GetHashCode() } ); protected static bool EqualsHelper(PersonEx first, PersonEx second) => EqualsHelper((Person)first, (Person)second) && first.MiddleName == second.MiddleName; public virtual bool Equals(PersonEx other) { //if ((object)this == null) // throw new InvalidOperationException("This is null."); if ((object)this == (object)other) return true; if ((object)other == null) return false; if (this.GetType() != other.GetType()) return false; return EqualsHelper(this, other); } public override bool Equals(Person other) => this.Equals(other as PersonEx); // Optional overloadings: public override bool Equals(object obj) => this.Equals(obj as PersonEx); public static bool Equals(PersonEx first, PersonEx second) => first?.Equals(second) ?? (object)first == (object)second; public static bool operator ==(PersonEx first, PersonEx second) => Equals(first, second); public static bool operator !=(PersonEx first, PersonEx second) => !Equals(first, second); } } 

Another key property MiddleName appeared in the heir class. Therefore, the first step is to:



(Otherwise, a comparison of objects that have all key fields equal, except MiddleName, will return the result of "objects are equal," which is not correct from an objective point of view.)


Wherein:



Next, the PersonEx.Equals (Object) method is implemented, which overlaps the inherited Equals (Object) method and represents a call to the PersonEx.Equals method (PersonEx), casting the incoming object to the PersonEx type using the as operator.


It is worth noting that the implementation of PersonEx.Equals (Object) is not mandatory, since in case of its absence and calling the client code of the Equals (Object) method, the inherited method Person.Equals (Object) would be called, which internally calls the virtual method PersonEx.Equals (Person), resulting in a call to PersonEx.Equals (PersonEx).
However, the PersonEx.Equals (Object) method is implemented for code "completeness" and greater speed (by minimizing the number of type conversions and intermediate method calls).


In other words, when creating the PersonEx class and inheriting the Person class, we acted in the same way as when creating the Person class and inheriting the Object class.


Now, no matter what method we called to the object of the class PersonEx:
Equals (PersonEx), Equals (Person), Equals (object),
for any one and the same pair of objects, the same result will be returned (when changing operands in some places, the same result will also be returned).
Polymorphism allows for this behavior.


We also implemented the PersonEx static method PersonEx.Equals (PersonEx, PersonEx) and the corresponding comparison operators PersonEx. == (PersonEx, PersonEx) and PersonEx.! = (PersonEx, PersonEx) in the PersonEx class, also acting in the same way as when creating the class Person.


Using the PersonEx.Equals method (PersonEx, PersonEx) or the PersonEx. == (PersonEx, PersonEx) and PersonEx.! = (PersonEx, PersonEx) operators for any one and the same pair of objects will give the same result as using the Equals instance methods. class PersonEx.


But then it becomes more interesting.


The PersonEx class "inherited" from the Person class the static Equals method (Person, Person) and the corresponding comparison operators == (Person, Person) and! = (Person, Person).


What result will be received if to execute the following code?


Code
 bool isSamePerson; PersonEx pex1 = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1)); PersonEx pex2 = new PersonEx("John", "Bobby", "Smith", new DateTime(1990, 1, 1)); //PersonEx pex2 = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1)); Person p1 = pex1; Person p2 = pex2; isSamePerson = Person.Equals(pex1, pex2); isSamePerson = PersonEx.Equals(p1, p2); isSamePerson = pex1 == pex2; isSamePerson = p1 == p2; 

Although the Equals method (Person, Person) and the == (Person, Person) and! = (Person, Person) comparison operators are static, the result will always be the same as when you called the Equals method (PersonEx, PersonEx ), operators == (PersonEx, PersonEx) and! = (PersonEx, PersonEx), or any of the instance Equals virtual methods.


To obtain this polymorphic behavior, the Equals static methods and the comparison operators "==" and "! =" Are implemented at each of the inheritance stages using the instance virtual Equals method.


Moreover, the implementation of the Equals method (PersonEx, PersonEx) and == (PersonEx, PersonEx) and! = (PersonEx, PersonEx) operators in the PersonEx class, as well as the PersonEx.Equals (Object) method, is optional.
The Equals method (PersonEx, PersonEx) and operators == (PersonEx, PersonEx) and! = (PersonEx, PersonEx) are implemented for code “completeness” and faster performance (by minimizing the number of type conversions and intermediate method calls).


The only obscure point in the "polymorphism" of static Equals, "==" and "! =" Is that if two objects of type Person or PersonEx are reduced to type object , then the comparison of objects with the help of == and ! = Operators will be done by reference , and using the Object.Equals (Object, Object) method - by value. But this is a “by design” platform.


In the continuation, we will consider the features of the implementation of comparison by value for objects - instances of structures , and also talk about cases when it is really advisable to implement for their types comparison of objects by value, and how to do it, incl. from the subject point of view.


')

Source: https://habr.com/ru/post/315258/


All Articles