2011-03-05 74 views
8

下面調用重載Enumerable.Select方法:重載的方法組參數會混淆重載解析嗎?

var itemOnlyOneTuples = "test".Select<char, Tuple<char>>(Tuple.Create); 

失敗的歧義錯誤(命名空間爲清楚起見移除):

The call is ambiguous between the following methods or properties: 
'Enumerable.Select<char,Tuple<char>> 
      (IEnumerable<char>,Func<char,Tuple<char>>)' 
and 
'Enumerable.Select<char,Tuple<char>> 
      (IEnumerable<char>, Func<char,int,Tuple<char>>)' 

我當然可以理解爲什麼指定類型參數明確地會導致一個模棱兩可的問題(兩個重載都會適用),但是我沒有看到這樣做。

對我來說這似乎很清楚,目的是調用第一個重載,方法組參數解析爲Tuple.Create<char>(char)。第二次過載不應適用,因爲Tuple.Create過載均不能轉換爲預期的Func<char,int,Tuple<char>>類型。我是猜測編譯器被Tuple.Create<char, int>(char, int)弄糊塗了,但是它的返回類型是錯誤的:它返回一個二元組,因此不能轉換爲相關的Func類型。

順便說一句,以下任何使編譯器高興:

  1. 指定類型參數的方法組參數:Tuple.Create<char>(也許這實際上是一個類型推斷問題?)。
  2. 使參數成爲lambda表達式而不是方法組:x => Tuple.Create(x)。 (在Select調用中可以很好地與類型推理結合使用)。

不出所料,試圖調用的Select的其他重載以這種方式也失敗:

var itemIndexTwoTuples = "test".Select<char, Tuple<char, int>>(Tuple.Create); 

這裏有什麼確切的問題?

回答

20

首先,我注意到,這是一個重複:

Why is Func<T> ambiguous with Func<IEnumerable<T>>?

這裏有什麼確切的問題?

托馬斯的猜測基本上是正確的。這裏是確切的細節。

讓我們一次完成一個步驟。我們有一個調用:

"test".Select<char, Tuple<char>>(Tuple.Create); 

重載解析必須確定調用Select的含義。在字符串或任何基類的字符串上沒有方法「選擇」,所以這必須是擴展方法。

候選集有多種可能的擴展方法,因爲字符串可以轉換爲IEnumerable<char>,並且大概在那裏有一個using System.Linq;。有許多擴展方法與模式「Select,generic arity two,使用給定的方法類型參數構造時的第一個參數採用IEnumerable<char>」匹配。

特別是兩個候選人是:

Enumerable.Select<char,Tuple<char>>(IEnumerable<char>,Func<char,Tuple<char>>) 
Enumerable.Select<char,Tuple<char>>(IEnumerable<char>,Func<char,int,Tuple<char>>) 

現在,我們面對的是第一個問題是考生適用?也就是說,是否有從每個提供的參數到相應的形式參數類型的隱式轉換?

一個很好的問題。顯然,第一個參數將是「接收器」,一個字符串,它可以隱式轉換爲IEnumerable<char>。現在的問題是第二個參數,即方法組「Tuple.Create」是否可以隱式轉換爲形式參數類型Func<char,Tuple<char>>Func<char,int, Tuple<char>>

何時可以轉換爲給定委託類型的方法組? 當重載解析成功時,方法組可以轉換爲委託類型,給定與委託的正式參數類型相同類型的參數。

也就是說,假如表達式'someA'的類型爲'A',如果在形式爲M(someA)的呼叫上的重載分辨率成功,則M可轉換爲Func<A, R>

重載分辨率在撥打Tuple.Create(someChar)時成功了嗎?是;超載分辨率將選擇Tuple.Create<char>(char)

重載分辨率在撥打Tuple.Create(someChar, someInt)時成功了嗎?是的,超載分辨率將選擇Tuple.Create<char,int>(char, int)

由於在這兩種情況下重載解析都會成功,所以方法組可以轉換爲兩種委託類型。 其中一種方法的返回類型不符合委託的返回類型的事實是不相關的;根據返回類型分析,重載分辨率不成功或失敗

有人可能會合理地說,從方法組到代理類型的可轉換性應基於返回類型分析而成功或失敗,但這不是指定語言的方式;該語言被指定爲使用重載解析作爲方法組轉換的測試,我認爲這是一個合理的選擇。

