63

I've noticed these two interfaces, and several associated classes, have been added in .NET 4. They seem a bit superfluous to me; I've read several blogs about them, but I still can't figure out what problem they solve that was tricky before .NET 4.

What use are IStructuralEquatable and IStructuralComparable?

thecoop
  • 45,220
  • 19
  • 132
  • 189

6 Answers6

54

All types in .NET support the Object.Equals() method which, by default, compares two types for reference equality. However, sometimes, it also desirable to be able to compare two types for structural equality.

The best example of this is arrays, which with .NET 4 now implement the IStructuralEquatable interface. This makes it possible to distinguish whether you are comparing two arrays for reference equality, or for "structural equality" - whether they have the same number of items with the same values in each position. Here's an example:

int[] array1 = new int[] { 1, 5, 9 };
int[] array2 = new int[] { 1, 5, 9 };

// using reference comparison...
Console.WriteLine( array1.Equals( array2 ) ); // outputs false

// now using the System.Array implementation of IStructuralEquatable
Console.WriteLine(
    StructuralComparisons.StructuralEqualityComparer.Equals( array1, array2 )
); // outputs true

Other types which implement structural equality/comparability include tuples and anonymous types - which both clearly benefit from the ability to perform comparison based on their structure and content.

A question you didn't ask is:

Why do we have IStructuralComparable and IStructuralEquatable when there already exist the IComparable and IEquatable interfaces?

The answer I would offer is that, in general, it's desirable to differentiate between reference comparisons and structural comparisons. It's normally expected that if you implement IEquatable<T>.Equals you will also override Object.Equals to be consistent. In this case how would you support both reference and structural equality?

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
LBushkin
  • 129,300
  • 32
  • 216
  • 265
  • 7
    Why can't you just specify an `IEqualityComparer` yourself that does this? What does the `IStructuralEquatable` interface add to this? – thecoop Aug 31 '10 at 14:39
  • 1
    @thecoop: There's two reasons. First, not all types implement an overload of `Equals` that accepts an `IEqualityComparer` - Array is an example, IIRC. Second, supplying an equality comparer is nice, but what if you want to express the fact that a certain method requires two objects that can be structurally compared? Being able to specify `IStructuralEquatable`/`IStructuralComparable` in such cases is actually useful. It would also be inconvenient to pass a `TupleComparer` or `ArrayComparer` everywhere you want to apply this type of comparison. The two approaches are not mutually exclusive. – LBushkin Aug 31 '10 at 14:49
  • 1
    How do such comparators relate to things like Dictionary and other collections? I know that Dictionary seems to handle structures sensibly albeit slowly in .Net 2.0; does .Net 4.0 (or 3.x for that matter) allow arrays to be conveniently stored in Dictionary (using the array contents as the key)? – supercat Aug 31 '10 at 15:13
  • @supercat: I don't believe you can use arrays as keys in .NET 4 any more easily than you could in previous versions. As far as I know, neither Dictionary nor other keyed collection classes use the `IStructuralEquatable` interface. But this would be easy enough to verify with some test code. – LBushkin Aug 31 '10 at 15:22
  • See http://stackoverflow.com/questions/5813113/can-net-test-arrays-for-equivalence-and-not-just-equal-references/5813160#5813160 for a version of this that addresses the problems @CodeInChaos points out. – E.Z. Hart Sep 06 '11 at 01:38
  • 1
    It's too bad .NET didn't better define equality, and incorporate two types of Equals/GetHashCode into the Framework, with `X.EquivalentTo(Y)` meaning that all members of the object referred to by `X` would be expected to behave equivalently to all those of the object referred to by `Y`, and `X.ValueEquals(Y)` meaning that simultaneously interchanging *all* references to `X` and `Y` would not affect the behavior of any members of either, other than an equivalence-related hash code. Note that both definitions can be evaluated for objects of *any* type. Note that base `Object.EquivalentTo`... – supercat Jul 03 '13 at 16:09
  • 1
    ...should test reference equality; base `Object.ValueEquals` should return `True` [swapping *all* references to two instances `X` and `Y` of type `System.Object` would have no observable effect on any members other than the equivalence-related `GetHashCode`], but entities whose states interact with other objects should override it to test reference equality. – supercat Jul 03 '13 at 16:11
  • 7
    I'm fairly certain this answer (and the comments) are inaccurate. .NET _does_ support two different versions of equality: `object.Equals` and `object.ReferenceEquals`. `Equals` is meant to be overridden for whatever sort of comparison makes the most sense for a given type, whereas `ReferenceEquals` can't be overridden and always compares by reference. – Zenexer Aug 30 '19 at 14:07
  • this is a similar question https://stackoverflow.com/questions/9548222/istructuralequatable-vs-equals –  Mar 03 '21 at 14:29
  • @thecoop not sure if you alread got an answer, but the current accepted answer is not correct, as Zenexer has pointed out that you can use Object.ReferenceEquals() to test reference equality. let me know if you are still interested in this question l. so I can prepare an answer that explains how it work, but it could be lengthy and you really need to be familar with IEqualityComparer to understand how ` IStructuralEquatable` work –  Mar 03 '21 at 14:29
  • I'm still confused why we need `IStructuralEquatable` when, as @Zenexer pointed out, we already have `object.Equals` and `object.ReferenceEquals`... – extremeandy Nov 03 '21 at 03:17
