About Comparing Objects by Value - 6: Structure Equality Implementation

    In a previous publication, we examined the features of the device and the operation of structures of the .NET platform , which are “types by value” (Value Types) in the context of comparison by the value of objects - instances of structures .


    Now consider a ready-made example of the implementation of comparison by the value of objects - instances of structures .


    Will the example for structures help to more accurately determine, from an object (domain) point of view, the scope of applicability of comparing objects by value in general, and thereby simplify the comparison example by the value of objects - instances of classes that are reference types (Reference Types) , derived in one of the previous publications ?


    PersonStruct structure:


    struct PersonStruct
    using System;
    namespace HelloEquatable
    {
        public struct PersonStruct : IEquatable, IEquatable
        {
            private static int GetHashCodeHelper(int[] subCodes)
            {
                int result = subCodes[0];
                for (int i = 1; i < subCodes.Length; i++)
                    result = unchecked(result * 397) ^ subCodes[i];
                return result;
            }
            private static string NormalizeName(string name) => name?.Trim() ?? string.Empty;
            private static DateTime? NormalizeDate(DateTime? date) => date?.Date;
            public string FirstName { get; }
            public string LastName { get; }
            public DateTime? BirthDate { get; }
            public PersonStruct(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()
                }
            );
            public static bool Equals(PersonStruct first, PersonStruct second) =>
                first.BirthDate == second.BirthDate &&
                first.FirstName == second.FirstName &&
                first.LastName == second.LastName;
            public static bool operator ==(PersonStruct first, PersonStruct second) =>
                Equals(first, second);
            public static bool operator !=(PersonStruct first, PersonStruct second) =>
                !Equals(first, second);
            public bool Equals(PersonStruct other) =>
                Equals(this, other);
            public static bool Equals(PersonStruct? first, PersonStruct? second) =>
                first == second;
            // Alternate version:
            //public static bool Equals(PersonStruct? first, PersonStruct? second) =>
            //    first.HasValue == second.HasValue &&
            //    (
            //        !first.HasValue || Equals(first.Value, second.Value)
            //    );
            public bool Equals(PersonStruct? other) => this == other;
            // Alternate version:
            //public bool Equals(PersonStruct? other) =>
            //    other.HasValue && Equals(this, other.Value);
            public override bool Equals(object obj) =>
                (obj is PersonStruct) && Equals(this, (PersonStruct)obj);
            // Alternate version:
            //public override bool Equals(object obj) =>
            //    obj != null &&
            //    this.GetType() == obj.GetType() &&
            //    Equals(this, (PersonStruct)obj);
        }
    }

    An example with the implementation of comparing objects by value for structures is smaller in volume and simpler in structure due to the fact that instances of structures cannot accept null values ​​and the fact that it is impossible to inherit from user-defined structures (User defined structs by the value of objects - instances of classes, taking into account inheritance, are considered in the fourth publication of this cycle).


    Similar to the previous examples, fields for comparison are defined and the GetHashCode () method is implemented.


    Methods and comparison operators are implemented sequentially as follows:


    1. Implemented the static method PersonStruct.Equals (PersonStruct, PersonStruct) to compare two instances of structures.
      This method will be used as a reference comparison method when implementing other methods and operators.
      Also, this method can be used to compare instances of structures in languages ​​that do not support operators.


    2. The operators PersonStruct. == (PersonStruct, PersonStruct) and PersonStruct.! = (PersonStruct, PersonStruct) are implemented.
      It should be noted that the C # compiler has an interesting feature:


      • If the structure T has overloaded operators T. == (T, T) and T.! = (T, T), for Nullable (Of T) structures the possibility of comparison also appears using the operators T. == (T, T) and T.! = (T, T).
      • This is probably the “magic” of the compiler, which checks for the existence of a value for instances of the structure, before checking the equality of the values directly , and does not lead to the packing of instances of structures into objects .
      • What is characteristic, in this case, comparing an instance of a Nullable (Of T) structure with untyped null also leads to a call to the operator T. == (T, T) or T.! = (T, T), while a similar comparison of an instance of a structure Nullable (Of T) , which does not have overloaded operators T. == (T, T) and T.! = (T, T), calls the operator Object. == (Object, Object) or Object.! = (Object , Object) and, as a result, to packaging an instance of the structure of an object .

    3. The PersonStruct.Equals (PersonStruct) method (implementing IEquatable (Of PersonStruct)) is implemented by calling the PersonStruct.Equals (PersonStruct, PersonStruct) method.


    4. To prevent the packing of instances of structures into an object , if one or two Nullable (Of PersonStruct) instances are involved in the comparison, the following are implemented:

    • PersonStruct.Equals (PersonStruct ?, PersonStruct?) Method - to prevent instances of structures of both arguments from packing into objects and calling the Object.Equals (Object, Object) method if at least one of the arguments is an Nullable (Of PersonStruct) instance. This method can also be used when comparing Nullable (Of PersonStruct) instances in languages ​​that do not support operators. The method is implemented as a call to the PersonStruct. == (PersonStruct, PersonStruct) operator. Next to the method is a commented code showing how to implement this method if the C # compiler did not support the above “magic” of using the T. == (T, T) and T.! = (T, T) operators for Nullable (Of T) arguments.


    • PersonStruct.Equals method (PersonStruct?) (Implementation of interface IEquatable (Of PersonStruct)?) - to prevent packaging Nullable (Of PersonStruct) -argumenta in object and call a method PersonStruct.Equals (Object). The method is also implemented as a call to the PersonStruct. == operator (PersonStruct, PersonStruct), with commented out implementation code in the absence of compiler magic.


    • Finally, the PersonStruct.Equals (Object) method is implemented that overrides the Object.Equals (Object) method .
      The method is implemented by checking the compatibility of the argument type with the type of the current object using the is operator , followed by casting the argument to PersonStruct and calling PersonStruct.Equals (PersonStruct, PersonStruct).

    Note:


    • Interface Implementation IEquatable (Of PersonStruct?) - IEquatable (Of Nullable (Of PersonStruct)) are included to demonstrate certain problems in the platform when working with structures to the extent that the packaging of their items in the object occurs more often than one would like, and you can expect.
    • In real projects, only if you do not need to specifically optimize performance, implement IEquatable (Of Nullable (Of T)) should not be for architectural reasons - you should not implement a typed IEquatable in a type T for some other type.
    • And in general, you should not clutter up the code with various premature optimizations, even if optimization is not performed in the platform itself. In this publication, we additionally look at how often packaging is performed when working with structures.

    For structures, an exhaustive implementation of comparing instances by value turned out to be much simpler and more compact due to the lack of inheritance in User defined structs, and also due to the absence of the need for null checks .
    (However, compared to the implementation for classes, a new logic has appeared that supports Nullable (Of T) arguments).


    In the next publication, we will summarize the cycle on the topic "Object Equality", including consider:


    • in what cases, from the subject and technical points of view, it is really advisable to implement a comparison of the values ​​of objects by value;
    • how in these cases it is possible to simplify the implementation of comparison by value for objects - instances of classes that are reference types (Reference Types) , taking into account the experience of a simplified implementation for structures .

    Also popular now: