閉じた階層

チャンピオン号: 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
}; 

モチベーション

多くのクラス型は、作成者以外の誰もが拡張することを意図していませんが、言語はその意図を表現する方法を提供しません。 クラスのコンシューマーの場合、派生クラスのセットが基底クラスを "枯渇" と見なさないことを意味し、警告を回避するためにスイッチ式にキャッチオール ケースを含める必要があります。

閉じたクラスは、一連の派生クラスが完全であることを示す方法を提供し、コードを使用してスイッチ式の網羅性に依存できるようにします。

詳細な設計

構文

クラスの修飾子として closed を許可します。 closed クラスは暗黙的に抽象です。 したがって、 sealed または static 修飾子を持つことはできません。

closed クラスでabstract修飾子を明示的に使用するとエラーになります。

閉じたクラスから派生するクラスは、明示的に宣言されていない限り、それ自体は閉じ ません

同じアセンブリの制限

あるアセンブリ内のクラスが 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) インターフェイス型パラメーターが共変または反変である可能性があるため、ある時点で閉じたインターフェイスを許可する場合、この規則では不十分な場合があります。 このような時点で、閉じた基本型のジェネリックインスタンス化ごとに、特定の派生型のジェネリックインスタンス化が 1 つだけになるように、ルールを調整する必要があります。

スイッチの網羅性

閉じたクラスのすべての直接の子孫を処理する 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>十分に分析せず、可能なすべてのXU2の制約に違反することを実現する。 そのため、一部の 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. 基本型に元の定義C₀を持つサブタイプ宣言S₀ごとに、基本型Cを持つ構築Sが存在するかどうかを判断します。
  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とそのサブタイプによって実装されたインターフェイスのセットを再帰的に収集することで、CからIへの明示的な参照変換が存在するかどうかを判断します。 インターフェイスのセットに Iが含まれており、 CIを実装していない場合は、 C から Iへの明示的な参照変換が存在します。 ( CIを実装する場合は、代わりに暗黙的な参照変換を使用できます)。

引き下げ

閉じたクラスは、 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) { }
}

"必須メンバー" 機能とは異なり、CompilerFeatureRequiredAttribute に加えて ObsoleteAttribute は生成されないことに注意してください。 後者のみが出力されます。

Multiple CompilerFeatureRequiredAttributes

次のようなシナリオでは、コンパイラはシンボルに関連するすべての必要な機能について、個別の 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 修飾子を追加したり、閉じたクラスから派生クラスを追加したりするのは破壊的変更です。 閉じたクラスを公開する前に、作成者はコンシューマーとの長期的なコントラクトを考慮する必要があります。

代替案

  • 新しい closed 修飾子の代わりに、閉じたクラスを [Closed] 属性で指定できます。
  • 子孫が許可される範囲は、さらにファイルに絞り込んだり (C# ではあまり優先順位がありません)、または閉じたクラスの本体内に入れ子になったクラスとして絞り込んだりすることができます。
  • 許可された子孫の閉じたセットは、宣言が行われる場所によって暗黙的に指定されるのではなく、リストとして指定できます。 これにより、他のアセンブリにクラスを含めることができるようになります。

オプション機能

  • インターフェイスを閉じることもできます。 ルールは非常によく似ています。

質問を開く

N/A