March 6, 2012

Implementing ==, != Operators In Reference Types

For more on equality operators see the Equals Operator Pattern

Microsoft guideline: Guidelines for Overloading Equals() and Operator == (C# Programming Guide)

Find out about The ReferenceEquals() method - Standard class operator == does reference comparison, so it will end up returning false if the two arguments point to different references where both refences hold the same state. To define == operator for a reference type you must be careful to define it in terms of "RefenceEqual()" and "Equals()". If you use == within the definition you will likely get an infinite recursion. This blogs explains why. Value types do not have the same problem as "==" does not do a reference comparison, it compares the value types field by field for equality.

Following example shows how a reference object behaves when the '==' and '!=' operators are overriden and when they are not. The class 'With' has the operators overriden and the class 'Sans' has not.
class TestRefernceTypeEqualityOperator
{
  internal class With
  {
    public string Name { get; set; }
    public int UniqueId { get; set; }

    public override bool Equals(object obj)
    {
      if (obj == null)
        return false;

      if (object.ReferenceEquals(this, obj))
        return true;

      if (!(obj is With))
        return false;
      With selection2 = (With)obj;

      if ((this.UniqueId == selection2.UniqueId) &&
        (this.Name == selection2.Name))
        return true;

      return base.Equals(obj);
    }


    public static bool operator ==(With obj1, With obj2)
    {
      // IF obj1 1 is null
      //   so obj2 must be for equality
      // ELSE obj1 is not null,  
      //   compare it with obj2 using above Equals() operator
      if (ReferenceEquals(obj1, null))    
        return ReferenceEquals(obj2, null); 
      else                  
        return obj1.Equals(obj2);
    }

    public static bool operator !=(With obj1, With obj2)
    {
      return !(obj1 == obj2);
    }


    public override int GetHashCode()
    {
      string ensemble = this.UniqueId.ToString() + this.Name;
      return ensemble.GetHashCode();
    }

  }


  internal class Sans
  {
    public string Name { get; set; }
    public int UniqueId { get; set; }

    public override bool Equals(object obj)
    {
      if (obj == null)
        return false;

      if (object.ReferenceEquals(this, obj))
        return true;

      if (!(obj is Sans))
        return false;
      Sans selection2 = (Sans)obj;

      if ((this.UniqueId == selection2.UniqueId) &&
        (this.Name == selection2.Name))
        return true;

      return base.Equals(obj);
    }


    public override int GetHashCode()
    {
      string ensemble = this.UniqueId.ToString() + this.Name;
      return ensemble.GetHashCode();
    }


  }

  public void Test()
  {
    // Note that 'with' and 'with2' are different references 
    // with the same state
    With with = new With() { Name= "Smeg", UniqueId = 2739 };
    With with2 = new With() { Name = "Smeg", UniqueId = 2739 };
    With with3 = new With() { Name = "Smeg", UniqueId = 2740 };
    Trace.Assert(with.Equals(with2));
    Trace.Assert(with == with2);
    Trace.Assert(!(with != with2));
    Trace.Assert(!(with == null));
    Trace.Assert(!(null == with));

    Trace.Assert(!with.Equals(with3));
    Trace.Assert(with != with3);     
    Trace.Assert(!with2.Equals(with3));
    Trace.Assert(with2 != with3);

    // Note that 'sans' and 'sans2' are different references 
    // with the same state
    Sans sans = new Sans() { Name = "Smeg", UniqueId = 2739 };
    Sans sans2 = new Sans() { Name = "Smeg", UniqueId = 2739 };
    Sans sans3 = new Sans() { Name = "Smeg", UniqueId = 2740 };
    Trace.Assert(sans.Equals(sans2));
    Trace.Assert(!(sans == sans2));
    Trace.Assert(sans != sans2);
    Trace.Assert(!(sans == null));
    Trace.Assert(!(null == sans));

    Trace.Assert(!sans.Equals(sans3));
    Trace.Assert(sans != sans3);
    Trace.Assert(!sans2.Equals(sans3));
    Trace.Assert(sans2 != sans3);
  }
}
The 2 highlited lines show the difference. Without '==', '!=' operator the comparison is by reference so where 'with == with2' succeeds 'sans == sans2' fails even though the different references refer to objects with exactly the same state (and even though the 'Equals()' method does return true in this case).

No comments: