Exploring the .NET CoreFX Part 7: Reference Versus Structural Equality
Exploring the .NET CoreFX .net core csharp system.collections.immutable
Published: 2014-11-27
Exploring the .NET CoreFX Part 7: Reference Versus Structural Equality

This is part 7/17 of my Exploring the .NET CoreFX series.

In the previous post, I referenced EqualityComparer.Default. If T does not implement IEquatable, EqualityComparer.Default will use the framework-defined Object.Equals(), which implements reference equality.

However, many times you want to compare two types for structural equality (i.e. identical content) rather than reference equality (i.e. two references point to the same instance of the class). The interface IStructuralEquatable was defined to allow a class to explicitly implement structural, rather than reference equality. Related classes include IStructuralComparable and StructuralComparisons.

IStructuralEquatable.Equals() also accepts a user-provided IEqualityComparer which will be used to compare the object’s member variables for equality.

Here’s some sample code which demonstrates its use:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// A comparer that considers double.NaN != double.NaN
public class NanComparer : IEqualityComparer
{
    public new bool Equals(object x, object y)
    {
        if (x is double)
            return (double) x == (double) y;
        else
            return EqualityComparer<object>.Default.Equals(x, y);
    }

    public int GetHashCode(object obj)
    {
        return EqualityComparer<object>.Default.GetHashCode(obj);
    }
}

// C#'s Array implements IStructualEquatable but does not implement IEquatable
double[] array1 = { double.NaN, 1.0, 2.0 };
double[] array2 = { double.NaN, 1.0, 2.0 };

// Compare the arrays for equality using Object.Equals() (reference equality).
Console.WriteLine(array1.Equals(array2)); // outputs false
IStructuralEquatable equ = array1;

// Call IStructuralEquatable.Equals using default comparer.
// EqualityComparer<object>.Default.Equals considers double.NaN to
// be equal to itself.
Console.WriteLine(equ.Equals(array2,
      EqualityComparer<object>.Default)); // outputs true

// Call IStructuralEquatable.Equals using
// StructuralComparisons.StructuralEqualityComparer. This falls back
// to EqualityComparer<object>.Default.Equals.
Console.WriteLine(equ.Equals(array2,
      StructuralComparisons.StructuralEqualityComparer)); // outputs true

// Call IStructuralEquatable.Equals using NanComparer.
Console.WriteLine(equ.Equals(array2,
      new NanComparer())); // outputs false because NaN != NaN

The .NET Core’s ImmutableArray class implements IStructuralEquatable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
namespace System.Collections.Immutable
{
    /// <summary>
    /// A readonly array with O(1) indexable lookup time.
    /// </summary>
    /// <typeparam name="T">The type of element stored by the array.</typeparam>
    [DebuggerDisplay("{DebuggerDisplay,nq}")]
    public partial struct ImmutableArray<T> : IReadOnlyList<T>, IList<T>,
        IEquatable<ImmutableArray<T>>, IImmutableList<T>, IList,
        IImmutableArray, IStructuralComparable, IStructuralEquatable
    {
        ...
    }
}

It is unclear to me why this is the only collection in System.Collections.Immutable to implement IStructuralEquatable.

Recommendations

  1. If a collection implements IStructuralEquatable, use IStructuralEquatable.Equals() to test for structural equality. Use StructuralComparisons.StructuralEqualityComparer for simple structural equality, or a custom IEqualityComparer otherwise.
  2. If a collection implements IStructuralComparable, use IStructuralComparable.CompareTo() to perform a structural comparison. Use StructuralComparisons.StructuralComparer for simple structural comparisons, or a custom IComparer otherwise.
  3. Consider implementing IStructuralComparable and IStructuralEquatable on custom collections.