📜 ⬆️ ⬇️

Comparing objects by value - 3, or Type-specific Equals & Equality operators

Earlier, we considered the correct implementation of the minimum necessary set of class modifications for comparing class objects by value.


Now consider the Type-specific implementation of comparing objects by value, including the implementation of the IEquatable Generic Interface (Of T) and the overloading of the "==" and "! =" Operators.


A type-specific comparison of objects allows to achieve:



In addition, implementation of Type-specific comparison by value is necessary for the following reasons:



The implementation of all methods of comparison at the same time presents certain difficulties, since for correct work it is required to provide:



Consider the Type-specific implementation of comparing objects by value, taking into account the above conditions, using the example of the Person class.


We will immediately provide the final version of the code with explanations why this was done exactly and how it works.
(Demonstrating the output of a solution with every nuance contains too many iterations.)


The Person class with the implementation of the full set of ways to compare objects by value:


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); } } 

  1. The Person.GetHashCode () method calculates the object's hash code based on the fields, the combination of which forms the unique value of a particular object.
    Features of hash calculation and requirements for overlapping of the Object.GetHashCode () method are given in the documentation , as well as in the first publication .


  2. Static protected helper method EqualsHelper (Person, Person) compares two objects by fields, the combination of the values ​​of which forms the uniqueness of the value of a specific object.


  3. The virtual method Person.Equals (Person) implements the IEquatable (Of Person) interface.
    (The method is declared virtual, since its overlap will be needed during inheritance - it will be discussed below.)


    • At the "zero" step, the code is commented out, checking for a null reference to the current object.
      If the link is null , an InvalidOperationException is thrown , indicating that the object is in an invalid state. Why it may be necessary - just below.
    • In the first step, equality is checked against the reference of the current and incoming objects. If yes, then the objects are equal (they are the same object).
    • In the second step, the reference to the incoming object is checked for null . If so, then the objects are not equal (they are different objects).
      (Equality by reference is checked using the == and ! = Operators, with the preliminary casting of operands to the object to call the non-overloaded operator, or using the Object.ReferenceEquals (Object, Object) method. If the == and ! = Operators are used, then in the case of casting operands to object, it is necessary because in this class these operators will be overloaded and will use the Person.Equals (Person) method themselves.)
    • Next, the identity of the types of the current and incoming objects is checked. If the types are not identical, then the objects are not equal.
      (Verification of the identity of object types, instead of checking compatibility, is used to account for the implementation of comparison by value when inheriting a type. More on this in a previous publication .)
    • Then, if the previous checks did not allow us to give a quick answer, whether the objects are equal or not, then the current and incoming objects are checked directly by value using the EqualsHelper (Person, Person) helper method.

  4. The Person.Equals (Object) method is implemented as a call to the Person.Equals (Person) method, casting the input object to the Person type using the as operator.


    • Note. If the object types are not compatible, the result of the cast will be null , which will result in the result of comparing objects in the Person.Equals (Person) method in the second step (the objects are not equal).
      However, in the general case, the result of the comparison in the Person.Equals (Person) method can be obtained in the first step (the objects are equal), since theoretically, in .NET it is possible to call an instance method without creating an instance (more on this in the first publication ).
      And then, if the reference to the current object is equal to null , the reference to the incoming object is not null , and the types of the current and incoming objects are incompatible, then such a call to Person.Equals (Object) followed by a call to Person.Equals (Person) will give incorrect the result in the first step is “objects are equal”, while in reality objects are not equal.
      It seems that such a rare case does not require special treatment, since calling an instance method and using its result does not make sense without creating the instance itself.
      If you need to take it into account, it is enough to uncomment the "zero step" code in the Person.Equals (Person) method, which not only prevents you from getting the theoretically possible incorrect result when calling the Person.Equals (Object) method, but also by directly calling the Person method .Quals (person) on a null object will generate a more informative exception at the "zero" step, instead of a NullReferenceException in the third step.

  5. To support static comparison of objects by value for CLS- compatible languages ​​that do not support operators or their overload, the static method Person.Equals (Person, Person) is implemented.
    (As a Type-specific, and faster, alternative to the Object.Equals (Object, Object) method.)
    (For the need to implement methods that match operators, and recommendations for matching operators and method names, see Jeffrey Richter’s CLR Via C # (Part II "Designing Types", Chapter 8 "Methods", Subchapter "Operator Overload Methods ").)


    • The method Person.Equals (Person, Person) is implemented by calling the instance instance method Person.Equals (Person), since this is necessary to ensure that "calling x == y yielded the same result as calling" y == x ", which meets the requirement" calling x.Equals (y) should give the same result as calling y . Equals (x) "(for more on the last requirement, including its provision for inheritance, see the previous publication ).
    • Since static methods for type inheritance cannot be overridden (it is about overlapping - override, not overriding - new), i.e. do not have polymorphic behavior, the reason for just such an implementation is a call to the static method Person.Equals (Person, Person) via a call to a virtual instance Person.Equals (Person) - precisely in the need to provide polymorphism for static calls, and thus ensure the results "static" and "instance" comparison with inheritance.
    • In the Person.Equals (Person, Person) method, invoking the instance method of Person.Equals (Person) is implemented by checking for null references to the object that calls the Equals (Person) method.
      If this object is null , then objects are compared by reference.

  6. The overloaded operators Person. == (Person, Person) and Person.! = (Person, Person) are implemented by calling the "as is" static method Person.Equals (Person, Person) (for the "! =" Operator paired with by the operator ! ).

So, we have found the correct and rather compact way of implementing in one class all the methods of comparing class objects by value, and even took into account the correctness of behavior in case of inheritance, laid down in the code the possibilities that we can use when inheriting.


At the same time, it is necessary to separately consider how for a given implementation variant of comparing objects by value correctly perform inheritance, if a field is included in the class of an inheritor that is included in the set of fields of the object that form the unique value of the object:


Let there is a class PersonEx, inheriting the class Person, and having an additional property MiddleName. In this case, the comparison of two objects of the class PersonEx:


 John Teddy Smith 1990-01-01 John Bobby Smith 1990-01-01 

In any realized way will give the result "objects are equal", which is wrong from the substantive point of view.


Thus, given the seemingly triviality of the task, in addition to the relatively high costs and risks, the realization of the comparison of objects by value in the current .NET infrastructure is fraught with the fact that once the class implements the comparison of objects by value, the comparison implementation will have to be dragged ( and do it in the right way) in the heir classes, which carries additional costs and the potential for error.


How to make the solution of this problem, as far as possible, easy and compact, let's talk in continuation .


')

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


All Articles