23

I had the same question. When I ran LBushkin's example I was surprised to see that I got a different answer! Even though that answer has 8 upvotes, it is wrong. After a lot of 'reflector'ing, here is my take on things.

Certain containers (arrays, tuples, anonymous types) support IStructuralComparable and IStructuralEquatable.

  • IStructuralComparable supports deep, default sorting.
  • IStructuralEquatable supports deep, default hashing.

{Note that EqualityComparer<T> supports shallow (only 1 container level), default hashing.}

As far as I see this is only exposed through the StructuralComparisons class. The only way I can figure out to make this useful is to make a StructuralEqualityComparer<T> helper class as follow:

    public class StructuralEqualityComparer<T> : IEqualityComparer<T>
    {
        public bool Equals(T x, T y)
        {
            return StructuralComparisons.StructuralEqualityComparer.Equals(x,y);
        }

        public int GetHashCode(T obj)
        {
            return StructuralComparisons.StructuralEqualityComparer.GetHashCode(obj);
        }

        private static StructuralEqualityComparer<T> defaultComparer;
        public static StructuralEqualityComparer<T> Default
        {
            get
            {
                StructuralEqualityComparer<T> comparer = defaultComparer;
                if (comparer == null)
                {
                    comparer = new StructuralEqualityComparer<T>();
                    defaultComparer = comparer;
                }
                return comparer;
            }
        }
    }

Now we can make a HashSet with items having containers within containers within containers.

        var item1 = Tuple.Create(1, new int[][] { new int[] { 1, 2 }, new int[] { 3 } });
        var item1Clone = Tuple.Create(1, new int[][] { new int[] { 1, 2 }, new int[] { 3 } });
        var item2 = Tuple.Create(1, new int[][] { new int[] { 1, 3 }, new int[] { 3 } });

        var set = new HashSet<Tuple<int, int[][]>>(StructuralEqualityComparer<Tuple<int, int[][]>>.Default);
        Console.WriteLine(set.Add(item1));      //true
        Console.WriteLine(set.Add(item1Clone)); //false
        Console.WriteLine(set.Add(item2));      //true

We can also make our own container play well with these other containers by implementing these interfaces.

public class StructuralLinkedList<T> : LinkedList<T>, IStructuralEquatable
    {
        public bool Equals(object other, IEqualityComparer comparer)
        {
            if (other == null)
                return false;

            StructuralLinkedList<T> otherList = other as StructuralLinkedList<T>;
            if (otherList == null)
                return false;

            using( var thisItem = this.GetEnumerator() )
            using (var otherItem = otherList.GetEnumerator())
            {
                while (true)
                {
                    bool thisDone = !thisItem.MoveNext();
                    bool otherDone = !otherItem.MoveNext();

                    if (thisDone && otherDone)
                        break;

                    if (thisDone || otherDone)
                        return false;

                    if (!comparer.Equals(thisItem.Current, otherItem.Current))
                        return false;
                }
            }

            return true;
        }

        public int GetHashCode(IEqualityComparer comparer)
        {
            var result = 0;
            foreach (var item in this)
                result = result * 31 + comparer.GetHashCode(item);

            return result;
        }

        public void Add(T item)
        {
            this.AddLast(item);
        }
    }

