Поделиться через


Статические абстрактные члены в интерфейсах

Заметка

Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки возможности. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.

Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия фиксируются в соответствующих собраниях по проектированию языка (LDM).

Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .

Выпуск чемпиона: https://github.com/dotnet/csharplang/issues/4436

Сводка

Интерфейс может указывать абстрактные статические члены, для которых классы и структуры, реализующие данный интерфейс, обязаны предоставить явную или неявную реализацию. Доступ к элементам можно получить вне параметров типа, ограниченных интерфейсом.

Мотивация

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

Эта функция позволяет универсальным алгоритмам работать с числовыми типами, задаваемыми ограничениями интерфейса, которые требуют наличие заданных операторов. Таким образом, алгоритмы могут быть выражены с точки зрения таких операторов:

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

Статические члены интерфейса сегодня неявно являются не виртуальными и не разрешают модификаторы abstract, virtual или sealed.

Предложение

Абстрактные статические элементы

Статические элементы интерфейса, отличные от полей, также могут иметь модификатор 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;
}

Семантика

Ограничения операторов

Сегодня все объявления унарных и бинарных операторов имеют некоторые требования, связанные по крайней мере с одним из их операндов типа T или T?, где T является типом экземпляра окружающего типа.

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

Чтобы параметр типа T считался "экземплярным типом заключающего типа", он должен соответствовать следующим требованиям:

  • T — это параметр прямого типа в интерфейсе, в котором происходит объявление оператора, и
  • T напрямую ограничен тем, что спецификация называет "тип экземпляра", т. е. окружающий интерфейс с собственными параметрами типа, используемыми в качестве аргументов типа.

Операторы и преобразования равенства

Абстрактные или виртуальные объявления операторов == и !=, а также абстрактные или виртуальные объявления неявных и явных операторов преобразования будут разрешены в интерфейсах. Производные интерфейсы также могут реализовывать их.

Для операторов == и != по крайней мере один тип параметра должен быть параметром типа, который рассматривается как "тип экземпляра охватывающего типа", как определено в предыдущем разделе.

Реализация статических абстрактных элементов

Правила, когда объявление статического члена в классе или структуре считается реализацией статического абстрактного члена интерфейса, а также требования, применяемые при этом, совпадают с правилами для членов экземпляра.

будет определено: могут быть дополнительные или другие правила, необходимые здесь, о которых мы еще не думали.

Интерфейсы в качестве аргументов типа

Мы обсудили проблему, вызванную 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.

Интерфейс, содержащий или наследующий статический абстрактный или виртуальный член, который не имеет самой конкретной реализации в интерфейсе, нельзя использовать в качестве аргумента типа. Если все статические абстрактные или виртуальные члены имеют наиболее конкретную реализацию, интерфейс можно использовать в качестве аргумента типа.

Доступ к статическим элементам абстрактного интерфейса

Доступ к элементу статического абстрактного интерфейса M можно получить в параметре типа T с помощью выражения T.M, если T ограничен интерфейсом I и M является доступным статическим абстрактным членом I.

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 Определяемые пользователем неявные преобразования

Следующие пункты списка

  • Определите типы S, S₀ и T₀.
    • Если E имеет тип, позвольте S быть этим типом.
    • Если S или T являются типами значений, допускающих значение NULL, пусть Sᵢ и Tᵢ будут их базовыми типами, в противном случае пусть Sᵢ и Tᵢ будут S и Tсоответственно.
    • Если Sᵢ или Tᵢ являются параметрами типа, позвольте S₀ и T₀ быть их эффективными базовыми классами, в противном случае позвольте S₀ и T₀ быть Sₓ и Tᵢсоответственно.
  • Найдите набор типов, D, из которых будут считаться определяемые пользователем операторы преобразования. Этот набор состоит из S0 (если S0 является классом или структурой), базовыми классами S0 (если S0 является классом) и T0 (если T0 является классом или структурой).
  • Найдите набор применимых пользовательских и поднятых операторов преобразования, U. Этот набор состоит из определяемых пользователем и снятых неявных операторов преобразования, объявленных классами или структурами в D, которые преобразуются из типа, охватывающего S в тип, охватываемый T. Если U пуст, преобразование не определено и возникает ошибка во время компиляции.

корректируются следующим образом:

  • Определите типы S, S₀ и T₀.
    • Если E имеет тип, позвольте S быть этим типом.
    • Если S или T являются типами значений, допускающих значение NULL, пусть Sᵢ и Tᵢ будут их базовыми типами, в противном случае пусть Sᵢ и Tᵢ будут S и Tсоответственно.
    • Если 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 не пуст, то U является U2
  • Если U пуст, преобразование не определено и возникает ошибка во время компиляции.

