共用方式為


介面中的靜態抽象成員

注意

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

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

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

總結

介面被允許指定抽象的靜態成員,實作此介面的類別和結構必須提供明確或隱含的實作。 成員可以通過受介面限制的類型參數來存取。

動機

目前沒有任何方法可以抽象化靜態成員,並撰寫一般化程序代碼,以跨定義這些靜態成員的類型套用。 對於只 以靜態形式存在的成員類型而言,這特別有問題,尤其是運算符。

這項功能可讓泛型演算法超過數值類型,由指定指定運算符存在之介面條件約束表示。 因此,演算法可以用這類運算符來表示:

// 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

靜態介面成員目前是隱含的非虛擬成員,不允許 abstractvirtualsealed 修飾詞。

建議

抽象靜態成員

除欄位外,允許靜態介面成員也具有 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;
}

語義學

操作員限制

今天,所有一元運算符和二元運算符宣告都有一些需求,其中至少有一個操作數的類型為 TT?,其中 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 用戶定義的隱含轉換

下列項目符號

  • 判斷類型 SS₀T₀
    • 如果 E 具有類型,請讓 S 為該類型。
    • 如果 ST 是可為 Null 的實值型別,請讓 SᵢTᵢ 成為其基礎類型,否則讓 SᵢTᵢ 成為 ST
    • 如果 SᵢTᵢ 是類型參數,請讓 S₀T₀ 成為其有效的基類,否則分別讓 S₀T₀ 分別 SₓTᵢ
  • 尋找一組類型,D,其中將考慮使用者定義轉換運算符。 此集合包含 S0(如果 S0 為類別或結構)、S0 的基類(如果 S0 為類別),以及 T0(如果 T0 為類別或結構)。
  • 尋找一組適用的使用者自訂與提升的轉換運算子,U。 此集合包含由 D 中的類別或結構宣告的使用者定義和提升的隱含轉換運算子,這些運算子將從包含 S 的類型轉換為由 T所包含的類型。 如果 U 是空的,則轉換未定義,而且會發生編譯時期錯誤。

會依照下列方式進行調整:

  • 判斷類型 SS₀T₀
    • 如果 E 具有類型,請讓 S 為該類型。
    • 如果 ST 是可為 Null 的實值型別,則讓 SᵢTᵢ 為其基礎類型,否則讓 SᵢTᵢ 分別為 ST
    • 如果 SᵢTᵢ 是類型參數,請讓 S₀T₀ 成為其有效的基類,否則分別讓 S₀T₀ 分別 SₓTᵢ
  • 尋找一組適用的使用者定義和提升轉換運算元,U
    • 尋找一組類型 D1,這些類型中將會考慮使用者定義的轉換運算子。 此集合包含 S0(如果 S0 為類別或結構)、S0 的基類(如果 S0 為類別),以及 T0(如果 T0 為類別或結構)。
    • 尋找一組適用的使用者定義和提升轉換運算元,U1。 此集合包含由 D1 中的類別或結構所宣告的用戶定義和提升至隱式的轉換運算符,這些運算符可將包含 S 的類型轉換為被 T包含的類型。
    • 如果 U1 不是空的,則 UU1。 否則
      • 找出包含使用者定義轉換運算子的類型集合 D2。 此集合包含 Sᵢ有效的介面集 及其基底介面(如果 Sᵢ 為類型參數),以及 Tᵢ有效的介面集(如果 Tᵢ 為類型參數)。
      • 尋找一組適用的使用者定義和提升轉換運算元,U2。 這個集合由介面 D2 所宣告的使用者定義和提升的隱含轉換運算子組成,這些運算子會將包含 S 的類型轉換為被 T所包含的類型。
      • 如果 U2 不是空的,則 UU2
  • 如果 U 是空的,則轉換未定義,而且會發生編譯時期錯誤。

•10.3.9 使用者定義的明確轉換

以下項目符號

  • 判斷類型 SS₀T₀
    • 如果 E 具有類型,請讓 S 為該類型。
    • 如果 ST 是可為 Null 的實值型別,請讓 SᵢTᵢ 為其基礎類型,否則讓 SᵢTᵢ 分別為 ST
    • 如果 SᵢTᵢ 是類型參數,請讓 S₀T₀ 成為其有效的基類,否則分別讓 S₀T₀ 分別 SᵢTᵢ
  • 尋找一組類型集合 D,用於考慮使用者定義的轉換運算子。 此集合包含 S0(如果 S0 是類別或結構)、S0 的基類(如果 S0 為類別)、T0(如果 T0 為類別或結構),以及 T0 的基類(如果 T0 為類別)。
  • 尋找一個適用的使用者定義和升級轉換運算子的集合,U。 這個集合包括由類別或結構在 D 中所宣告的使用者定義和提升的隱含或明確轉換運算符,用於從包含或被 S 包含的類型轉換成包含或被 T包含的類型。 如果 U 是空的,則轉換未定義,而且會發生編譯時期錯誤。

將依照下列方式進行調整:

  • 判斷類型 SS₀T₀
    • 如果 E 具有類型,請讓 S 為該類型。
    • 如果 ST 是可為 Null 的值類型,則 SᵢTᵢ 應是其基礎類型,否則 SᵢTᵢ 應分別是 ST
    • 如果 SᵢTᵢ 是類型參數,請讓 S₀T₀ 成為其有效的基類,否則分別讓 S₀T₀ 分別 SᵢTᵢ
  • 尋找一組適用的使用者定義和提升轉換運算元,U
    • 尋找一組類型,D1,其中將考慮使用者定義轉換運算符。 此集合包含 S0(如果 S0 是類別或結構)、S0 的基類(如果 S0 為類別)、T0(如果 T0 為類別或結構),以及 T0 的基類(如果 T0 為類別)。
    • 尋找一組適用的使用者定義和提升轉換運算元,U1。 這個集合包含由類別或結構在 D1 中宣告的使用者定義和提升的隱含或明確轉換運算符,這些運算符負責將類型從包含或包含於 S 的類型轉換為包含或包含於 T的類型。
    • 如果 U1 不是空的,則 UU1。 否則
      • 尋找類型集 D2,從中將考慮使用者定義的轉換運算符。 此集合包含 Sᵢ有效的介面集 及其基底介面(如果 Sᵢ 為類型參數),以及 Tᵢ有效的介面集 及其基底介面(如果 Tᵢ 為類型參數)。
      • 尋找一組適用的使用者定義和提升轉換運算元,U2。 這個集合由介面在 D2 中所宣告的使用者定義和提升的隱含或明確轉換運算子組成,這些運算子將類型從包括或被 S 包含的類型轉換為包括或被 T包含的類型。
      • 如果 U2 不是空的,則 UU2
  • 如果 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/5783https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes

設計會議