Tuesday, June 2, 2015

C# objects equality

Object Equality Schematics

Equals(Object) method override

  • override Equals(Object) method let you to check if two different object instances are the same object from their content point of view, using a.Equals(b).

Equality and inequality operators overload

  • when override Equals(Object) method is a good practice to override the equality == and inequality != operators to avoid mistake if never mind to use the Equals method.

IEquatable<T> interface implementation

  • when override Equals(Object) method is a good practice to implements the IEquatable<T> interface and call the Equals(T) method from the Equals(Object) to increment efficiency avoiding box-unboxing from Object to T type.

Collections HashSet and Dictionary

  • overriding of the Equals method in objects allow to achieve follow results for Equals objects :
    • HashSet avoid double insertion.
    • Dictionary find object key already present using Equals objects not inserted in the dictionary.

GetHashCode

  • when override Equals method for use of objects in collections :
    • must implement GetHashCode or as default it will declare two object different regardless they are equal using Equals method.
    • when two objects have the same hashcode then a second detailed check will ensure they equals using Equals method :
      • two different object can have the same hash code ( this of course decrease performance cause more detailed equals check will be done eg. key collisions ).
      • two equals object must have the same hash code or the equals method check will skipped and they are stated as different.

Sample unit test code

Without Equals(Object)

using System.Diagnostics;

namespace TestHashCode
{
    class Program
    {
        static void Main(string[] args)
        {
            var a = new Test() { value = 10 };
            var b = new Test() { value = 10 };

            Debug.Assert(!a.Equals(b));
        }
    }

    public class Test
    {
        public int value { get; set; }
    }

}

With Equals(Object) and without equality inequality operators

using System.Diagnostics;

namespace TestHashCode
{
    class Program
    {
        static void Main(string[] args)
        {
            var a = new Test() { value = 10 };
            var b = new Test() { value = 10 };

            Debug.Assert(!ReferenceEquals(a, b)); // different instances
            Debug.Assert(a != b); // operator not overloaded
            Debug.Assert(a.Equals(b)); // same equals object            
        }
    }

    public class Test
    {
        public int value { get; set; }
        
        public override bool Equals(object obj)
        {
            return value == ((Test)obj).value;
        }        
    }

}

With Equals(Object) and with equality inequality operators

using System.Diagnostics;

namespace TestHashCode
{
    class Program
    {
        static void Main(string[] args)
        {
            var a = new Test() { value = 10 };
            var b = new Test() { value = 10 };

            Debug.Assert(!ReferenceEquals(a, b)); // different instances
            Debug.Assert(a == b); // operator overloaded            
            Debug.Assert(a.Equals(b)); // same equals object            
        }
    }

    public class Test
    {
        public int value { get; set; }
        
        public override bool Equals(object obj)
        {
            return value == ((Test)obj).value;
        }

        public static bool operator ==(Test x, Test y)
        {
            return x.Equals(y);
        }

        public static bool operator !=(Test x, Test y)
        {
            return !x.Equals(y);
        }
    }

}

With Equals(T)

Test results (in Release mode) :
  • with Equals(Object) : ~5 sec
  • with Equals(T) : ~1 sec

using System;
using System.Diagnostics;

namespace TestHashCode
{
    class Program
    {
        static void Main(string[] args)
        {
            var a = new Test() { value = 10 };
            var b = new Test() { value = 10 };
            var c = 0;

            var sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < 1500000000; ++i)
            {
                if (a == b) ++c;
            }
            Console.WriteLine(sw.Elapsed);
        }
    }

    public class Test : IEquatable<Test>
    {
        public int value { get; set; }        
        
        public bool Equals(Test other)
        {
            return value == other.value;
        }
        
        public override bool Equals(object obj)
        {
            return Equals((Test)obj); // recall Equals(T)
        }              
        
        public static bool operator ==(Test x, Test y)
        {
            return x.Equals(y);
        }

        public static bool operator !=(Test x, Test y)
        {
            return !x.Equals(y);
        }
    }

}

Without GetHashCode

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace TestHashCode
{
    class Program
    {
        static void Main(string[] args)
        {
            var a = new Test() { value = 10 };
            var b = new Test() { value = 10 };

            var hs = new HashSet<Test>();
            hs.Add(a);
            // element b will added cause GetHashCode returns different values
            // for different objects as default
hs.Add(b); 
            
            Debug.Assert(hs.Count == 2);

            var dict = new Dictionary<Test, int>();
            dict.Add(a, a.value);
            Debug.Assert(!dict.ContainsKey(b));            
        }
    }

    public class Test : IEquatable<Test>
    {
        public int value { get; set; }        
        
        public bool Equals(Test other)
        {
            return value == other.value;
        }
        
        public override bool Equals(object obj)
        {
            return Equals((Test)obj); // recall Equals(T)
        }              
        
        public static bool operator ==(Test x, Test y)
        {
            return x.Equals(y);
        }

        public static bool operator !=(Test x, Test y)
        {
            return !x.Equals(y);
        }
    }

}

With GetHashCode

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace TestHashCode
{
    class Program
    {
        static void Main(string[] args)
        {
            var a = new Test() { value = 10 };
            var b = new Test() { value = 10 };

            var hs = new HashSet<Test>();
            hs.Add(a);
            // element b will not added despite its different instance
hs.Add(b); Debug.Assert(hs.Count == 1); var dict = new Dictionary<Test, int>(); dict.Add(a, a.value); Debug.Assert(dict.ContainsKey(b)); } } public class Test : IEquatable<Test> { public int value { get; set; } public bool Equals(Test other) { return value == other.value; } public override bool Equals(object obj) { return Equals((Test)obj); // recall Equals(T) } public static bool operator ==(Test x, Test y) { return x.Equals(y); } public static bool operator !=(Test x, Test y) { return !x.Equals(y); } public override int GetHashCode() { return value; } } }

Reference material


Creative Commons License
C# Objects Equality by Lorenzo Delana is licensed under a Creative Commons Attribution 4.0 International License.

No comments:

Post a Comment