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 集合由以下方式決定:
- 對於給定的閉型態
C,設C₀為其原始定義。 - 對於每個其基礎型態具有原始定義
S₀的子型態宣告C₀,判定是否S存在一個構造使其基型C為 。- 另見 §19.6.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# 中這在先例不多)或封閉類別的內部巢狀類別。
- 允許的閉後代集合可以以列表形式給出,而非由宣告位置隱含。 這將允許將類別納入其他組裝。
選用功能
- 介面也可以被允許關閉。 規則會非常相似。
未解決的問題
不適用