封閉階層

Note

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

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

您可以在 規範的文章中深入瞭解將功能規範納入 C# 語言標準的過程。

Champion 期數:https://github.com/dotnet/csharplang/issues/9499

總結

允許宣告 closed一個類別。 這可防止直接衍生類別在不同組合中宣告:

// Assembly 1
public closed record class GateState;
public record class Closed : GateState;
public record class Open(float Percent) : GateState;

// Assembly 2
public record class Locked : GateState; // ERROR - 'GateState' is a closed class

由於所有導出類別皆在封閉類別的組合中宣告,因此可以推導出涵蓋所有類別的消耗 switch 式來「耗盡」封閉類別——無需提供預設情況以避免警告。

// Assembly 3
GateState state = ...;
string description = state switch
{
    Closed => "closed",
    Open(var percent) => $"{percent}% open"
    // No warning about missing cases
}; 

動機

許多類別類型本不打算由作者自行擴充,但語言中既無法表達這種意圖,更不用說防止這種意圖發生。 對於類別的消費者來說,這表示不會考慮任何衍生類別來「耗盡」基底類別,且交換式表達式需要包含一個籠統案例以避免警告。

封閉類別提供了一種表示導出類別集合已完成的方法,並允許使用者在切換表達式中依賴此資料來取得完整內容。

詳細設計

Syntax

允許 closed 作為職業的修正值。 類別 closed 是隱含抽象的。 因此,它不能同時帶有 sealed a 或 static 修飾詞。

明確使用 abstract 修飾符在類別上 closed 是錯誤的。

從封閉類別衍生的類別本身 並非 封閉類別,除非明確宣告為封閉類別。

同組限制

如果一個組件中宣告 closed 了類別,那麼直接從該類別推導到另一個組件是錯誤的:

// Assembly 1
public closed class CC { ... } 
public class CO : CC { ... }     // Ok, same assembly

// Assembly 2
public class C1 : CC { ... }     // Error, 'CC' is closed and in a different assembly
public class C2 : CO { ... }     // Ok, 'CO' is not closed

模組也有同樣的限制。 某型的 closed 子型別必須位於與基型相同的模組內。

型別參數限制

如果一個泛型類別直接衍生自封閉類別,則其所有型別參數必須在基底類別規範中使用:

closed class C<T> { ... }
class D1<U> : C<U> { ... }   // Ok, 'U' is used in base class
class D2<V> : C<V[]> { ... } // Ok, 'V' is used in base class
class D3<W> : C<int> { ... } // Error, 'W' is not used in base class

此規則旨在確保導出型別存在單一通用實例,能「耗盡」封閉基底型別的通用實例。

註: 如果我們允許封閉介面,這條規則可能不夠,因為 a) 類別可以實作同一介面的多個通用實例,b) 介面類型參數可以是共變或逆變的。 此時我們需要精煉規則,確保每一個封閉基底型別的通用實例化,每個派生型別的通用實例化都只會有一個。

開關的徹底性

switch一個處理封閉類別所有直系後代的表達式,將被視為已耗盡該類別。 這表示部分非完整性的警告將不再顯示:

CC cc = ...;
_ = cc switch
{
    CO co => ...,
    // No warning about non-exhaustive switch
};

另一方面,這也意味著閉基底類別出現在所有直系後代之後,可能是錯誤:

_ = cc switch
{
    CO co => ...,
    CC cc => ..., // Error, case cannot be reached
};

註: 某些封閉基底類別的泛型實例可能不存在有效的導出類別。 窮盡交換器只需指定實際可行的導出型態情況。

例如:

closed class C<T> { ... }
class D1<U> : C<U> { ... }
class D2<V> : C<V[]> { ... }

對於 C<string>,例如,不存在對應的實例 D2<...>化 ,且在開關中也不需要給出 的情況 D2<...> :

C<string> cs = ...;
_ = cs switch
{
    D1<string> d1 => ...,
    // No need for a 'D2<...>' case - no instantiation corresponds to 'C<string>'
}

當某子類型無法使用時,則是徹底的

如果某子型別因限制違規、可及性違規或其他原因在特定使用地點不有效,則無法透過子類型來完成切換。

closed class C;
class D1 : C;
class Container
{
    protected class D2 : C;
}

class Program
{
    int M(C c)
        => c switch
        {
            D1 => 1,
            // warning: switch is non-exhaustive. Pattern 'C' is not handled.
        };
}

當泛型子型別不可 言說時,這同樣適用,且其適用性可能取決於最終型別的參數替換。

closed class C<T> { ... }
class D1<U> : C<U> { ... }
class D2<V> : C<V[]> { ... }

class Program
{
    int M<X>(C<X> c)
        => c switch
        {
            D1<X> => 1,
            // warning: switch is non-exhaustive. Pattern 'C' is not handled.
        };
}

子型限制不影響完整性

該語言並未根據基礎型態與子型別定義中對型別參數的限制,細化判斷子型態是否可行。

closed class C<T>;
class D1<U1> : C<U1>;
class D2<U2> : C<U2> where U2 : struct;

class Program
{
    int M1<X>(C<X> c) where X : class
    {
        // warning: switch is not exhaustive. Pattern 'C<X>' is not handled.
        return c switch
        {
            D1<X> => 1,
        };
    }

