共用方式為


協變傳回

注意

本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。

功能規格與已完成實作之間可能有一些差異。 這些差異是在的相關 語言設計會議(LDM)注意事項中擷取的。

您可以在關於 規格的一篇文章中深入了解將功能規格納入 C# 語言標準的過程

冠軍問題:https://github.com/dotnet/csharplang/issues/49

總結

支援 covariant 傳回型別。 具體來說,允許方法的覆寫宣告比所覆寫的方法更衍生的傳回型別,而且同樣地允許覆寫唯讀屬性來宣告更多衍生型別。 在更衍生的型別中出現的覆寫宣告,必須提供的傳回型別至少要和其基礎型別中的覆寫宣告一樣具體。 方法或屬性的呼叫端將會在調用中靜態獲得更為具體的返回型別。

動機

程式代碼中常見的模式是,必須發明不同的方法名稱,才能解決覆寫必須傳回與覆寫方法相同的類型的語言條件約束。

這在工廠模式中很有用。 例如,在 Roslyn 程式碼庫中,我們會有

class Compilation ...
{
    public virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
    public override CSharpCompilation WithOptions(Options options)...
}

詳細設計

這是 C# 中 共變 傳回型別的規格。 我們的意圖是允許方法被覆寫時能返回比所覆寫方法更衍生的返回型別,並且同樣允許覆寫只讀屬性以返回更衍生的型別。 方法或屬性的呼叫端會以靜態方式接收更精確的傳回型別,而在更衍生的型別中出現的覆寫必須提供的傳回型別至少要與其基底型別中的覆寫一樣具體或更具體。


類別方法覆寫

