Search code examples
c#recordequality

Uniquness of a record containing a collection


I am using C# records in a HashSet to enforce uniqueness. This works well, but as soon as the record contains a Collection then it stops working.

ie. this passes:

public record Person
{
    public string FirstName { get; set; }
}

HashSet<Person> uniquePeople = new();
uniquePeople.Add(new Person { FirstName = "John" });
uniquePeople.Add(new Person { FirstName = "John" });
Assert.That(uniquePeople.Count() == 1);

But this fails:

public record Person
{
    public string FirstName { get; set; }
    public HashSet<int> FavouriteNumbers { get; set; } = new();
}

HashSet<Person> uniquePeople = new();
uniquePeople.Add(new Person { FirstName = "John" });
uniquePeople.Add(new Person { FirstName = "John" });
Assert.That(uniquePeople.Count() == 1);

How can I enforce uniqueness on nested collections using records?


Solution

  • You want custom Equals and GetHashCode since you want to compare FavouriteNumbers as sets, not as references; that's why I suggest turning record (which has its own implementation of these methods) into class:

    // Note class, not record
    public class Person : IEquatable<Person> {
      public string FirstName { get; set; }
      public HashSet<int> FavouriteNumbers { get; set; } = new();
    
      public override int GetHashCode() =>
        HashCode.Combine(FirstName, FavouriteNumbers.Count);
    
      public bool Equals(Person? other) => other is not null &&
        string.Equals(FirstName, other.FirstName) &&
        FavouriteNumbers.SetEquals(other.FavouriteNumbers);
    
      public override bool Equals(object? obj) => Equals(obj as Person);
    
      public override string ToString() => $"{FirstName}";
    }
    

    If you have to deal with record (you can't change it), you can implement custom comparer:

    public sealed class PersonEqualityComparer : IEqualityComparer<Person> {
      public bool Equals(Person? x, Person? y) {
        if (ReferenceEquals(x, y))
          return true;
        if (x is null || y is null)
          return false;
    
        return string.Equals(x.FirstName, y.FirstName) &&
               x.FavouriteNumbers.SetEquals(y.FavouriteNumbers);
      }
    
      public int GetHashCode(Person obj) =>
        obj is null ? 0 : HashCode.Combine(obj.FirstName, obj.FavouriteNumbers.Count);
    }
    

    and then use it:

    // note PersonEqualityComparer
    HashSet<Person> uniquePeople = new(new PersonEqualityComparer());
    
    uniquePeople.Add(new Person { FirstName = "John" });
    uniquePeople.Add(new Person { FirstName = "John" });