2013-04-06 39 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