類別覆寫方法的現有條件約束(§15.6.5

  • override 方法和覆寫的基底方法具有相同的傳回類型。

已修改為

  • override 方法必須具有可透過識別轉換轉換的傳回型別,或 (如果方法有傳回值 - 不是 ref 傳回 請參閱 .1.1.0.5 隱含參考轉換至覆寫基底方法的傳回型別。

而且下列額外需求會附加至該清單:

  • 覆寫方法必須具有可以透過識別轉換轉換的傳回型別,或者(如果該方法具備傳回值,而非 ref 傳回§13.1.0.5)能隱式參考轉換至在(直接或間接)覆寫方法的基類型中所宣告的覆寫基底方法的每個覆寫的傳回型別。
  • 覆寫方法的傳回型別必須至少和該方法一樣具有存取性(存取性範疇 - §7.5.3)。

此條件約束允許 private 類別中的覆寫方法具有 private 傳回型別。 不過,需要在 public 類型中實作 public 覆寫方法,才能有 public 返回類型。

類別屬性和索引器覆寫

類別覆寫屬性的現有條件約束(§15.7.6

覆寫屬性宣告應指定與繼承屬性完全相同的存取修飾詞和名稱,且在覆寫屬性類型與繼承屬性之間應有 的識別轉換。 如果繼承的屬性只有單一存取子(亦即,如果繼承的屬性是唯讀或唯寫的),則覆寫屬性應該只包含該存取子。 如果繼承的屬性包含這兩個存取子(亦即,如果繼承的屬性為讀寫),則覆寫屬性可以包含單一存取子或兩個存取子。

已修改為

覆寫屬性宣告應指定與繼承屬性完全相同的存取修飾詞和名稱,且必須有一個身分轉換 或 (如果繼承的屬性是只讀的,且具有傳回值,而不是 引用傳回值§13.1.0.5)從覆寫屬性的類型到繼承屬性的類型的隱含參考轉換。 如果繼承的屬性只有單一存取子(亦即,如果繼承的屬性是唯讀或唯寫的),則覆寫屬性應該只包含該存取子。 如果繼承的屬性包含這兩個存取子(亦即,如果繼承的屬性為讀寫),則覆寫屬性可以包含單一存取子或兩個存取子。 覆寫屬性的類型必須至少與被覆寫屬性一樣可存取(可存取性領域 - §7.5.3)。


下列草稿規格的其餘部分,提出了進一步擴充介面方法的協變返回的建議,以供日後再做考量。

介面方法、屬性和索引器的覆寫

在 C# 8.0 中引入 DIM 功能後,新增允許的成員類型,我們進一步增加了對 override 成員的支持,以及支持協變返回。 這些遵循針對類別所指定的 override 成員規則,並具有下列差異:

以下課程中的文字:

覆寫宣告所覆寫的方法稱為 覆寫基底方法。 針對在類別 M中宣告的覆寫方法 C,覆寫的基底方法是透過檢查 C的每個基類來確定,從 C 的直接基類開始,然後繼續每個後續的直接基類,直到在某個基類類型中找到至少一個具有與 M 相同簽章的可存取方法,而這是在替換型別參數之後的結果。

將被賦予介面的對應規格:

覆寫宣告所覆寫的方法稱為 覆寫基底方法。 對於在介面 M宣告的覆寫方法 I,通過檢查 I的每個直接或間接基底介面,來確定其覆寫的基底方法,收集宣告了一個可訪問方法且在替換類型參數後與 M 簽章相同的介面的集合。 如果這個介面集具有的最衍生型別,並且此集合中的每個型別都可以等同或有隱含的參考轉換至該型別,且該型別包含唯一的此類方法宣告,則這是被覆寫的基底方法

我們同樣允許介面中的 override 屬性和索引器,如 \15.7.6 Virtual、sealed、override 和 abstract 存取子所指定,

名稱查閱

在類別 override 宣告存在的情況下,名稱查閱會修改其結果,這是透過在類別階層中,自標識符修飾語類型開始,運用最衍生的 override 宣告的成員詳細資訊來完成的(如果沒有修飾語,則從 this 開始)。 例如,我們在 §12.6.2.2 對應參數

在類別中定義的虛擬方法和索引器中,參數清單會從以接收者的靜態類型為起點並搜尋其基類時,找到的函式成員的第一個宣告或覆寫中挑選。

我們新增至此

針對介面中定義的虛擬方法和索引器,參數清單是從在包含此函式成員覆寫宣告的型別中,尋找最衍生型別後所得到的函式成員宣告或覆寫中選擇。 如果沒有唯一的這類類型存在,則為編譯時期錯誤。

對於屬性或索引器存取的結果類型,現有的文字

  • 如果 I 識別的是一個實例屬性,則結果為屬性存取,包含E的相關聯實例表達式,以及屬性類型的相關聯型別。 如果 T 是類別類型,則會從從 T開始尋找的屬性的第一個宣告或覆寫中挑選相關聯的類型,並搜尋其基類。

使用增強

如果 T 是介面類型,則會從 T 或其最衍生的直接或間接基底介面中找到之屬性的宣告或覆寫中挑選相關聯的類型。 如果沒有唯一的這類類型存在,則為編譯時期錯誤。

應該在 §12.8.12.3 索引器存取 進行類似的變更

第12.8.10調用運算式中, 我們補充現有的文字

  • 否則,結果為一個值,其類型為方法或委託的返回類型的相關聯類型。 如果調用是實例方法,且接收者為類別類型 T,則相關聯的類型將從從 T 開始,搜尋其基類時找到的該方法的第一個宣告或覆寫中挑選出。

如果調用的是實例方法,且接收者是介面類型 T,則會從 T 及其直接或間接基底介面中最派生的介面所宣告或覆蓋的方法中挑選關聯的類型。 如果沒有唯一的這類類型存在,則為編譯時期錯誤。

隱含介面實現

規格的這個區段

為了進行介面映射,當類別成員 A 與介面成員 B 匹配時:

  • AB 是方法,AB 的名稱、類型和正式參數清單相同。
  • AB 是屬性,AB 的名稱和類型相同,而且 AB 具有相同的存取子(如果不是明確的介面成員實作,A 允許有其他存取子)。
  • AB 是事件,而 AB 的名稱和類型相同。
  • AB 是索引器,AB 的類型和正式參數清單相同,而且 A 具有與 B 相同的存取子(A 如果不是明確的介面成員實作,則允許有額外的存取子)。

修改方式如下:

為了進行介面映射,當類別成員 A 與介面成員 B 匹配時:

  • AB 是方法,而且 AB 的名稱和正式參數清單都相同,而且 A 的傳回型別可透過隱含參考轉換成 B的傳回型別,轉換為傳回型別 B 的傳回型別。
  • AB 是屬性,AB 的名稱相同,AB 具有相同的存取子(如果A 不屬於明確的介面成員實作,則允許有額外的存取子),而且 A 的類型可通過身分轉換轉為 B 的傳回類型,或者,如果 A 是只讀屬性,則隱含參考轉換。
  • AB 是事件,而 AB 的名稱和類型相同。
  • AB 是索引器,AB 的正式參數清單相同,A 具有與 B 相同的存取子(如果A 不是明確的介面成員實作,則允許有其他存取子),而且 A 的類型可轉換成透過身分識別轉換的傳回型別 B 或 如果 A 是唯讀索引器,則為隱含參考轉換。

這在技術上是一項重大變更,因為如下列程式所示,今天會列印 "C1.M",但在提議的修訂下會列印 "C2.M"。

using System;

interface I1 { object M(); }
class C1 : I1 { public object M() { return "C1.M"; } }
class C2 : C1, I1 { public new string M() { return "C2.M"; } }
class Program
{
    static void Main()
    {
        I1 i = new C2();
        Console.WriteLine(i.M());
    }
}

由於這項重大變更,我們可能會考慮不支援隱含實作上的共變數傳回型別。

介面實作的條件約束

我們需要一個規則,即明確介面實作必須宣告的傳回型別,其衍生程度不能低於在其基底介面中任何覆寫所宣告的傳回型別。

API 相容性影響

待定

待解決問題

規範沒有說明呼叫端如何獲得更細緻的傳回類型。 大概會以呼叫端取得最衍生的覆寫參數規格的類似方式來完成。


如果我們有下列介面:

interface I1 { I1 M(); }
interface I2 { I2 M(); }
interface I3: I1, I2 { override I3 M(); }

請注意,在 I3中,I1.M()I2.M() 的方法已「合併」。 實作 I3時,必須同時實作兩者。

一般而言,我們需要明確的實作來指涉原始方法。 問題是,在課堂上

class C : I1, I2, I3
{
    C IN.M();
}

這是什麼意思? 應該 N 是什麼?

我建議我們允許實施 I1.MI2.M(但不能同時實施),並將此視為兩者皆已實施。

缺點

  • 每項語言的改變必須能夠自行帶來收益。
  • [ ] 我們應該確保效能合理,即使在深層繼承階層的情況下
  • [ ] 我們應該確保翻譯策略的副產物不會影響語言語意,即使在從舊編譯器使用新的 IL 時也不能影響。

替代方案

我們可以稍微放寬語言規則,以便在來源中允許。

// Possible alternative. This was not implemented.
abstract class Cloneable
{
    public abstract Cloneable Clone();
}

class Digit : Cloneable
{
    public override Cloneable Clone()
    {
        return this.Clone();
    }

    public new Digit Clone() // Error: 'Digit' already defines a member called 'Clone' with the same parameter types
    {
        return this;
    }
}

未解決的問題

  • [ ] 編譯為使用此功能的 API 如何在舊版的語言中運作?

設計會議