    int M2<X>(C<X> c) where X : class
    {
        return c switch
        {
            D1<X> => 1,
            C<X> => 2, // ok
        };
    }
}

例如,上述開關表達式並未對構造D2<X>進行足夠精確的分析,無法意識到所有可能X的限制都違反了。U2 因此,它假設有些 D2<X> 部分是可能的,並要求使用者透過窮盡基礎型態來處理。

當不存在子類型時,徹底性

當封閉類別沒有子類型時,空切換不被視為窮盡的。

備註:這假設在正常程式碼中屬於「中間狀態」。 作者很可能會在此情況下修改,宣告子類型。 這種行為相當於一種「怪癖」——儘管「所有 0 子型別都被處理」,語言仍要求使用者處理基底型別。

closed class C;

class Program
{
    int M1(C c)
        // warning: switch is not exhaustive.
        => c switch
        {
        };

    int M2(C c)
        => c switch
        {
            C => 1, // ok
        };
}

型別參數的窮盡性,僅限於閉型態

受限於封閉類別的型別參數,在窮舉性檢查中也被視為封閉類別。

closed class C;
class D1 : C;
class D2 : C;

class Program
{
    int M1<X>(X x) where X : C
        => x switch
        {
            D1 => 1,
            D2 => 2,
        };

    int M2<X>(X x) where X : C
        => x switch
        {
            D1 => 1,
            D2 => 2,
            C => 3, // error: 'C' is subsumed by the previous cases
        };
}

封閉類的子類型判定

開關在閉類類型上的全面性,是透過檢查該開關是否涵蓋輸入封閉類類型的 子類型集合 而決定。

封閉類別的子類型 S 集合由以下方式決定:

  1. 對於給定的閉型態 C,設 C₀ 為其原始定義。
  2. 對於每個其基礎型態具有原始定義S₀的子型態宣告C₀,判定是否S存在一個構造使其基型C為 。
  3. 若存在此類 , S 則包含在 子型集合中。

封閉類別的介面可轉換性

如果一個封閉類別的所有子類型都是密封的或有密封的階層,則稱為其有密封階層。 也就是說,擴展階層中的所有類別要麼封閉,要麼封閉。

當封閉類別的階層結構 封閉時,便會引入 介面可轉換 性限制。 這防止了嘗試轉換成介面型態,因為介面型別不可能成功。

此限制本質上類似於從封閉類別類型轉換為介面型態的 明確引用 轉換。 參見 §10.3.5 明確的參考轉換。

var c = new C();
var i = (I)c; // error

closed class C { }
sealed class D1 : C { }
sealed class D2 : C { }
interface I { }

我們透過遞迴收集由 C 及其子類型實作的介面集合,判斷從 到 的I顯式參考轉換C是否存在。 若介面集合包含 I,且 C 不實作 I,則存在從 C 到 I的明確參考轉換。 (若 C 實作 I,則可使用隱含參考轉換。)

降低

封閉類別會以 IsClosedType 屬性產生,以便被消耗型編譯器辨識。

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
    public sealed class IsClosedTypeAttribute : Attribute { }
}

阻擋來自其他語言/編譯器的子型別

封閉類不應繼承自不支援封閉類的語言。 這是透過在所有封閉類的建構子中加入 [CompilerFeatureRequired("ClosedClasses")] 來達成的。

// Authoring assembly, built with .NET 10 SDK
closed class C1
{
    public C1() { }
    public C1(int param) { }
}

// Consuming assembly, built with .NET 8 SDK
class C2 : C1
{
    public C2() { } // error: 'C1.C1()' requires compiler feature "ClosedClasses"
    public C2() : base(42) { } // error: 'C1.C1(int)' requires compiler feature "ClosedClasses"
}

元資料「視圖」為 C1:

[IsClosedType]
class C1
{
    [CompilerFeatureRequired("ClosedClasses")]
    public C1() { }
    [CompilerFeatureRequired("ClosedClasses")]
    public C1(int param) { }
}

請注意,與「必要成員」功能不同,ObsoleteAttribute 不會在 CompilerFeatureRequiredAttribute 之外同時輸出。 只有後者會被發射。

多個編譯器功能必需屬性

在如下情境下,編譯器會針對每個與符號相關的特徵分別發出 CompilerFeatureRequired一個 :

closed class C1
{
    public C() { }
    public required string P { get; set; }
}

// Metadata:
class C1
{
    [Obsolete("Types with required members are not supported in this version of your compiler")]
    [CompilerFeatureRequired("RequiredMembers")]
    [CompilerFeatureRequired("ClosedClasses")]
    public C1() { }
}

缺點

  • 它可以是破壞性變更,例如在現有類別中新增 closed 修飾符,或是從封閉類別新增衍生類別。 在發表封閉類別之前,作者需要考慮它與消費者所暗示的長期契約。

Alternatives

  • 關閉類別可closed指定屬性,取代新的[Closed]修飾符。
  • 後代允許的範圍可以進一步縮小到檔案(雖然在 C# 中這在先例不多)或封閉類別的內部巢狀類別。
  • 允許的閉後代集合可以以列表形式給出,而非由宣告位置隱含。 這將允許將類別納入其他組裝。

選用功能

  • 介面也可以被允許關閉。 規則會非常相似。

未解決的問題

不適用