2013-04-06 63 views
6

我有一个类BaseClass,其中BaseClass只有一个Id属性。使用基类IEqualityComparer执行Distinct(),并仍然返回子类类型?

我现在需要对这些对象中的某些对象的集合进行区分。我有以下代码一遍又一遍地对每个子类:

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()调用的返回值?

回答

5

如果您查看Distinct的定义,则只涉及一个泛型类型参数(并且没有一个TCollection用于输入和输出集合,而一个TCollection用于比较器)。这意味着您的BaseClassComparer将结果类型限制为基类,并且分配中的转换是不可能的。

你可能会创建一个泛型参数,它的泛型参数至少是基类,这可能会让你更接近你想要做的。这看起来像

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解释了整个问题(根据协方差和反变量)的根本原因。

1

试想一下,如果你有:

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

什么你所期望的positions类型是什么?由于编译器从给出Distinct的参数中推导出实现IEqualityComparer<BaseClass>的参数,表达式的类型是IEnumerable<BaseClass>

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

0

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

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

如果不指定,编译器推断T的类型为BaseClass,因为BaseClassComparer实施了IEqualityComparer<BaseClass>

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

这不是已经回答了吗? – nawfal 2013-05-27 03:24:14

+0

是的,我编译并运行了这个代码 – 2013-05-27 04:18:44

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或任何更大的类型(基本类型比广告“大”类型;还有更多的动物比世界长颈鹿。)

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

那么,到底什么时候才能真正推断出类型参数呢?我们有两个候选人:PositionBaseClass两者都满足规定的范围Position满足第一个边界,因为它与第一个边界相同,并且满足第二个边界,因为它小于第二个边界。 BaseClass满足第一个边界,因为它大于第一个边界,并且与第二个边界相同。

我们有两个获奖者。我们需要一个打破平局。我们在这种情况下做什么?

这是一个辩论的一个观点,有三方面的观点:选择更具体的类型,选择更一般的类型,或者使类型推断失败。我不会重复整个论点,但足以说“选择更一般”的一方赢得了这一天。

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

所以你去了。在这种情况下,类型推断选择更一般的类型,并推断该调用意味着Distinct<BaseClass>。类型推断决不会考虑返回类型,并且它肯定不会考虑将表达式分配给,因此它选择与assign-to变量不兼容的类型不是它的商业。

我的建议是在这种情况下显式声明类型参数。

+0

在'T MyMethod(IEnumerable ,IEqualityComparer ,T)'与'Giraffe'和'Animal'之间的关系中,选择更一般的类型Animal:导致一个'Giraffe'变量,但它允许'Duck'作为第三个参数。然而,如果需要,选择更具体的类型'Giraffe':返回值可以隐式转换为更大的类型'动物';和一个返回类型'IEnumerable '隐式转换为'IEnumerable '。这是最常用的变体界面。我想知道为什么选择更普通的类型? – Virtlink 2013-04-06 15:49:34

+0

第四选择怎么样:选择一个编译的选项? – Tergiver 2013-04-08 15:52:20

+1

@Tergiver:那么我们正在考虑的是*背景*,这无情地导致了解决我们不想解决的NP-HARD问题。当上下文是指定类型变量的赋值时非常容易,但如果变量是“var”呢?如果上下文本身在调用方法的参数中呢?现在我们必须在* that *方法上执行重载解析问题,以确定哪一个编译。 – 2013-04-08 16:00:03