📜 ⬆️ ⬇️

Comparing objects by value - 2, or Features of the implementation of the Equals method

In a previous publication, we looked at the general principles for implementing the minimum necessary class modifications to enable the comparison of class objects by value using the standard .NET framework infrastructure.

These improvements include the overlapping methods Object.Equals (Object) and Object.GetHashCode () .

Let us dwell in more detail on the implementation features of the Object.Equals (Object) method to meet the following requirement in the documentation:
')
x.Equals(y) returns the same value as y.Equals(x). 

The Person class, created in a previous publication , contains the following implementation of the Equals (Object) method:

Person.Equals (Object)
 public override bool Equals(object obj) { if ((object)this == obj) return true; var other = obj as Person; if ((object)other == null) return false; return EqualsHelper(this, other); } 

After checking the referential equality of the current and incoming objects, in case of a negative result of the check, the incoming object is coerced to type Person in order to be able to compare objects by value.

In accordance with the example given in the documentation , the cast is performed using the as operator. Check whether it gives the correct result.

We implement the PersonEx class, inheriting the Person class, adding the Middle Name property to personal data, and overlapping the methods of Person.Equals (Object) and Person.GetHashCode () accordingly.

PersonEx class:

class PersonEx
 using System; namespace HelloEquatable { public class PersonEx : Person { 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() => base.GetHashCode() ^ this.MiddleName.GetHashCode(); protected static bool EqualsHelper(PersonEx first, PersonEx second) => EqualsHelper((Person)first, (Person)second) && first.MiddleName == second.MiddleName; public override bool Equals(object obj) { if ((object)this == obj) return true; var other = obj as PersonEx; if ((object)other == null) return false; return EqualsHelper(this, other); } } } 

It is easy to notice that if the Equals (Object) method is called on the Person object and the PersonEx class object is passed to it, if these objects (persons) have the first name, last name and date of birth, the Equals method will return true , otherwise the method will return false .

(When executing the Equals method, an incoming object that has PersonEx type at runtime will be successfully cast to Person using the as operator, and then objects will be compared by field values ​​that are only in the Person class, and the corresponding result.)

Obviously, from the substantive point of view, this is the wrong behavior:

The coincidence of the name, surname and date of birth does not mean that it is the same person, because one person has no middle name attribute (it’s not about the undefined attribute value, but about the absence of the attribute itself), and the other has the middle name attribute.
(These are different types of entities.)

If, on the contrary, the object of the class PersonEx calls the Equals (Object) method and passes the object of the class Person to it, the Equals method will return false in any case, regardless of the values ​​of the object properties.

(When executing the Equals method, an incoming object having a type of Person at runtime (runtime) will not be successfully cast to the PersonEx type using the as operator — the cast will be null and the method will return false .)

Here we observe the correct behavior from the substantive point of view, in contrast to the previous case.

These behaviors can easily be verified by running the following code:

Code
 var person = new Person("John", "Smith", new DateTime(1990, 1, 1)); var personEx = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1)); bool isSamePerson = person.Equals(personEx); bool isSamePerson2 = personEx.Equals(person); 

However, in the context of this publication, we are more interested in the compliance of the implemented Equals (Object) behavior with the requirements in the documentation , rather than the correctness of logic from the substantive point of view.

Namely, compliance with the requirement:

 x.Equals(y) returns the same value as y.Equals(x). 

This requirement is not met.

(And from the point of view of common sense, what problems can there be with the current implementation of Equals (Object)?
The data type developer has no information on how exactly the objects will be compared - x.Equals (y) or y.Equals (x) - both in the client code (when explicitly calling Equals), and when placing objects in hash sets (hash cards) and dictionaries (inside the sets / dictionaries themselves).

In this case, the behavior of the program will be non-deterministic, and depend on the implementation details.)

Consider exactly how you can implement the Equals (Object) method, providing the expected behavior.


Currently the correct method proposed by Jeffrey Richter in the book CLR via C # (Part II "Designing Types", Chapter 5 "Primitive, Reference, and Value Types", Subchapter "Object Equality and Identity"), when comparing objects directly by value, the types of objects at runtime (runtime) obtained using the Object.GetType () method are checked for equality (instead of one-way compatibility checks and coercion of object types using the as operator):

 if (this.GetType() != obj.GetType()) return false; 

It should be noted that the use of this method is not unambiguous, since There are three different ways to check for equality of instances of the Type class, with theoretically different results for the same operands:

1. According to the documentation for the Object.GetType () method:

 For two objects x and y that have identical runtime types, Object.ReferenceEquals(x.GetType(),y.GetType()) returns true. 

Thus, objects of the Type class can be checked for equality by comparing by reference:

 bool isSameType = (object)obj1.GetType() == (object)obj2.GetType(); 
or
 bool isSameType = Object.ReferenceEquals(obj1.GetType(), obj2.GetType()); 

2. The Type class has Equals (Object) and Equals (Type) methods, whose behavior is defined as follows:
Determines the type of the specified object.

Return value
Type: System.Boolean
it is the current system; otherwise, false. This method also returns false if:
o is null.
o cannot be cast or converted to a Type object.

Remarks
This method overrides Object.Equals. It casts the type.Equals (Type) method.
and
Determines the system.

Return value
Type: System.Boolean
it is the current system; otherwise, false.

