Search code examples
c#linqdistinctiequalitycomparer

Doing Distinct() using base class IEqualityComparer, and still return the child class type?


I have a number of classes that derive from a class BaseClass where BaseClass just has an `Id property.

I now need to do distinct on a collections of some of these objects. I have the following code over and over for each of the child classes:

public class PositionComparer : IEqualityComparer<Position>
{
    public bool Equals(Position x, Position y)
    {
        return (x.Id == y.Id);
    }

    public int GetHashCode(Position obj)
    {
        return obj.Id.GetHashCode();
    }
}

Given the logic is just based on Id, I wanted to created a single comparer to reduce duplication:

public class BaseClassComparer : IEqualityComparer<BaseClass>
{
    public bool Equals(BaseClass x, BaseClass y)
    {
        return (x.Id == y.Id);
    }

    public int GetHashCode(BaseClass obj)
    {
        return obj.Id.GetHashCode();
    }
}

But this doesn't seem to compile:

  IEnumerable<Position> positions = GetAllPositions();
  positions = allPositions.Distinct(new BaseClassComparer())

...as it says it can't convert from BaseClass to Position. Why does the comparer force the return value of this Distinct() call?


Solution

  • If you look at the definition of Distinct there is only one generic type parameter involved (and not one TCollection used for input and output collections and one TComparison for the comparer). That means that your BaseClassComparer constrains the result type to base class and the conversion at the assignment is not possible.

    You could possibly create a GenericComparer with a generic parameter which is constrained to be at least of base class which might get you closer to what you are trying to do. This would look like

    public class GenericComparer<T> : IEqualityComparer<T> where T : BaseClass
    {
        public bool Equals(T x, T y)
        {
            return x.Id == y.Id;
        }
    
        public int GetHashCode(T obj)
        {
            return obj.Id.GetHashCode();
        }
    }
    

    Because you need an instance and not just a method call you can't let the generic type be inferred by the compiler (see this discussion) but have to do so when creating the instance:

    IEnumerable<Position> positions;
    positions = allPositions.Distinct(new GenericComparer<Position>());
    

    Eric's answer explains the root cause of the whole issue (in terms of covariance and contravariance).