使用基类IEqualityComparer做Distinct(),仍然返回子类类型?

时间:2013-04-06 13:13:27

标签: c# linq distinct iequalitycomparer

我有一些派生自类BaseClass的类,其中BaseClass只有一个`Id属性。

我现在需要对其中一些对象的集合做区别。我为每个子类一遍又一遍地使用以下代码:

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

鉴于逻辑基于Id,我想创建一个比较器以减少重复:

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

但这似乎没有编译:

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

...因为它说它无法从BaseClass转换为Position。为什么比较器会强制执行此Distinct()调用的返回值?

5 个答案:

答案 0 :(得分:8)

更新:这个问题是the subject of my blog in July 2013。谢谢你提出的好问题!


您在泛型方法类型推断算法中发现了一个不幸的边缘情况。我们有:

Distinct<X>(IEnumerable<X>, IEqualityComparer<X>)

接口是:

IEnumerable<out T> -- covariant

IEqualityComparer<in T> -- contravariant

当我们从allPositionsIEnumerable<X>进行推断时,我们说“IEnumerable<T>在T中是协变的,所以我们可以接受Position 或任何更大的类型< / em>。(基本类型比派生类型“更大”;世界上有比动物长颈鹿更多的动物。)

当我们从比较器进行推断时,我们说“IEqualityComparer<T>在T中是逆变的,所以我们可以接受BaseClass 或任何较小的类型。”

那么当实际推断出类型参数时会发生什么?我们有两位候选人:PositionBaseClass两者都满足规定的界限Position满足第一个边界,因为它与第一个边界相同,并且满足第二个边界,因为它小于第二个边界。 BaseClass满足第一个边界,因为它大于第一个边界,并且与第二个边界相同。

我们有两名获胜者。我们需要打破平局。在这种情况下我们该怎么做?

这是一个争论点,三方面存在争议:选择更具体的类型,选择更一般的类型,或者让类型推断失败。我不会重复整个论点,但足以说“选择更一般”的一方赢得了胜利。

(更糟糕的是,规范中有一个错字说“选择更具体”是正确的做法!这是设计过程中编辑错误的结果,从未纠正过。编译器实现“选择更通用”。我已经提醒Mads错误,希望这将在C#5规范中修复。)

所以你去吧。在这种情况下,类型推断选择更通用的类型,并推断出调用意味着Distinct<BaseClass>。类型推断永远不会考虑返回类型,并且它当然不会将表达式分配给,因此它选择的类型是与指定变量不兼容的不是它的业务。

我的建议是在这种情况下明确说明类型参数。

答案 1 :(得分:7)

如果你看一下Distinct的定义,只涉及一个泛型类型参数(而不是一个TCollection用于输入和输出集合,一个TComparison用于比较器)。这意味着您的BaseClassComparer将结果类型约束为基类,并且无法在赋值时进行转换。

你可能会创建一个带有泛型参数的GenericComparer,该参数被限制为至少是基类,这可能会让你更接近你想要做的事情。这看起来像

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

因为您需要一个实例而不仅仅是方法调用,所以您不能让编译器(see this discussion)推断泛型类型,但在创建实例时必须这样做:

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

Eric's answer解释了整个问题的根本原因(在协方差和逆变方面)。

答案 2 :(得分:1)

想象一下,如果你有:

var positions = allPositions.Distinct(new BaseClassComparer());

您期望positions的类型是什么?由于编译器从赋予实现Distinct的{​​{1}}的参数中推断出,所以表达式的类型为IEqualityComparer<BaseClass>

该类型无法自动转换为IEnumerable<BaseClass>,因此编译器会产生错误。

答案 3 :(得分:0)

由于IEqualityComparer<T>T类型中具有逆变性,因此如果您将通用参数指定为Distinct,则可以将基类比较器与distinct结合使用:

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

如果您未指定此内容,则编译器会将T的类型推断为BaseClass,因为BaseClassComparer实现了IEqualityComparer<BaseClass>

答案 4 :(得分:0)

您的代码需要进行少量更改。贝娄工作实例:

public class BaseClass
{
    public int Id{get;set;}
}

public class Position : BaseClass
{
    public string Name {get;set;}
}
public class Comaprer<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();
    }
}
class Program
{
    static void Main(string[] args)
    {
        List<Position> all = new List<Position> { new Position { Id = 1, Name = "name 1" }, new Position { Id = 2, Name = "name 2" }, new Position { Id = 1, Name = "also 1" } };
        var distinct = all.Distinct(new Comaprer<Position>());

        foreach(var d in distinct)
        {
            Console.WriteLine(d.Name);
        }
        Console.ReadKey();
    }
}