Home > Blockchain >  How to compare records containing collections of records without implementing equality comparisons
How to compare records containing collections of records without implementing equality comparisons

Time:09-29

C# 9 introduces records, which among other benefits allow for really easy comparison. Is there some way I can take advantage of that functionality to compare a record composed of a collection of other records?

For example:

record Foo
{
    string Name {get; set;}
    List<Bar> Bars {get; set;}
    public Foo(string name, params int[] values)
    {
        Name = name;
        Bars = values.Select(v => new Bar(v)).ToList();
    }
}

record Bar
{
    int Value {get; set;}
    public Bar(int value) => Value = value;
}

Somewhere else in the code:

var foo1 = new Foo("Hi",1,2,3);
var foo2 = new Foo("Hi",1,2,3);

return foo1 == foo2; // I want this to return true

By the way I am not looking for a solution to this specific piece of code. I know I can override the == operator or implement IComparable<Foo>, etc. My goal is to leverage built-in functionality so that I don't have to implement my own methods every time I want to compare a data container composed of a collection of data containers. Is there a way to do that?

Thanks!

CodePudding user response:

Unfortunately there is no "nice" way to do what you want. You typically have one of two choices:

  • Manually implement equality for your record, in this case using SequenceEqual for the list, or
  • only use classes that have value semantics.

For the latter you could write a wrapper class for list, e.g. as described in this answer to "record types with collection properties & collections with value semantics", and then only use that class in your records.

CodePudding user response:

I actually found an ok solution. You can extend List<T> to override Equals and GetHashCode

public class ValueEqualityList<T>:List<T>
{
    private readonly bool _requireMathcingOrder;
    public ValueEqualityList(bool requireMatchingOrder = false) => _requireMathcingOrder = requireMatchingOrder;

    public override bool Equals(object other)
    {
        if (!(other is IEnumerable<T> enumerable)) return false;
        if(!_requireMathcingOrder)return enumerable.ScrambledEquals(this);
        return enumerable.SequenceEqual(this);
    }

    public override int GetHashCode()
    {
        var hashCode = 0;
        foreach (var item in this)
        {
            hashCode ^= item.GetHashCode();
        }

        return hashCode;
    }
}

Foo becomes:

record Foo
{
    string Name {get; set;}
    List<Bar> Bars {get; set;}
    public Foo(string name, params int[] values)
    {
        Name = name;

        //this is the line that changed
        Bars = new ValueEqualityList<Bar>(values.Select(v => new Bar(v)));
    }
}

This uses some helper code:

static class EnumerableExtensions
{

    /// <summary>
    /// Returns true if both enumerables contain the same items, regardless of order. O(N*Log(N))
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="first"></param>
    /// <param name="second"></param>
    /// <returns></returns>
    public static bool ScrambledEquals<T>(this IEnumerable<T> first, IEnumerable<T> second)
    {
        var counts = first.GetCounts();

        foreach (var item in second)
        {
            if (!counts.TryGetValue(item, out var count)) return false;
            count -= 1;
            counts[item] = count;
            if (count < 0) return false;
        }

        return counts.Values.All(c => c == 0);
    }


    public static Dictionary<T, int> GetCounts<T>(this IEnumerable<T> enumerable)
    {

        var counts = new Dictionary<T, int>();
        foreach (var item in enumerable)
        {
            if (!counts.TryGetValue(item, out var count))
            {
                count = 0;
            }

            count  ;
            counts[item] = count;
        }

        return counts;
    }

}
  • Related