Now we can make a HashSet with items having containers within custom containers within containers.

        var item1 = Tuple.Create(1, new StructuralLinkedList<int[]> { new int[] { 1, 2 }, new int[] { 3 } });
        var item1Clone = Tuple.Create(1, new StructuralLinkedList<int[]> { new int[] { 1, 2 }, new int[] { 3 } });
        var item2 = Tuple.Create(1, new StructuralLinkedList<int[]> { new int[] { 1, 3 }, new int[] { 3 } });

        var set = new HashSet<Tuple<int, StructuralLinkedList<int[]>>>(StructuralEqualityComparer<Tuple<int, StructuralLinkedList<int[]>>>.Default);
        Console.WriteLine(set.Add(item1));      //true
        Console.WriteLine(set.Add(item1Clone)); //false
        Console.WriteLine(set.Add(item2));      //true
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
jyoung
  • 5,071
  • 4
  • 30
  • 47
5

The description of the IStructuralEquatable Interface says (in the "Remarks" section):

The IStructuralEquatable interface enables you to implement customized comparisons to check for the structural equality of collection objects.

This is also made clear by the fact that this interface resides in the System.Collections namespace.

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
5

Here is another example that illustrates a possible usage of the two interfaces:

var a1 = new[] { 1, 33, 376, 4};
var a2 = new[] { 1, 33, 376, 4 };
var a3 = new[] { 2, 366, 12, 12};

Debug.WriteLine(a1.Equals(a2)); // False
Debug.WriteLine(StructuralComparisons.StructuralEqualityComparer.Equals(a1, a2)); // True

Debug.WriteLine(StructuralComparisons.StructuralComparer.Compare(a1, a2)); // 0
Debug.WriteLine(StructuralComparisons.StructuralComparer.Compare(a1, a3)); // -1
Marc Sigrist
  • 3,964
  • 3
  • 22
  • 23
  • 1
    BTW it would probably be a good idea to add a generic type constraint to your StructuralEqualityComparer. e.g. where T : IStructuralEquatable – AndrewS Jan 17 '14 at 15:04
1

C# in a nutshell book:

Because Array is a class, arrays are always (themselves) reference types, regardless of the array’s element type. This means that the statement arrayB = arrayA results in two variables that reference the same array. Similarly, two distinct arrays will always fail an equality test—unless you use a custom equality comparer. Framework 4.0 introduced one for the purpose of comparing elements in arrays which you can access via the StructuralComparisons type.

object[] a1 = { "string", 123, true};
object[] a2 = { "string", 123, true};

Console.WriteLine(a1 == a2);               // False
Console.WriteLine(a1.Equals(a2));          // False

IStructuralEquatable se1 = a1;
Console.WriteLine(se1.Equals(a2, StructuralComparisons.StructuralEqualityComparer));    // True
Console.WriteLine(StructuralComparisons.StructuralEqualityComparer.Equals(a1, a2));     // True

object[] a3 = {"string", 123, true};
object[] a4 = {"string", 123, true};
object[] a5 = {"string", 124, true};

IStructuralComparable se2 = a3;
Console.WriteLine(se2.CompareTo(a4, StructuralComparisons.StructuralComparer));    // 0
Console.WriteLine(StructuralComparisons.StructuralComparer.Compare(a3, a4));       // 0
Console.WriteLine(StructuralComparisons.StructuralComparer.Compare(a4, a5));       // -1
Console.WriteLine(StructuralComparisons.StructuralComparer.Compare(a5, a4));       // 1
Sina Lotfi
  • 3,044
  • 1
  • 23
  • 30
0

F# started using them since .net 4. ( .net 2 is here)

These interfaces are crucial to F#

let list1 = [1;5;9] 
let list2 = List.append [1;5] [9]

printfn "are they equal? %b" (list1 = list2)

list1.GetType().GetInterfaces().Dump()

enter image description here

Rm558
  • 4,621
  • 3
  • 38
  • 43