協變傳回
注意
本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。
功能規格與已完成實作之間可能有一些差異。 這些差異是在的相關
您可以在關於 規格的一篇文章中深入了解將功能規格納入 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 方法和覆寫的基底方法具有相同的傳回類型。
已修改為
而且下列額外需求會附加至該清單:
此條件約束允許 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
匹配時:
A
和B
是方法,A
和B
的名稱、類型和正式參數清單相同。A
和B
是屬性,A
和B
的名稱和類型相同,而且A
與B
具有相同的存取子(如果不是明確的介面成員實作,A
允許有其他存取子)。A
和B
是事件,而A
和B
的名稱和類型相同。A
和B
是索引器,A
和B
的類型和正式參數清單相同,而且A
具有與B
相同的存取子(A
如果不是明確的介面成員實作,則允許有額外的存取子)。
修改方式如下:
為了進行介面映射,當類別成員
A
與介面成員B
匹配時:
A
和B
是方法,而且A
和B
的名稱和正式參數清單都相同,而且A
的傳回型別可透過隱含參考轉換成B
的傳回型別,轉換為傳回型別B
的傳回型別。A
和B
是屬性,A
和B
的名稱相同,A
與B
具有相同的存取子(如果A
不屬於明確的介面成員實作,則允許有額外的存取子),而且A
的類型可通過身分轉換轉為B
的傳回類型,或者,如果A
是只讀屬性,則隱含參考轉換。A
和B
是事件,而A
和B
的名稱和類型相同。A
和B
是索引器,A
和B
的正式參數清單相同,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.M
或 I2.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 如何在舊版的語言中運作?
設計會議
- 一些在 https://github.com/dotnet/roslyn/issues/357的討論。
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-08.md
- 正在進行非線上討論,以決定是否只在 C# 9.0 中支援覆寫類別方法。