7

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

...因为它说它不能从转换BaseClassPosition. 为什么比较器强制此Distinct()调用的返回值?

4

5 回答 5

8

更新:这个问题是我 2013 年 7 月博客的主题。谢谢你的好问题!


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

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

接口在哪里:

IEnumerable<out T> -- covariant

IEqualityComparer<in T> -- contravariant

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

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

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

我们有两个赢家。我们需要一个决胜局。在这种情况下我们该怎么办?

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

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

所以你去。在这种情况下,类型推断会选择更通用的类型并推断出调用的意思Distinct<BaseClass>。类型推断从不考虑返回类型,当然也不考虑表达式被分配的内容,因此它选择与分配给变量不兼容的类型这一事实不是它的业务。

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

于 2013-04-06T14:18:31.997 回答
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();
    }
}

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

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

Eric 的回答解释了整个问题的根本原因(就协变和逆变而言)。

于 2013-04-06T13:18:21.353 回答
1

想象一下,如果你有:

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

你期望的类型positions是什么?Distinct正如编译器从给定的实现参数推断出IEqualityComparer<BaseClass>的,表达式的类型是IEnumerable<BaseClass>

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

于 2013-04-06T13:20:53.787 回答
0

由于IEqualityComparer<T>在类型中是逆变的T,如果您将泛型参数指定为,则可以使用具有 distinct 的基类比较器Distinct

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

如果你没有指定这个,编译器会推断出的类型TBaseClass因为BaseClassComparerimplements IEqualityComparer<BaseClass>

于 2013-04-06T13:41:54.053 回答
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();
    }
}
于 2013-04-06T13:46:07.747 回答