因此我們有兩個適用的候選人。有什麼方法可以決定哪個是比其他好?規範指出,轉換到更具體的類型更好;如果你有

void M(string s) {} 
void M(object o) {} 
... 
M(null); 

然後重載決議選擇字符串版本,因爲字符串比對象更具體。這些委託類型之一是否比其他委託更具體?不,沒有比另一個更具體。 (這是更好的轉換規則的簡化,實際上有很多同分決賽,但他們都不適用於這裏。)

因此是沒有根據的喜歡一個比其他。

同樣,一個可以合理地說,當然,有一個基礎,即,這些轉換的一個會產生委託返回類型不匹配錯誤,其中一人不肯。此外,雖然,被指定的語言通過考慮形參類型之間的關係來思考betterness,而不是你所選擇的轉換是否最終會導致錯誤。

由於沒有基礎,使更喜歡一個比其他,這是一個多義性錯誤。

這是很容易建立類似的模糊性錯誤。例如:

void M(Func<int, int> f){} 
void M(Expression<Func<int, int>> ex) {} 
... 
M(x=>Q(++x)); 

這就是曖昧。即使它是非法的表達式樹內++,兌換邏輯不考慮拉姆達的身體是否有它裏面的東西,將是一個表達式樹非法的。轉換邏輯只是確保類型檢出,而且確實如此。鑑於此,沒有理由選擇其中一個M,因此這是一個含糊不清的問題。

您注意,

"test".Select<char, Tuple<char>>(Tuple.Create<char>); 

成功。你現在知道爲什麼。重載決策必須確定是否

Tuple.Create<char>(someChar) 

Tuple.Create<char>(someChar, someInt) 

會成功。既然第一個是候選人,第二個候選人是不適用的,因此不會變得模棱兩可。

你也注意到,

"test".Select<char, Tuple<char>>(x=>Tuple.Create(x)); 

是毫不含糊的。 Lambda轉換考慮到返回的表達式類型與目標委託的返回類型的兼容性。不幸的是,方法組和lambda表達式使用兩種細微差別的算法來確定可轉換性,但我們現在堅持使用它。請記住,方法組轉換的語言比lambda轉換的時間要長得多;如果他們在同一時間被添加,我想他們的規則將會一致。

+4

埃裏克,*「我們堅持[兩個微妙不同的算法]」的可轉換性的聲明表明,有一個向後兼容性考慮在這裏玩,是嗎?假設允許方法組轉換使用返回類型分析來解決分辨率問題,可能會導致現有代碼導致不同的超載選擇。但是,我無法構建這種情況實際發生的情況 - 實際上是否存在這種情況?或者這更多的是平衡低價值用例的風險/回報的問題? – LBushkin 2011-03-10 07:52:08

+0

我很欣賞這個答案中的細節;我想我現在對此有一個更好的把握。實際上,我預料到我的代碼會工作,因爲我的印象是,在C#4中,方法組的返回類型被給予了更多的'關注',但大概只是爲了類型推理?無論如何,對我來說,重載決策和類型推理對他們來說有點魔力;我的直覺經常讓我失望。 – Ani 2011-03-10 08:49:40

+0

另一方面,我發現C#傾向於如此強烈地將以下兩個方面持續分開,這讓我感到有些困惑:a)用戶可能意味着什麼b)用戶可能認爲合法。想要'b)'在'a)'中發揮更大的作用是錯誤的嗎? – Ani 2011-03-10 08:54:35

5

我猜編譯器被Tuple.Create<char, int>(char, int)弄糊塗了,但是它的返回類型是錯誤的:它返回一個二元組。

返回類型不是方法簽名的一部分,所以在重載解析期間不考慮它;在之後,僅在之後驗證了超載。因此,據編譯器知道,Tuple.Create<char, int>(char, int)是一個有效的候選人,它不會比Tuple.Create<char>(char)更好或更差,所以編譯器無法做出決定。

+0

謝謝,這聽起來很合理。你有任何參考證實這一點? – Ani 2011-03-05 15:09:39

+0

這不僅僅是合理的,它是準確的。 SO的參考文獻很難,投票是匿名的。 – 2011-03-05 15:58:05

+0

@Hans:對不起,沒有理解你的評論。如果不清楚的話,我正在談論文件。你想說什麼? – Ani 2011-03-05 16:03:48