Inside these methods are implemented as follows:

 public override bool Equals(Object o) { if (o == null) return false; return Equals(o as Type); } 

and

 public virtual bool Equals(Type o) { if ((object)o == null) return false; return (Object.ReferenceEquals(this.UnderlyingSystemType, o.UnderlyingSystemType)); } 

As you can see, the result of executing both Equals methods for objects of the Type class in the general case may differ from comparing objects by reference, since in the case of using the Equals methods, not the objects of the Type class are compared by reference, but their UnderlyingSystemType properties belonging to the same class.

However, from the description of the Equals methods of the Type.Equals (Object) class it appears that they are not intended to compare directly the objects of the Type class.

Note:
For the Type.Equals (Object) method, the problem of non-compliance with the requirement (as a result of using the as operator)
 x.Equals(y) returns the same value as y.Equals(x). 
will not occur, unless in descendants of the Type class the method is incorrectly overridden.
To prevent this potential problem, it might be advisable for developers to declare the method as sealed .

3. The Type class, starting with the .NET Framework 4.0, has overloaded operators == or ! = , Whose behavior is described in a simple way, without describing implementation details:
Indicates whether two Type objects are equal.

Return value
Type: System.Boolean
true if left is equal to right; otherwise, false.
and
Indicates whether two Type objects are not equal.

Return value
Type: System.Boolean
true if left is not equal to right; otherwise, false.
The study of source codes also does not provide information on the implementation details for finding out the internal logic of the operators:

 public static extern bool operator ==(Type left, Type right); 
 public static extern bool operator !=(Type left, Type right); 

Based on the analysis of three documented ways of comparing objects of the Type class, it seems that the most correct way to compare objects is to use the operators "==" and "! =".
Depending on the target platform (Target Platform) of the project, the source code will be assembled either using comparison by reference (identical to the first option) or using overloaded operators == and ! = .

We implement the classes Person and PersonEx accordingly:

class Person (with new Equals method)
 using System; namespace HelloEquatable { public class Person { 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() => 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 override bool Equals(object obj) { if ((object)this == obj) return true; if (obj == null) return false; if (this.GetType() != obj.GetType()) return false; return EqualsHelper(this, (Person)obj); } } } 

class PersonEx (with new Equals method)
 using System; namespace HelloEquatable { public class PersonEx : Person { 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() => base.GetHashCode() ^ this.MiddleName.GetHashCode(); protected static bool EqualsHelper(PersonEx first, PersonEx second) => EqualsHelper((Person)first, (Person)second) && first.MiddleName == second.MiddleName; public override bool Equals(object obj) { if ((object)this == obj) return true; if (obj == null) return false; if (this.GetType() != obj.GetType()) return false; return EqualsHelper(this, (PersonEx)obj); } } } 

Now the following requirement for the implementation of the Equals (Object) method will be met:

 x.Equals(y) returns the same value as y.Equals(x). 

which is easily verified by code execution:

Code
 var person = new Person("John", "Smith", new DateTime(1990, 1, 1)); var personEx = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1)); bool isSamePerson = person.Equals(personEx); bool isSamePerson2 = personEx.Equals(person); 

Notes on the implementation of the Equals (Object) method:

  1. first, links that indicate the current and incoming objects are checked for equality, and, if the links match, true is returned;
  2. then the reference to the incoming object is checked for null , and, in the case of a positive check result, false is returned;
  3. then the identity of the types of the current and incoming objects is checked, and, in the case of a negative check result, false is returned;
  4. at the last stage, the input object is cast to the type of this class and the objects are directly compared by value.

Thus, we have found the optimal way to implement the expected behavior of the Equals (Object) method.


For dessert, check the correct implementation of Equals (Object) in the standard library.

Method Uri.Equals (Object) :

Compares two Uri instances for equality.

Syntax
public override bool Equals (object comparand)

Parameters
comparand
Type: System.Object
The Uri Instance or a URI identifier to compare with the current instance.

Return value
Type: System.Boolean
A boolean value that is true if the two instances are the same URI; otherwise, false.

Uri.Equals (Object)
 public override bool Equals(object comparand) { if ((object)comparand == null) { return false; } if ((object)this == (object)comparand) { return true; } Uri obj = comparand as Uri; // // we allow comparisons of Uri and String objects only. If a string // is passed, convert to Uri. This is inefficient, but allows us to // canonicalize the comparand, making comparison possible // if ((object)obj == null) { string s = comparand as string; if ((object)s == null) return false; if (!TryCreate(s, UriKind.RelativeOrAbsolute, out obj)) return false; } // method code ... } 

It is logical to assume that the following requirement for the implementation of the Equals (Object) method is not met:

 x.Equals(y) returns the same value as y.Equals(x). 

Since the String class and the String.Equals (Object) method, in turn, do not “know” about the existence of the Uri class.

It is easy to check in practice by running the code:

Code
 const string uriString = "https://www.habrahabr.ru"; Uri uri = new Uri(uriString); bool isSameUri = uri.Equals(uriString); bool isSameUri2 = uriString.Equals(uri); 

In the sequel, we will look at the implementation of the IEquatable (Of T) interface and the IEquatable (Of T). Equals (T) type-specific method, overloading equality and inequality operators to compare objects by value, and find the most compact, consistent and productive way to implement in one class all kinds of checks by value.

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


All Articles