介面中的靜態抽象成員
注意
本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。
功能規格與已完成實作之間可能有一些差異。 這些差異是在相關 語言設計會議(LDM)的筆記中擷取的。
總結
介面被允許指定抽象的靜態成員,實作此介面的類別和結構必須提供明確或隱含的實作。 成員可以通過受介面限制的類型參數來存取。
動機
目前沒有任何方法可以抽象化靜態成員,並撰寫一般化程序代碼,以跨定義這些靜態成員的類型套用。 對於只 以靜態形式存在的成員類型而言,這特別有問題,尤其是運算符。
這項功能可讓泛型演算法超過數值類型,由指定指定運算符存在之介面條件約束表示。 因此,演算法可以用這類運算符來表示:
// Interface specifies static properties and operators
interface IAddable<T> where T : IAddable<T>
{
static abstract T Zero { get; }
static abstract T operator +(T t1, T t2);
}
// Classes and structs (including built-ins) can implement interface
struct Int32 : …, IAddable<Int32>
{
static Int32 IAddable.operator +(Int32 x, Int32 y) => x + y; // Explicit
public static int Zero => 0; // Implicit
}
// Generic algorithms can use static members on T
public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
T result = T.Zero; // Call static operator
foreach (T t in ts) { result += t; } // Use `+`
return result;
}
// Generic method can be applied to built-in and user-defined types
int sixtyThree = AddAll(new [] { 1, 2, 4, 8, 16, 32 });
語法
介面成員
此功能可讓靜態介面成員宣告為虛擬。
C# 11 之前的規則
在 C# 11 之前,介面中的實例成員會隱含抽象化(或虛擬,如果有預設實作),但可以選擇性地擁有 abstract
(或 virtual
) 修飾詞。 非虛擬實例成員必須明確標示為 sealed
。
靜態介面成員目前是隱含的非虛擬成員,不允許 abstract
、virtual
或 sealed
修飾詞。
建議
抽象靜態成員
除欄位外,允許靜態介面成員也具有 abstract
修飾詞。 抽象靜態成員不允許有主體(或在屬性的情況下,不允許存取子有主體)。
interface I<T> where T : I<T>
{
static abstract void M();
static abstract T P { get; set; }
static abstract event Action E;
static abstract T operator +(T l, T r);
static abstract bool operator ==(T l, T r);
static abstract bool operator !=(T l, T r);
static abstract implicit operator T(string s);
static abstract explicit operator string(T t);
}
虛擬靜態成員
允許欄位以外的靜態介面成員也具有 virtual
修飾詞。 虛擬靜態成員必須有主體。
interface I<T> where T : I<T>
{
static virtual void M() {}
static virtual T P { get; set; }
static virtual event Action E;
static virtual T operator +(T l, T r) { throw new NotImplementedException(); }
}
非虛擬的明確靜態成員
為了與非虛擬實例成員對稱,即使靜態成員(欄位除外)預設為非虛擬,仍然應該允許使用選擇性的 sealed
修飾詞:
interface I0
{
static sealed void M() => Console.WriteLine("Default behavior");
static sealed int f = 0;
static sealed int P1 { get; set; }
static sealed int P2 { get => f; set => f = value; }
static sealed event Action E1;
static sealed event Action E2 { add => E1 += value; remove => E1 -= value; }
static sealed I0 operator +(I0 l, I0 r) => l;
}
介面成員的實作
今天的規則
類別和結構可以隱含或明確實作介面的抽象實例成員。 類別或結構中的隱含實作介面成員是一般(虛擬或非虛擬)成員宣告,它恰好也實作了介面成員。 成員甚至可以繼承自基類,因此甚至不存在於類別宣告中。
明確實作的介面成員會使用限定名稱來識別相關的介面成員。 實作的內容無法作為類別或結構中的成員直接存取,必須透過介面存取。
建議
類別和結構中不需要新的語法,以利靜態抽象介面成員的隱含實作。 現有的靜態成員宣告用來達成此目的。
靜態抽象介面成員的明確實作會使用限定的名稱以及 static
修飾詞。
class C : I<C>
{
string _s;
public C(string s) => _s = s;
static void I<C>.M() => Console.WriteLine("Implementation");
static C I<C>.P { get; set; }
static event Action I<C>.E // event declaration must use field accessor syntax
{
add { ... }
remove { ... }
}
static C I<C>.operator +(C l, C r) => new C($"{l._s} {r._s}");
static bool I<C>.operator ==(C l, C r) => l._s == r._s;
static bool I<C>.operator !=(C l, C r) => l._s != r._s;
static implicit I<C>.operator C(string s) => new C(s);
static explicit I<C>.operator string(C c) => c._s;
}
語義學
操作員限制
今天,所有一元運算符和二元運算符宣告都有一些需求,其中至少有一個操作數的類型為 T
或 T?
,其中 T
是封入類型的實例類型。
這些需求需要放寬,以便允許受限操作數是被視為「封閉類型的實例類型」的型別參數。
為了讓類型參數 T
計算為「封入類型的實例類型」,它必須符合下列需求:
-
T
是運算符宣告發生之介面上的直接型別參數, -
T
直接 受規格所呼叫的「實例類型」所限制,也就是周圍介面及其本身的類型參數作為類型自變數。
相等運算子和轉換
介面中將允許 ==
和 !=
運算子的抽象/虛擬宣告,以及隱含和明確轉換運算符的抽象/虛擬宣告。 衍生介面也會允許實作它們。
對於 ==
和 !=
運算子,至少有一個參數類型必須是計入為「封閉類型的實例類型」的類型參數,如上一節中所定義。
實作靜態抽象成員
在類別或結構中,當靜態成員宣告被視為實作靜態抽象介面成員時,其相關規則及適用需求,與實例成員的情況相同。
TBD: 我們可能尚未想到這裡需要的額外或不同的規則。
介面作為類型參數
我們已討論 https://github.com/dotnet/csharplang/issues/5955 所引發的問題,並決定新增介面作為類型自變數的使用限制(https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts)。 以下是 https://github.com/dotnet/csharplang/issues/5955 提出並由 LDM 核准的限制。
不含最特定實作的靜態抽象或虛擬成員之介面,或繼承此類成員之介面,不能作為型別參數使用。 如果所有靜態抽象/虛擬成員都有最特定的實作,介面就可以當做類型自變數使用。
存取靜態抽象介面成員
當 T
受到介面 I
限制,而且 M
是可存取 I
的可存取靜態抽象成員時,可以使用 T.M
表達式參數 T
存取靜態介面成員 M
。
T M<T>() where T : I<T>
{
T.M();
T t = T.P;
T.E += () => { };
return t + T.P;
}
在執行階段,所使用的實際成員實作是存在於作為類型參數提供的實際類型上。
C c = M<C>(); // The static members of C get called
由於查詢表達式是以語法重寫的形式進行規格,因此 C# 實際上可讓您使用 類型 作為查詢來源,只要它具有您使用之查詢運算符的靜態成員! 換句話說,如果 語法 適合,我們就允許! 我們認為此行為在原始 LINQ 中不是刻意或重要的,而且我們不想在類型參數上執行支援它的工作。 如果有情境,我們將聽到相關資訊,並可以選擇稍後考慮接納這些情境。
變異數安全 §18.2.3.2
差異安全性規則應該套用至靜態抽象成員的簽章。 應調整 https://github.com/dotnet/csharplang/blob/main/proposals/variance-safety-for-static-interface-members.md#variance-safety 中提出的新增內容
這些限制不適用於靜態成員宣告內的類型出現次數。
至
這些限制不適用於宣告中 非虛擬、非抽象 靜態成員的類型。
•10.5.4 用戶定義的隱含轉換
下列項目符號
- 判斷類型
S
、S₀
和T₀
。- 如果
E
具有類型,請讓S
為該類型。 - 如果
S
或T
是可為 Null 的實值型別,請讓Sᵢ
和Tᵢ
成為其基礎類型,否則讓Sᵢ
和Tᵢ
成為S
和T
。 - 如果
Sᵢ
或Tᵢ
是類型參數,請讓S₀
和T₀
成為其有效的基類,否則分別讓S₀
和T₀
分別Sₓ
和Tᵢ
。
- 如果
- 尋找一組類型,
D
,其中將考慮使用者定義轉換運算符。 此集合包含S0
(如果S0
為類別或結構)、S0
的基類(如果S0
為類別),以及T0
(如果T0
為類別或結構)。 - 尋找一組適用的使用者自訂與提升的轉換運算子,
U
。 此集合包含由D
中的類別或結構宣告的使用者定義和提升的隱含轉換運算子,這些運算子將從包含S
的類型轉換為由T
所包含的類型。 如果U
是空的,則轉換未定義,而且會發生編譯時期錯誤。
會依照下列方式進行調整:
- 判斷類型
S
、S₀
和T₀
。- 如果
E
具有類型,請讓S
為該類型。 - 如果
S
或T
是可為 Null 的實值型別,則讓Sᵢ
和Tᵢ
為其基礎類型,否則讓Sᵢ
和Tᵢ
分別為S
和T
。 - 如果
Sᵢ
或Tᵢ
是類型參數,請讓S₀
和T₀
成為其有效的基類,否則分別讓S₀
和T₀
分別Sₓ
和Tᵢ
。
- 如果
- 尋找一組適用的使用者定義和提升轉換運算元,
U
。- 尋找一組類型
D1
,這些類型中將會考慮使用者定義的轉換運算子。 此集合包含S0
(如果S0
為類別或結構)、S0
的基類(如果S0
為類別),以及T0
(如果T0
為類別或結構)。 - 尋找一組適用的使用者定義和提升轉換運算元,
U1
。 此集合包含由D1
中的類別或結構所宣告的用戶定義和提升至隱式的轉換運算符,這些運算符可將包含S
的類型轉換為被T
包含的類型。 - 如果
U1
不是空的,則U
為U1
。 否則- 找出包含使用者定義轉換運算子的類型集合
D2
。 此集合包含Sᵢ
有效的介面集 及其基底介面(如果Sᵢ
為類型參數),以及Tᵢ
有效的介面集(如果Tᵢ
為類型參數)。 - 尋找一組適用的使用者定義和提升轉換運算元,
U2
。 這個集合由介面D2
所宣告的使用者定義和提升的隱含轉換運算子組成,這些運算子會將包含S
的類型轉換為被T
所包含的類型。 - 如果
U2
不是空的,則U
是U2
- 找出包含使用者定義轉換運算子的類型集合
- 尋找一組類型
- 如果
U
是空的,則轉換未定義,而且會發生編譯時期錯誤。
•10.3.9 使用者定義的明確轉換
以下項目符號
- 判斷類型
S
、S₀
和T₀
。- 如果
E
具有類型,請讓S
為該類型。 - 如果
S
或T
是可為 Null 的實值型別,請讓Sᵢ
和Tᵢ
為其基礎類型,否則讓Sᵢ
和Tᵢ
分別為S
和T
。 - 如果
Sᵢ
或Tᵢ
是類型參數,請讓S₀
和T₀
成為其有效的基類,否則分別讓S₀
和T₀
分別Sᵢ
和Tᵢ
。
- 如果
- 尋找一組類型集合
D
,用於考慮使用者定義的轉換運算子。 此集合包含S0
(如果S0
是類別或結構)、S0
的基類(如果S0
為類別)、T0
(如果T0
為類別或結構),以及T0
的基類(如果T0
為類別)。 - 尋找一個適用的使用者定義和升級轉換運算子的集合,
U
。 這個集合包括由類別或結構在D
中所宣告的使用者定義和提升的隱含或明確轉換運算符,用於從包含或被S
包含的類型轉換成包含或被T
包含的類型。 如果U
是空的,則轉換未定義,而且會發生編譯時期錯誤。
將依照下列方式進行調整:
- 判斷類型
S
、S₀
和T₀
。- 如果
E
具有類型,請讓S
為該類型。 - 如果
S
或T
是可為 Null 的值類型,則Sᵢ
和Tᵢ
應是其基礎類型,否則Sᵢ
和Tᵢ
應分別是S
和T
。 - 如果
Sᵢ
或Tᵢ
是類型參數,請讓S₀
和T₀
成為其有效的基類,否則分別讓S₀
和T₀
分別Sᵢ
和Tᵢ
。
- 如果
- 尋找一組適用的使用者定義和提升轉換運算元,
U
。- 尋找一組類型,
D1
,其中將考慮使用者定義轉換運算符。 此集合包含S0
(如果S0
是類別或結構)、S0
的基類(如果S0
為類別)、T0
(如果T0
為類別或結構),以及T0
的基類(如果T0
為類別)。 - 尋找一組適用的使用者定義和提升轉換運算元,
U1
。 這個集合包含由類別或結構在D1
中宣告的使用者定義和提升的隱含或明確轉換運算符,這些運算符負責將類型從包含或包含於S
的類型轉換為包含或包含於T
的類型。 - 如果
U1
不是空的,則U
為U1
。 否則- 尋找類型集
D2
,從中將考慮使用者定義的轉換運算符。 此集合包含Sᵢ
有效的介面集 及其基底介面(如果Sᵢ
為類型參數),以及Tᵢ
有效的介面集 及其基底介面(如果Tᵢ
為類型參數)。 - 尋找一組適用的使用者定義和提升轉換運算元,
U2
。 這個集合由介面在D2
中所宣告的使用者定義和提升的隱含或明確轉換運算子組成,這些運算子將類型從包括或被S
包含的類型轉換為包括或被T
包含的類型。 - 如果
U2
不是空的,則U
是U2
- 尋找類型集
- 尋找一組類型,
- 如果
U
是空的,則轉換未定義,而且會發生編譯時期錯誤。
默認實作
在此提案中,額外的 功能是允許介面中的靜態虛擬成員像實例虛擬/抽象成員一樣具有預設實作。
此處的一個複雜之處是預設實作會想要以「虛擬」方式呼叫其他靜態虛擬成員。 允許在介面上直接呼叫靜態虛擬成員,會需要傳遞一個隱藏的類型參數,代表著「自我」類型,即當前被真正叫用的靜態方法所使用的類型。 這似乎很複雜、昂貴且可能令人困惑。
我們討論了一個更簡單的版本,其維持了目前提案中的限制:靜態虛擬成員只能在類型參數上調用。 由於具有靜態虛擬成員的介面通常會有代表「自我」類型的明確型別參數,所以這不會造成太大損失:其他靜態虛擬成員只能在該自我類型上呼叫。 這個版本比較簡單,而且似乎相當可行。
在 https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md#default-implementations-of-abstract-statics,我們決定支持靜態成員的預設實作,並遵循和拓展 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/default-interface-methods.md 中所建立的規則。
模式比對
考慮以下程式代碼,使用者可能合理地預期它會列印「True」(就如同在內嵌寫入常數模式時所示):
M(1.0);
static void M<T>(T t) where T : INumberBase<T>
{
Console.WriteLine(t is 1); // Error. Cannot use a numeric constant
Console.WriteLine((t is int i) && (i is 1));
}
不過,由於模式的輸入類型不是 double
,因此常數 1
模式會先對傳入的 T
進行類型檢查,確認是否符合 int
。 這不直觀,因此會封鎖它,直到未來的 C# 版本針對衍生自 INumberBase<T>
的類型,新增較佳的數值比對處理。 為此,我們會說,我們將明確辨認 INumberBase<T>
為所有「數字」都會衍生自的類型,如果我們嘗試將數值常數模式與無法呈現該模式的數字類型相符,就會封鎖該模式(例如,限制為 INumberBase<T>
的類型參數,或繼承自 INumberBase<T>
的使用者定義數字類型)。
正式來說,我們會將常數模式中 與 模式相容的例外新增至定義中:
常數模式會針對常數值測試表達式的值。 常數可以是任何常數表達式,像是字面常數、宣告
const
變數的名稱或列舉常數。 當輸入值不是開啟類型時,常數表達式會隱含轉換成相符表達式的類型;如果輸入值的類型與常數表達式的類型 模式相容,則模式比對作業是錯誤的。 如果所比對的常數運算式是數值,且輸入值是繼承自System.Numerics.INumberBase<T>
的類型,並且無法將常數運算式轉換為輸入值的型別,則模式比對會發生錯誤。
我們也為關係型模式新增類似的例外狀況:
當輸入的類型有定義適合的內建二元關係運算符時,且該運算符適用於以輸入為左操作數和以指定常數為右操作數,則該運算符的評估被視為關係模式的意義。 否則,我們會使用明確的可為空值或拆箱轉換,將輸入轉換成表達式的類型。 如果沒有任何這類轉換存在,則為編譯時期錯誤。 如果輸入類型是受限於 的類型參數,或是繼承自
System.Numerics.INumberBase<T>
的類型,而且輸入類型沒有定義適當的內建二進位關係運算符,則為編譯時期錯誤。 如果轉換失敗,模式會被視為不相符。 如果轉換成功,則模式比對作業的結果是評估表達式 e OP v 的結果,其中 e 是轉換的輸入,OP 是關係運算符,而 v 是常數運算式。
缺點
- 「靜態抽象」是新的概念,且會有意義地新增至 C# 的概念負載。
- 要開發這項功能並不便宜。 我們應該確保這是值得的。
替代方案
結構限制
另一種方法是直接具有「結構條件約束」,並明確要求在型別參數上存在特定運算符。 其缺點如下: - 這部分每次都必須重複撰寫。 使用具名的約束條件似乎更好。 - 這是一種全新的條件約束,而建議的功能則利用介面條件約束的現有概念。 - 它只適用於運算符,而不容易適用於其他類型的靜態成員。
未解決的問題
靜態抽象介面和靜態類別
如需詳細資訊,請參閱 https://github.com/dotnet/csharplang/issues/5783 和 https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes。
設計會議
- https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-02-08.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-04-05.md
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-06-29.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-04-06.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-06-06.md