Table of Contents

Structural equality comparisons

.NET provides reference types with a default implementation of object.GetHashCode() and object.Equals(object) that considers every object to be unique, and therefore these methods only return 'equivalent' results when two references are to the same actual object. Types may override these methods to provide by-value equality and hash functions, and in fact are encouraged to do so. EqualityComparer<T>.Default in fact relies on this methods to perform its function.

Even when types override these methods, they are often not implemented for deep by-value comparison. This is particularly true when a type contains collection members, since testing a collection's contents for by-value equality of each element can be difficult.

Nerdbank.MessagePack alleviates these difficulties somewhat by providing structural (i.e. deep, by-value) equality testing and hashing for arbitrary types, using the same GenerateShapeAttribute technology that it uses for serialization. It does this via the StructuralEqualityComparer.GetDefault method, which returns an instance of IEqualityComparer<T> for the specified type that provides deep, by-value checking and hashing.

Here is an example of using this for structural equality checking for a user-defined type that does not implement it itself:

void Sample()
{
    var data1a = new MyData { A = "foo", B = new MyDeeperData { C = 5 } };
    var data1b = new MyData { A = "foo", B = new MyDeeperData { C = 5 } };
    var data2 = new MyData { A = "foo", B = new MyDeeperData { C = 4 } };
    Console.WriteLine($"data1a == data1b? {data1a == data1b}"); // false
    Console.WriteLine($"data1a.Equals(data1b)? {data1a.Equals(data1b)}"); // false
    bool equalByValue = StructuralEqualityComparer.GetDefault<MyData>().Equals(data1a, data1b);
    Console.WriteLine($"data1a equal to data1b by value? {equalByValue}"); // true

    Console.WriteLine($"data1a == data2? {data1a == data2}"); // false
    Console.WriteLine($"data1a.Equals(data2)? {data1a.Equals(data2)}"); // false
    equalByValue = StructuralEqualityComparer.GetDefault<MyData>().Equals(data1a, data2);
    Console.WriteLine($"data1a equal to data2 by value? {equalByValue}"); // false
}

[GenerateShape]
internal partial class MyData
{
    public string? A { get; set; }
    public MyDeeperData? B { get; set; }
}

internal class MyDeeperData
{
    public int C { get; set; }
}

Collision resistant hashing functions can be produced by calling GetHashCollisionResistant instead of GetDefault, as described in our topic on hash collisions.

Learn more from StructuralEqualityComparer.