Pulse7
9/10/2017 - 12:26 PM

Equality .NET

Equality in .NET

## There is 4 methods on **System.Object**: - static Equals() - virtual Equals() - static ReferenceEquals() - virtual GetHashCode()

There is 9 Interfaces:

  • IEquatable<T>
  • IEqualityComparer
  • IEqualityComparer<T>
  • IComparable
  • IComparable<T>
  • IComparer
  • IComparer<T>
  • IStructuralEquatable
  • IStructuralComparable

There is only 6 if you don't count legacy ones:

  • IEquatable<T>
  • IEqualityComparer<T>
  • IComparable<T>
  • IComparer<T>
  • IStructuralEquatable
  • IStructuralComparable

Default == behaviour:

  • ReferenceTypes - ReferenceEquality (** Except Strings **)
  • ValueTypes - ValueEquality

== Behave like that because it emits a ceq operator when there is no static operator == available

  • ceq - compare equal ( il operator )
  • When there is static bool operator == available it will use that
  • static bool operator == can be inherited

Boxing

  int x = 3;
  int y = 3;
  Console.WriteLine(x==3); // True
  IComparable ic1 = x;
  IComparable ic2 = y;
  Console.WriteLine(ic1==ic2); // False because x&y were boxed into ref type

String == is always case sensitive

  string s1 = "apple";
  string s2 = "Apple";
  Console.WriteLine(s1==s2); // False

Making string IgnoreCase comparison

  string s1 = "apple";
  string s2 = "Apple";
  Console.WriteLine(s1.Equals(s2, StringComparison.OrdinalIgnoreCase)); // True

Float precision

  float six = 6.000_000_0f;
  float nearlySix = 6.000_000_1f;
  Console.WriteLine(six==nearlySix); // True

Float rounding

  float x = 5.05f;
  float y = 0.95f;
  float z = x + y;
  Console.WriteLine(z); // 6
  Console.WriteLine(z==6); // False

Float rounding

  float x = 5.05f;
  float y = 0.95f;
  float z = x + y;
  Console.WriteLine(z); // 6
  Console.WriteLine(z==6); // False

virtual Object::Equals has default compare semantics

  • referenceTypes refEquality
  • valueTypes valEquality
  • Exceptions:
    • String valEquality
    • Delegates valEquality
    • Tuples valEquality

Value types get virtual Equals implementation from System.ValueType it uses reflection to compare all the fields

  • System.ValueType.Equals calls Equals() on every field - true if all return true
  • Always better to override Equals in structs to avoid reflection implementation
  public struct Food
    private string _name;
    public Food(string name)
    {
        this._name = name;
    }

Food banana = new Food("banana"); Food banana2 = new Food("banana"); Console.WriteLine(banana.Equals(banana2)); // True

Static Object.Equals

  • If both args == null returns true
  • Because static Equals uses instance Equals it will automatically pick up overriden version
public static bool Equals(Object obj1, Object obj2){
    if (obj1==obj2)
      return true;
    if (obj1==null || obj2==null)
      return false;
    else
      return obj1.Equals(obj2);
}

== Operator is determined at compile time

object s1 = "apple";
object s2 = String.Copy((string)s1);
Console.WriteLine(s1==s2); // False, because compile types of s1 and s2 is Object

Gotcha with generics

string s1 = "apple";
string s2 = String.Copy(s1);
DisplayIfArgsAreEqual(s1,s2);

static void DisplayIfArgsAreEqual(T arg1, T arg2) where T:class { Console.WriteLine(arg1==arg2); // False, Object == used }

Implementing ValueType equality example

public struct FoodItem:IEquatable
{
    private readonly string _name;
    private readonly FoodGroup _group;
public string Name => _name;
public FoodGroup Group => _group;

public FoodItem(string name, FoodGroup group)
{
    this._name = name;
    this._group = group;
}

public static bool operator ==(FoodItem lhs, FoodItem rhs)
{
    return lhs.Equals(rhs);
}

public static bool operator !=(FoodItem lhs, FoodItem rhs)
{
    return !lhs.Equals(rhs);
}

public bool Equals(FoodItem other)
{
    return _name == other._name && _group == other._group;
}

public override bool Equals(object obj)
{
    if (obj is FoodItem foodObj)
        return Equals(foodObj);
    return false;
}

public override int GetHashCode()
{
    return _name.GetHashCode() ^ _group.GetHashCode();
}

public override string ToString()
{
    return _name;
}

}

Implementing ReferenceType equality example

public class Food
{
    private readonly string _name;
    private readonly FoodGroup _group;
public String Name => _name;
public FoodGroup Group => _group;

public Food(string name, FoodGroup group)
{
    _name = name ;
    _group = group;
}

public override string ToString()
{
    return _name;
}

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

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

public override bool Equals(object obj)
{
    if (obj == null)
        return false;
    if (ReferenceEquals(obj, this))
        return true;
    if (obj.GetType() != this.GetType())
        return false;
    Food rhs = obj as Food;
    return this._name == rhs._name && this._group == rhs._group;
}

public override int GetHashCode()
{
    return _name.GetHashCode() ^ _group.GetHashCode();
}

}

public sealed class CookedFood:Food { private readonly string _cookingMethod; public string CookingMethod => _cookingMethod; public CookedFood(string name, FoodGroup group, string cookingMethod) : base(name, group) { this._cookingMethod = cookingMethod; }

public override string ToString()
{
    return String.Format("{0} {1}", _cookingMethod, Name);
}

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

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

public override bool Equals(object obj)
{
    if (!base.Equals(obj))
        return false;
    CookedFood rhs = obj as CookedFood;
    return this._cookingMethod == rhs._cookingMethod;
}

public override int GetHashCode()
{
    return base.GetHashCode() ^ _cookingMethod.GetHashCode();
}

}

Comparison convention x.CompareTo(y)

  • 0 => x==y
  • NegativeNumber => x<y
  • PositiveNumber => x>y

String == uses Ordinal CaseSensitive comparison

String interning

  • String.Copy(s) // return copy
  • String.Intern(s) // return shareable instance from pool
  string s1 = "apple";
  string s2 = "app"+"le"; // Compiler optimization
  Console.WriteLine(s1==s2); // True
  string s3 = "orange";
  string s4 = String.Copy(s3); // String.Copy will always copy string without pooling
  Console.WriteLine(s3==s4); // False