簡體   English   中英

C# 方法重載決議未選擇具體泛型覆蓋

[英]C# Method overload resolution not selecting concrete generic override

這個完整的 C# 程序說明了這個問題:

public abstract class Executor<T>
{
    public abstract void Execute(T item);
}

class StringExecutor : Executor<string>
{
    public void Execute(object item)
    {
        // why does this method call back into itself instead of binding
        // to the more specific "string" overload.
        this.Execute((string)item);
    }

    public override void Execute(string item) { }
}

class Program
{
    static void Main(string[] args)
    {
        object item = "value";
        new StringExecutor()
            // stack overflow
            .Execute(item); 
    }
}

我遇到了一個 StackOverlowException,我可以追溯到這個調用模式,我試圖將調用轉發到更具體的重載。 令我驚訝的是,調用並沒有選擇更具體的重載,而是回調到自身。 它顯然與泛型的基類型有關,但我不明白為什么它不選擇 Execute(string) 重載。

有沒有人對此有任何見解?

上面的代碼是為了顯示模式而簡化的,實際結構有點復雜,但問題是一樣的。

看起來在 C# 規范 5.0, 7.5.3 Overload Resolution 中提到了這一點:

重載解析選擇要在 C# 中的以下不同上下文中調用的函數成員:

  • 調用在調用表達式中命名的方法(第 7.6.5.1 節)。
  • 調用在對象創建表達式(第 7.6.10.1 節)中命名的實例構造函數。
  • 通過元素訪問(第 7.6.6 節)調用索引器訪問器。
  • 調用在表達式中引用的預定義或用戶定義的運算符(第 7.3.3 節和第 7.3.4 節)。

這些上下文中的每一個都以自己獨特的方式定義了一組候選函數成員和參數列表,如上面列出的部分中詳細描述的。 例如,方法調用的候選集不包括標記為覆蓋的方法(第 7.4 節),如果派生類中的任何方法適用(第 7.6.5.1節),則基類中的方法不是候選方法

當我們看 7.4 時:

類型 T 中具有 K 個類型參數的名稱 N 的成員查找處理如下:

• 首先,確定一組名為 N 的可訪問成員:

  • 如果 T 是類型參數,則該集合是
    指定為 T 的主要約束或次要約束(第 10.1.5 節)的每個類型中名為 N 的可訪問成員,以及對象中名為 N 的可訪問成員集。

  • 否則,該集合由 T 中名為 N 的所有可訪問(第 3.5 節)成員組成,包括繼承成員和對象中名為 N 的可訪問成員。 如果 T 是構造類型,則成員集是通過替換第 10.3.2 節中描述的類型參數來獲得的。 從集合中排除包含覆蓋修飾符的成員。

如果您刪除override ,編譯器會在您投射項目時選擇Execute(string)重載。

正如 Jon Skeet關於重載文章中提到的,當調用一個類中的方法時,該方法也覆蓋了來自基類的同名方法,編譯器將始終采用類內方法而不是覆蓋,而不管“特定性” " 類型,前提是簽名是“兼容的”。

Jon 繼續指出,這是避免跨越繼承邊界的重載的一個很好的論據,因為這正是可能發生的那種意外行為。

正如其他答案所指出的,這是設計使然。

讓我們考慮一個不太復雜的例子:

class Animal
{
  public virtual void Eat(Apple a) { ... }
}
class Giraffe : Animal
{
  public void Eat(Food f) { ... }
  public override void Eat(Apple a) { ... }
}

問題是為什么giraffe.Eat(apple)解析為Giraffe.Eat(Food)而不是虛擬的Animal.Eat(Apple)

這是兩個規則的結果:

(1) 在解析重載時,接收者的類型比任何參數的類型都重要。

我希望很清楚為什么必須如此。 編寫派生類的人比編寫基類的人擁有更多的知識,因為編寫派生類的人使用了基類,反之亦然。

Giraffe的人說“我有辦法讓Giraffe任何食物”,這需要長頸鹿消化內部的特殊知識。 該信息不存在於基類實現中,它只知道如何吃蘋果。

因此,重載決議應始終優先選擇派生類的適用方法,而不是選擇基類的方法,而不管參數類型轉換的效果如何。

(2) 選擇覆蓋或不覆蓋虛擬方法不是類的公共表面區域的一部分。 這是一個私有的實現細節。 因此,在進行重載決策時不必做出會根據方法是否被覆蓋而改變的決定。

重載決議絕不能說“我要選擇虛擬的Animal.Eat(Apple)因為它被覆蓋了”。

現在,您可能會說:“好吧,假設我打電話時我Giraffe里面。” Giraffe內部的代碼擁有私有實現細節的所有知識,對吧? 所以當面對giraffe.Eat(apple) ,它可以決定調用 virtual Animal.Eat(Apple)而不是Giraffe.Eat(Food) giraffe.Eat(apple) ,對吧? 因為它知道有一個實現可以理解吃蘋果的長頸鹿的需求。

這是一種比疾病更糟糕的治療方法。 現在我們遇到了一種情況,即相同的代碼根據運行的位置而具有不同的行為! 你可以想象在類外調用giraffe.Eat(apple) ,重構它使其在類內,突然可觀察到的行為發生變化!

或者,你可能會說,嘿,我意識到我的 Giraffe 邏輯實際上足夠通用,可以移動到基類,但不能移動到 Animal,所以我將重構我的Giraffe代碼:

class Mammal : Animal 
{
  public void Eat(Food f) { ... } 
  public override void Eat(Apple a) { ... }
}
class Giraffe : Mammal
{
  ...
}

現在將所有來電giraffe.Eat(apple)Giraffe突然有重構后的不同重載決議的行為呢? 那將是非常出乎意料的!

C#是一門成功的語言; 我們非常希望確保簡單的重構(例如更改方法在層次結構中被覆蓋的位置)不會導致行為的細微變化。

加起來:

  • 重載解析將接收器優先於其他參數,因為調用知道接收器內部結構的專用代碼比調用更通用的代碼更好。
  • 重載決議期間不考慮方法是否以及在何處被覆蓋; 為了重載解析的目的,所有方法都被視為從未被覆蓋。 這是一個實現細節,而不是類型表面的一部分。
  • 解決了重載解析問題——當然是模可訪問性! -- 無論問題出現在代碼中的哪個位置,都是一樣的。 我們沒有一種算法來解決接收者是包含代碼的類型的問題,而另一種算法是當調用屬於不同的類時。

關於相關問題的其他想法可以在這里找到: https : //ericlippert.com/2013/12/23/closer-is-better/和這里https://blogs.msdn.microsoft.com/ericlippert/2007/09/ 04/未來的變化-第三部分/

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM