Закрытые иерархии

Вопрос чемпиона: 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
}; 

Мотивация

Многие типы классов не предназначены для расширения кем-либо, но их авторов, но язык не дает никакого способа выразить это намерение, не говоря уже о том, чтобы защитить от него происходит. Для потребителей класса это означает, что набор производных классов не будет считаться "исчерпанием" базового класса, а выражение коммутатора должно включать регистр catch-all, чтобы избежать предупреждений.

Закрытые классы позволяют указать, что набор производных классов завершен и позволяет использовать код для обеспечения исчерпывающего объема выражений коммутатора.

Подробный дизайн

Синтаксис

Разрешить closed в качестве модификатора для классов. Класс closed неявно абстрагируется. Таким образом, он также не может иметь sealed или 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

Это правило заключается в том, чтобы обеспечить единый универсальный экземпляр производного типа, который "исчерпает" заданный универсальный экземпляр закрытого базового типа.

Примечание: Это правило может быть недостаточно, если мы разрешаем закрытые интерфейсы в какой-то момент, так как классы могут реализовывать несколько универсальных экземпляров одного интерфейса, а параметры типа интерфейса 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 { }

Мы определяем, существует ли явное преобразование CI ссылок, рекурсивно собирая набор интерфейсов, реализованных C и его подтипами. Если набор интерфейсов включает в себя Iи не реализуетI, то явное преобразование ссылок существует в CI.C (В случае реализации 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"
}

Метаданные "view" ( C1Представление):

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

Обратите внимание, что в отличие от функции "обязательных элементов", устаревшаяAttribute не создается в дополнение к компилятору CompilerFeatureRequiredAttribute. Создается только последний.

Несколько компиляторовFeatureRequiredAttributes

В следующем сценарии компилятор будет выдавать отдельную 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#) или внутри тела закрытого класса как вложенные классы.
  • Закрытый набор разрешенных потомков может быть предоставлен в виде списка вместо подразумеваемого объявления. Это позволит включить классы в другие сборки.

Дополнительные функции

  • Интерфейсы также могут быть закрыты. Правила будут очень похожи.

Открытые вопросы

N/A