Search code examples
c#linqiequalitycomparer

Use IEqualityComparer to generate a distinct list based on two properties in C#


I am trying to generate a listbox of items that is a concatenation of two strings. I have created a class that implements IEqualityComparer and I want to make this list distinct.

private void PopulateFamily()
    {
        var source = _mfgOrdersData
            .Select(o => new FamilySelector(o.ItemWrapper.ItemClass, o.ItemWrapper.Family))
            .Distinct()
            .OrderBy(f => f.CodeFamily)
            .ToList();

        FamilyFilterListBox.DataSource = source;
        FamilyFilterListBox.ValueMember = "Family";
        FamilyFilterListBox.DisplayMember = "CodeFamily";
    }

class FamilySelector : IEqualityComparer<FamilySelector>
{
    public FamilySelector(string code, string family)
    {
        Code = code;
        Family = family;
    }

    public string Code { get; set; }
    public string Family { get; set; }
    public string CodeFamily { get { return string.Format("{0}\t{1}", Code, Family); } }

    public bool Equals(FamilySelector x, FamilySelector y)
    {
        return x.Code == y.Code && x.Family == y.Family;
    }

    public int GetHashCode(FamilySelector obj)
    {
        return obj.Code.GetHashCode() ^ obj.Family.GetHashCode();
    }
}

The problem is that I am getting the list non distinct. The same item appears multiple times. I presume that Equals() or GetHashCode() are not implemented correctly.


Solution

  • Currently you run Distinct() on a collection of FamilySelector instances which results to comparing by reference equality.

    To do it right, you should pass an instance of IEqualityComparer to Distinct() call:

    var source = _mfgOrdersData
        .Select(o => new FamilySelector(o.ItemWrapper.ItemClass, o.ItemWrapper.Family))
        .Distinct(new FamilySelector())
        .OrderBy(f => f.CodeFamily)
        .ToList();
    

    You should add parameterless constructor to FamilySelector class so that code could be compiled.

    I'd also suggest small refactoring of FamilySelector class. Currently it holds the data and performs comparison. Usually implementation of IEqualityComparer is a data-less class that just performs a comparison:

    class FamilyData
    {
        public FamilyData(string code, string family)
        {
            Code = code;
            Family = family;
        }
    
        public string Code { get; set; }
        public string Family { get; set; }
        public string CodeFamily { get { return string.Format("{0}\t{1}", Code, Family); } }
    }
    
    class FamilySelector : IEqualityComparer<FamilyData>
    {
        public bool Equals(FamilyData x, FamilyData y)
        {
            return x.Code == y.Code && x.Family == y.Family;
        }
    
        public int GetHashCode(FamilyData obj)
        {
            return obj.Code.GetHashCode() ^ obj.Family.GetHashCode();
        }
    }
    
    var source = _mfgOrdersData
        .Select(o => new FamilyData(o.ItemWrapper.ItemClass, o.ItemWrapper.Family))
        .Distinct(new FamilySelector())
        .OrderBy(f => f.CodeFamily)
        .ToList();