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;
}
}