我有一些派生自类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()
调用的返回值?
答案 0 :(得分:8)
更新:这个问题是the subject of my blog in July 2013。谢谢你提出的好问题!
您在泛型方法类型推断算法中发现了一个不幸的边缘情况。我们有:
Distinct<X>(IEnumerable<X>, IEqualityComparer<X>)
接口是:
IEnumerable<out T> -- covariant
和
IEqualityComparer<in T> -- contravariant
当我们从allPositions
到IEnumerable<X>
进行推断时,我们说“IEnumerable<T>
在T中是协变的,所以我们可以接受Position
或任何更大的类型< / em>。(基本类型比派生类型“更大”;世界上有比动物长颈鹿更多的动物。)
当我们从比较器进行推断时,我们说“IEqualityComparer<T>
在T中是逆变的,所以我们可以接受BaseClass
或任何较小的类型。”
那么当实际推断出类型参数时会发生什么?我们有两位候选人:Position
和BaseClass
。 两者都满足规定的界限。 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();
}
}