§10.3.9 определяемые пользователем явные преобразования

Следующие пункты списка

  • Определите типы S, S₀ и T₀.
    • Если E имеет тип, позвольте S быть этим типом.
    • Если S или T являются типами значений, допускающих значение NULL, пусть Sᵢ и Tᵢ будут их базовыми типами, в противном случае пусть Sᵢ и Tᵢ будут S и Tсоответственно.
    • Если Sᵢ или Tᵢ являются параметрами типа, позвольте S₀ и T₀ быть их эффективными базовыми классами, в противном случае позвольте S₀ и T₀ быть Sᵢ и Tᵢсоответственно.
  • Найдите набор типов, D, из которых будут считаться определяемые пользователем операторы преобразования. Этот набор состоит из S0 (если S0 является классом или структурой), базовыми классами S0 (если S0 является классом), T0 (если T0 является классом или структурой), а базовые классы T0 (если T0 является классом).
  • Найдите набор применимых пользовательских и поднятых операторов преобразования, U. Этот набор состоит из определяемых пользователем и снятых неявных или явных операторов преобразования, объявленных классами или структурами в D, которые преобразуются из типа, охватывающего или охватывающего S в тип, охватывающий или охватываемый T. Если U пуст, преобразование не определено и возникает ошибка во время компиляции.

корректируются следующим образом:

  • Определите типы S, S₀ и T₀.
    • Если E имеет тип, позвольте S быть этим типом.
    • Если S или T являются типами значений, допускающих значение NULL, пусть Sᵢ и Tᵢ будут их базовыми типами, в противном случае пусть Sᵢ и Tᵢ будут S и Tсоответственно.
    • Если 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 не пуст, то U является U2
  • Если U пуст, преобразование не определено и возникает ошибка во время компиляции.

Реализации по умолчанию

В этом предложении дополнительная функция состоит в том, чтобы позволить виртуальным статическим членам в интерфейсах иметь реализации по умолчанию, как и у экземплярных виртуальных или абстрактных членов.

Одним из осложнений здесь является то, что реализации по умолчанию хотели бы вызывать другие статические виртуальные члены "виртуально". Чтобы статические виртуальные члены могли вызываться непосредственно на интерфейсе, требуется передавать скрытый параметр типа, представляющий тип "self", на котором действительно был вызван тот статический метод. Это кажется сложным, дорогим и потенциально запутанным.

Мы обсудили более простую версию, которая поддерживает ограничения текущего предложения, что статические виртуальные члены могут вызывать только для параметров типа. Так как интерфейсы со статическими виртуальными элементами часто имеют явный параметр типа, представляющий тип self, это не будет большой потерей: другие статические виртуальные члены могут вызываться только для этого типа. Эта версия значительно проще и кажется вполне выполнимой.

На 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>, и отсутствует константное преобразование из константного выражения в тип входного значения, операция сопоставления шаблонов считается ошибкой.

Мы также добавим аналогичное исключение для реляционных шаблонов:

Если входные данные являются типом, для которого определен подходящий встроенный двоичный реляционный оператор, который применим к входным данным в качестве левого операнда и заданной константы в качестве правого операнда, оценка этого оператора принимается в качестве значения реляционного шаблона. В противном случае мы преобразуем входные данные в тип выражения с помощью явного значения NULL или распаковки преобразования. Это ошибка во время компиляции, если такого преобразования нет. Это ошибка во время компиляции, если входной тип является параметром типа, ограниченным или типом, наследуемым от System.Numerics.INumberBase<T>, и тип ввода не имеет подходящего встроенного двоичного реляционного оператора. Шаблон считается несоответствующим, если преобразование не удалось. Если преобразование завершается успешно, результат операции сопоставления шаблонов является результатом оценки выражения e OP v, где e является преобразованным входным, OP является реляционным оператором, а v — константным выражением.

Недостатки

  • "static abstract" — это новая концепция, и она будет значительно увеличивать объем концептуальной нагрузки C#.
  • Это не дешёвая функция для разработки. Мы должны убедиться, что это стоит.

Альтернативы

Структурные ограничения

Альтернативный подход заключается в том, чтобы иметь "структурные ограничения" напрямую и явно требовать наличия конкретных операторов в параметре типа. Недостатки этого: - Это придется записывать каждый раз. Наличие ограничения с именем кажется более предпочтительным. — Это совершенно новый вид ограничения, в то время как предлагаемая функция использует существующую концепцию ограничений интерфейса. — Это будет работать только для операторов, но (не легко) для других видов статических членов.

Неразрешенные вопросы

Статические абстрактные интерфейсы и статические классы

Дополнительные сведения см. в https://github.com/dotnet/csharplang/issues/5783 и https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes.

Встречи по дизайну