Методы интерфейса по умолчанию
Заметка
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Различия фиксируются в примечаниях соответствующих собраний по проектированию языка (LDM) .
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Сводка
Добавьте поддержку виртуальных методов расширения — в интерфейсах, имеющих конкретные реализации. Для класса или структуры, реализующих такой интерфейс, требуется наличие одной наиболее конкретной реализации метода интерфейса, которая может быть реализована самим классом или структурой, либо унаследована от их базовых классов или интерфейсов. Методы виртуального расширения позволяют автору API добавлять методы в интерфейс в будущих версиях, не нарушая совместимости исходного кода или двоичной совместимости с существующими реализациями этого интерфейса.
Они похожи на Java «Методы по умолчанию».
(На основе вероятной методики реализации) эта функция требует соответствующей поддержки в CLI/CLR. Программы, использующие эту функцию, не могут выполняться в более ранних версиях платформы.
Мотивация
Основные мотивы этой функции:
- Методы интерфейса по умолчанию позволяют автору API добавлять методы в интерфейс в будущих версиях без нарушения исходной или двоичной совместимости с существующими реализациями этого интерфейса.
- Эта функция позволяет C# взаимодействовать с API, предназначенными для Android (Java) и iOS (Swift), которые поддерживают аналогичные функции.
- Как оказалось, добавление реализаций интерфейса по умолчанию предоставляет элементы функции языка "признаки" (https://en.wikipedia.org/wiki/Trait_(computer_programming)). Трейты доказали свою эффективность как мощный метод программирования (http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf).
Подробный дизайн
Синтаксис интерфейса расширен, чтобы позволить
- объявления членов, объявляющие константы, операторы, статические конструкторы и вложенные типы;
- тело для метода или индексатора, свойства или аксессора событий (то есть, "реализации по умолчанию");
- объявления членов, объявляющие статические поля, методы, свойства, индексаторы и события;
- объявления членов с использованием синтаксиса явной реализации интерфейса; и
- Явные модификаторы доступа (доступ по умолчанию —
public
).
Члены с телами позволяют интерфейсу предоставлять реализацию по умолчанию для метода в классах и структурах, которые не предоставляют собственную реализацию.
Интерфейсы могут не содержать состояние экземпляра. Хотя статические поля теперь разрешены, поля экземпляров не допускаются в интерфейсах. Автоматические свойства экземпляра не поддерживаются в интерфейсах, так как они неявно объявляют скрытое поле.
Статические и частные методы позволяют использовать рефакторинг и организацию кода, используемого для реализации общедоступного API интерфейса.
Переопределение метода в интерфейсе должно использовать явный синтаксис реализации интерфейса.
Ошибка заключается в объявлении типа класса, типа структуры или типа перечисления в области параметра типа, который был объявлен с variance_annotation. Например, объявление C
, приведённое ниже, является ошибкой.
interface IOuter<out T>
{
class C { } // error: class declaration within the scope of variant type parameter 'T'
}
Конкретные методы в интерфейсах
Простейшая форма этой функции — это возможность объявлять конкретный метод в интерфейсе, который является методом с текстом.
interface IA
{
void M() { WriteLine("IA.M"); }
}
Класс, реализующий этот интерфейс, не должен реализовывать его конкретный метод.
class C : IA { } // OK
IA i = new C();
i.M(); // prints "IA.M"
Окончательное переопределение для IA.M
в классе C
— это конкретный метод M
, объявленный в IA
. Обратите внимание, что класс не наследует члены из своих интерфейсов; это остаётся неизменным с введением этой функции:
new C().M(); // error: class 'C' does not contain a member 'M'
В члене экземпляра интерфейса this
имеет тип обрамляющего интерфейса.
Модификаторы в интерфейсах
Синтаксис интерфейса упрощён, чтобы разрешить модификаторы на его элементах. Разрешены следующие: private
, protected
, internal
, public
, virtual
, abstract
, sealed
, static
, extern
и partial
.
Элемент интерфейса, объявление которого содержит текст, является элементом virtual
, если не используется модификатор sealed
или private
. Модификатор virtual
может использоваться для элемента функции, который в противном случае будет неявно virtual
. Аналогичным образом, хотя abstract
используется по умолчанию для элементов интерфейса без тел, этот модификатор может быть явно задан. Невиртуальный член может быть объявлен с помощью ключевого слова sealed
.
Это ошибка, если член функции private
или sealed
интерфейса не имеет тела. Элемент функции private
может не иметь модификатора sealed
.
Модификаторы доступа могут использоваться для элементов интерфейса всех типов элементов, разрешенных. Уровень доступа public
— это значение по умолчанию, но оно может быть явно задано.
Open Issue: Необходимо указать точное определение модификаторов доступа, таких как
protected
иinternal
, и какие объявления переопределяют или не переопределяют их (в производном интерфейсе) или реализуют их (в классе, реализующем интерфейс).
Интерфейсы могут объявлять элементы static
, включая вложенные типы, методы, индексаторы, свойства, события и статические конструкторы. Уровень доступа по умолчанию для всех элементов интерфейса public
.
Интерфейсы не могут объявлять конструкторы экземпляров, деструкторы или поля.
закрытая проблема: должны ли объявления операторов быть разрешены в интерфейсе? Вероятно, не операторы преобразования, но как насчёт других? принятия решений. Операторы разрешены за исключением для операторов преобразования, равенства и неравенства.
Вопрос закрыт: следует ли разрешать
new
в объявлениях членов интерфейса, которые скрывают членов базовых интерфейсов? Решение: Да.
решенная проблема: В настоящее время мы не разрешаем
partial
в интерфейсе или его элементах. Для этого потребуется отдельное предложение. решение: Да. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#permit-partial-in-interface
Явная реализация в интерфейсах
Явные реализации позволяют программисту предоставлять наиболее конкретную реализацию виртуального члена в интерфейсе, в котором компилятор или среда выполнения в противном случае не будет находить его. Объявление реализации разрешено явным образом реализовать конкретный метод базового интерфейса путем определения объявления с именем интерфейса (в данном случае модификатор доступа не разрешен). Неявные реализации не допускаются.
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
void IA.M() { WriteLine("IB.M"); } // Explicit implementation
}
interface IC : IA
{
void M() { WriteLine("IC.M"); } // Creates a new M, unrelated to `IA.M`. Warning
}
Явные реализации в интерфейсах не могут быть объявлены sealed
.
Общедоступные virtual
члены функции в интерфейсе могут быть реализованы только в производном интерфейсе явным образом (путем указания имени в объявлении с типом интерфейса, который первоначально объявил метод, и пропуская модификатор доступа). Элемент должен быть доступным, где он реализован.
Переабстракция
Виртуальный (конкретный) метод, объявленный в интерфейсе, может быть повторно извлечен в производном интерфейсе.
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
abstract void IA.M();
}
class C : IB { } // error: class 'C' does not implement 'IA.M'.
Модификатор abstract
требуется в объявлении IB.M
, чтобы указать, что выполняется реабстракция IA.M
.
Это полезно в производных интерфейсах, где реализация метода по умолчанию является неуместной, и более соответствующая реализация должна быть предоставлена путем реализации классов.
Наиболее конкретное правило реализации
Мы требуем, чтобы каждый интерфейс и класс имели наиболее конкретную реализацию для каждого виртуального члена среди существующих в типе или его прямых и косвенных интерфейсах. наиболее конкретная реализация — это уникальная реализация, которая более конкретна, чем любая другая реализация. Если реализации нет, сам элемент считается наиболее конкретной реализацией.
Одна реализация M1
считается более конкретной, чем другая реализация M2
, если M1
объявлены в типе T1
, M2
объявлены в типе T2
, и либо
-
T1
содержитT2
среди своих прямых или косвенных интерфейсов или -
T2
— это тип интерфейса, ноT1
не является типом интерфейса.
Например:
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
void IA.M() { WriteLine("IC.M"); }
}
interface ID : IB, IC { } // compiles, but error when a class implements 'ID'
abstract class C : IB, IC { } // error: no most specific implementation for 'IA.M'
abstract class D : IA, IB, IC // ok
{
public abstract void M();
}
public class E : ID { } // Error. No most specific implementation for 'IA.M'
Наиболее конкретное правило реализации гарантирует, что конфликт (т. е. неоднозначность, возникающая при наследовании алмазов), явно разрешается программистом в момент возникновения конфликта.
Поскольку мы поддерживаем явные реабстракции в интерфейсах, мы могли бы сделать это также и в классах.
abstract class E : IA, IB, IC // ok
{
abstract void IA.M();
}
закрытая проблема: следует ли поддерживать явные реализации абстрактных интерфейсов в классах? Решение : НЕТ
Кроме того, это ошибка, если в объявлении класса наиболее конкретная реализация некоторых методов интерфейса является абстрактной реализацией, объявленной в интерфейсе. Это существующее правило, используемое с использованием новой терминологии.
interface IF
{
void M();
}
abstract class F : IF { } // error: 'F' does not implement 'IF.M'
Виртуальное свойство, объявленное в интерфейсе, может иметь наиболее конкретную реализацию для своего аксессора get
в одном интерфейсе и наиболее конкретную реализацию для аксессора set
в другом интерфейсе. Это считается нарушением правила наиболее конкретной реализации и создает ошибку компилятора.
методы static
и private
Так как интерфейсы теперь могут содержать исполняемый код, полезно абстрагировать общий код в частные и статические методы. Теперь мы разрешаем их в интерфейсах.
закрытая задача: следует ли поддерживать частные методы? Следует ли поддерживать статические методы? Решение : ДА
открыть вопрос: следует ли разрешать методы интерфейса быть
protected
илиinternal
или другим типом доступа? Если да, что такое семантика? Ониvirtual
по умолчанию? Если да, есть ли способ сделать их не-виртуальными?
закрытая проблема: если мы поддерживаем статические методы, следует ли поддерживать (статические) операторы? Решение : ДА
Вызовы базового интерфейса
Синтаксис в этом разделе не реализован. Остается активным предложением.
Код в типе, наследуемом от интерфейса с методом по умолчанию, может явно вызывать базовую реализацию этого интерфейса.
interface I0
{
void M() { Console.WriteLine("I0"); }
}
interface I1 : I0
{
override void M() { Console.WriteLine("I1"); }
}
interface I2 : I0
{
override void M() { Console.WriteLine("I2"); }
}
interface I3 : I1, I2
{
// an explicit override that invoke's a base interface's default method
void I0.M() { I2.base.M(); }
}
Метод экземпляра (нестатического) позволяет вызывать реализацию метода доступного экземпляра в прямом базовом интерфейсе, невиртуально именуя его с помощью синтаксиса base(Type).M
. Это полезно, когда переопределение, требуемое из-за ромбовидного наследования, разрешается путем делегирования конкретной базовой реализации.
interface IA
{
void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
override void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
override void IA.M() { WriteLine("IC.M"); }
}
class D : IA, IB, IC
{
void IA.M() { base(IB).M(); }
}
Если доступ к элементу virtual
или abstract
осуществляется с помощью синтаксиса base(Type).M
, необходимо, чтобы Type
содержал уникальное наиболее специфическое переопределение для M
.
Привязка базовых предложений
Интерфейсы теперь содержат типы. Эти типы могут использоваться в базовом предложении в качестве базовых интерфейсов. При привязке основной клаузулы может потребоваться знать набор базовых интерфейсов для связывания этих типов (например, для их поиска и разрешения защищенного доступа). Таким образом, значение базового предложения интерфейса определяется циклическим образом. Чтобы разбить цикл, мы добавим новые языковые правила, соответствующие аналогичному правилу, уже существующему для классов.
При определении значения interface_base интерфейса базовые интерфейсы временно считаются пустыми. Интуитивно это гарантирует, что значение базового предложения не может рекурсивно зависеть от самого себя.
Мы использовали следующие правила:
"Если класс B является производным от класса A, это ошибка времени компиляции, если A зависит от B. Класс напрямую зависит от своего прямого базового класса (если таковой имеется), и напрямую зависит откласса, в котором он непосредственно вложен (если есть)." Учитывая это определение, полный набор классов , от которых зависит класс, является рефлекторным и транзитивным закрытием напрямую зависит от связи".
Ошибка компиляции возникает, если интерфейс напрямую или косвенно наследуется от самого себя. Базовые интерфейсы интерфейса — это явные базовые интерфейсы и их базовые интерфейсы. Другими словами, набор базовых интерфейсов — это полное транзитивное замыкание явных базовых интерфейсов, их явных базовых интерфейсов и т. д.
Мы корректируем их следующим образом:
Если класс B является производным от класса A, это ошибка времени компиляции для A, зависят от B. Класс напрямую зависит от его прямой базовый класс (если таковой) и напрямую зависит от тип , в котором он немедленно вложен (при наличии).
Если интерфейс IB расширяет интерфейс IA, это ошибка компиляции, если IA зависит от IB. Интерфейс напрямую зависит от его прямых базовых интерфейсов (если таковые имеются) и также зависит от типа, в котором он вложен непосредственно (при наличии).
Учитывая эти определения, полный набор типов , от которых зависит тип, представляет собой рефлексивное и транзитивное замыкание отношения непосредственно зависит от.
Влияние на существующие программы
Приведенные здесь правила не влияют на смысл существующих программ.
Пример 1.
interface IA
{
void M();
}
class C: IA // Error: IA.M has no concrete most specific override in C
{
public static void M() { } // method unrelated to 'IA.M' because static
}
Пример 2.
interface IA
{
void M();
}
class Base: IA
{
void IA.M() { }
}
class Derived: Base, IA // OK, all interface members have a concrete most specific override
{
private void M() { } // method unrelated to 'IA.M' because private
}
Те же правила дают аналогичные результаты аналогичной ситуации, включающей методы интерфейса по умолчанию:
interface IA
{
void M() { }
}
class Derived: IA // OK, all interface members have a concrete most specific override
{
private void M() { } // method unrelated to 'IA.M' because private
}
закрытая проблема: убедитесь, что это предполагаемое следствие спецификации. Решение : ДА
Разрешение метода во время выполнения
Закрытая проблема: Спецификация должна описывать алгоритм разрешения методов среды выполнения при наличии методов интерфейса по умолчанию. Необходимо убедиться, что семантика согласуется с семантикой языка, например, которые объявленные методы выполняют и не переопределяют или не реализуют метод
internal
.
API поддержки CLR
Чтобы компиляторы определяли, когда они компилируют для среды выполнения, поддерживающей эту функцию, библиотеки для таких сред изменяются, чтобы объявить этот факт через API, рассмотренный в https://github.com/dotnet/corefx/issues/17116. Мы добавляем
namespace System.Runtime.CompilerServices
{
public static class RuntimeFeature
{
// Presence of the field indicates runtime support
public const string DefaultInterfaceImplementation = nameof(DefaultInterfaceImplementation);
}
}
открытый вопрос: является ли это лучшим именем для функции CLR? Функция CLR делает гораздо больше, чем просто это (например, смягчает ограничения защиты, поддерживает переопределения в интерфейсах и т. д.). Возможно, он должен называться как "конкретные методы в интерфейсах" или "признаки"?
Дополнительные области, которые необходимо указать
- [ ] Было бы полезно систематизировать типы эффектов исходной и двоичной совместимости, которые возникают при добавлении методов интерфейса по умолчанию и переопределения в существующие интерфейсы.
Недостатки
Для этого предложения требуется координированное обновление спецификации CLR (для поддержки конкретных методов в интерфейсах и разрешения методов). Поэтому это довольно дорого, и поэтому может быть целесообразно выполнить в сочетании с другими возможностями, которые мы также ожидаем, требующие изменений в CLR.
Альтернативы
Нет.
Неразрешенные вопросы
- Открытые вопросы вызываются на протяжении всего предложения, приведенного выше.
- См. также https://github.com/dotnet/csharplang/issues/406 для списка открытых вопросов.
- Подробная спецификация должна описать механизм разрешения, используемый во время выполнения, чтобы выбрать точный метод, который необходимо вызвать.
- Взаимодействие метаданных, созданных новыми компиляторами и потребляемых старыми компиляторами, необходимо подробно проработать. Например, необходимо убедиться, что представление метаданных, которое мы используем, не приводит к добавлению реализации по умолчанию в интерфейсе для разрыва существующего класса, реализующего этот интерфейс при компиляции более старым компилятором. Это может повлиять на представление метаданных, которое мы можем использовать.
- Проект должен учитывать взаимодействие с другими языками и существующими компиляторами для других языков.
Устраненные вопросы
Абстрактное переопределение
На ранней версии черновика спецификации была возможность "переопределить" унаследованный метод.
interface IA
{
void M();
}
interface IB : IA
{
override void M() { }
}
interface IC : IB
{
override void M(); // make it abstract again
}
Мои заметки за 2017-03-20 показали, что мы решили не разрешать это. Однако для этого есть по крайней мере два варианта использования:
- API Java, с которыми некоторые пользователи этой функции надеются взаимодействовать, зависят от этого объекта.
- Программирование с характеристиками получает преимущества от этого. Reabstraction является одним из элементов языковой особенности "traits" (https://en.wikipedia.org/wiki/Trait_(computer_programming)). Следующие классы разрешены:
public abstract class Base
{
public abstract void M();
}
public abstract class A : Base
{
public override void M() { }
}
public abstract class B : A
{
public override abstract void M(); // reabstract Base.M
}
К сожалению, этот код нельзя рефакторингировать как набор интерфейсов (признаков), если это не разрешено. По принципу жадности Джареда, это должно быть разрешено.
закрытый вопрос: должна ли быть разрешена повторная абстракция? [ДА] Мои заметки были неправильными. Заметки LDM говорят, что реабстрация разрешена в интерфейсе. Не в классе.
Виртуальный модификатор и запечатанный модификатор
Мы решили явно разрешить модификаторы, указанные в элементах интерфейса, если нет причины запретить некоторые из них. Это приводит к интересному вопросу о виртуальном модификаторе. Должно ли это потребоваться для членов с реализацией по умолчанию?
Мы могли бы сказать, что:
- Если отсутствует реализация и не указаны ни virtual, ни sealed, предполагается, что член является абстрактным.
- Если существует реализация, и ни абстрактные, ни запечатанные не указаны, предполагается, что член является виртуальным.
- Запечатанный модификатор требуется для того, чтобы метод не был виртуальным и абстрактным.
Кроме того, можно сказать, что для виртуального члена требуется виртуальный модификатор. Т. е. если есть член с реализацией, не помеченной явным образом виртуальным модификатором, он не является ни виртуальным, ни абстрактным. Этот подход может обеспечить более эффективное взаимодействие при перемещении метода из класса в интерфейс:
- абстрактный метод остается абстрактным.
- Виртуальный метод остается виртуальным.
- Метод без модификатора не остается ни виртуальным, ни абстрактным.
- Запечатанный модификатор нельзя применить к методу, который не является переопределением.
Как ты думаешь?
закрытая проблема: должен ли конкретный метод (с реализацией) быть неявно
virtual
? [ДА]
Решения: принятые в LDM 2017-04-05:
- невиртуальные должны быть явно выражены через
sealed
илиprivate
. -
sealed
— это ключевое слово для создания элементов экземпляра интерфейса с телами, не виртуальными - Мы хотим разрешить все модификаторы в интерфейсах
- Доступ по умолчанию для элементов интерфейса, включая вложенные типы, является публичным.
- частные функции в интерфейсах по умолчанию закрыты, и
sealed
для них не разрешен. - Частные классы (в интерфейсах) разрешены и могут быть закрыты, то есть закрыты в смысле закрытого класса.
- При отсутствии хорошего предложения частичное использование по-прежнему не допускается для интерфейсов или их членов.
Двоичная совместимость 1
Когда библиотека предоставляет реализацию по умолчанию
interface I1
{
void M() { Impl1 }
}
interface I2 : I1
{
}
class C : I2
{
}
Мы понимаем, что реализация I1.M
в C
I1.M
. Что делать, если сборка, содержащая I2
, изменяется следующим образом и перекомпилируется
interface I2 : I1
{
override void M() { Impl2 }
}
но C
не перекомпилируется. Что происходит при запуске программы? Вызов (C as I1).M()
- Выполняется
I1.M
- Выполняется
I2.M
- Вызывает некоторую ошибку среды выполнения
решение: принято 2017-04-11: выполняется I2.M
, что является наиболее однозначным и конкретным переопределением во время выполнения.
Методы доступа к событиям (закрытые)
закрытая проблема: можно ли переопределить событие "по частям"?
Рассмотрим этот случай:
public interface I1
{
event T e1;
}
public interface I2 : I1
{
override event T
{
add { }
// error: "remove" accessor missing
}
}
Эта "частичная" реализация события не допускается, так как в классе синтаксис объявления события не разрешает только один метод доступа; оба (или ни другое) должны быть предоставлены. Вы можете добиться того же, разрешив аксессору удаления быть неявно абстрактным в синтаксисе за счет отсутствия тела.
public interface I1
{
event T e1;
}
public interface I2 : I1
{
override event T
{
add { }
remove; // implicitly abstract
}
}
Обратите внимание, что это новый (предлагаемый) синтаксис. В текущей грамматике методы доступа к событиям имеют обязательный текст.
закрытая проблема: может ли метод доступа к событиям (неявно) абстрагироваться бездействием тела, аналогично тому, как методы в интерфейсах и методах доступа к свойствам (неявно) абстрагируются бездействием тела?
решение : (2017-04-18) Нет, объявления событий требуют обоих конкретных методов доступа (или ни другого).
Переабстракция в классе (закрыто)
закрытая проблема: Мы должны подтвердить, что это разрешено (в противном случае добавление реализации по умолчанию будет критическим изменением):
interface I1
{
void M() { }
}
abstract class C : I1
{
public abstract void M(); // implement I1.M with an abstract method in C
}
решение : (2017-04-18) Да, добавление текста в объявление члена интерфейса не должно прерывать C.
Запечатанное переопределение (закрыто)
Предыдущий вопрос неявно предполагает, что модификатор sealed
можно применить к override
в интерфейсе. Это противоречит спецификации черновика. Нужно ли разрешать запечатывание переопределения? Следует учитывать эффекты совместимости исходного и двоичного кода при запечатывание.
закрытая проблема: следует ли разрешать запечатывать переопределение?
Решение : (2017-04-18) Не будем разрешать sealed
на переопределениях в интерфейсах. Единственное использование sealed
в членах интерфейса заключается в том, чтобы сделать их не виртуальными в первоначальном объявлении.
Алмазное наследование и закрытые классы
Проект предложения предпочитает переопределение класса для переопределения интерфейса в сценариях наследования алмазов:
Мы требуем, чтобы каждый интерфейс и класс имели наиболее конкретное переопределение для каждого метода интерфейса среди переопределений, отображаемых в типе или его прямых и косвенных интерфейсах. наиболее конкретное переопределение — это уникальное переопределение, более конкретное, чем все другие переопределения. Если переопределения нет, сам метод считается наиболее конкретным переопределением.
Один переопределенный
M1
считается более конкретным, чем другое переопределениеM2
, еслиM1
объявлен в типеT1
,M2
объявлен в типеT2
и либо
T1
содержитT2
среди своих прямых или косвенных интерфейсов илиT2
— это тип интерфейса, ноT1
не является типом интерфейса.
Сценарий заключается в этом
interface IA
{
void M();
}
interface IB : IA
{
override void M() { WriteLine("IB"); }
}
class Base : IA
{
void IA.M() { WriteLine("Base"); }
}
class Derived : Base, IB // allowed?
{
static void Main()
{
IA a = new Derived();
a.M(); // what does it do?
}
}
Мы должны подтвердить это поведение (или принять другое решение)
закрытое обсуждение: подтвердите черновик спецификации выше для наиболее конкретного переопределения, поскольку оно применяется к смешанным классам и интерфейсам (классу отдается приоритет над интерфейсом). См. https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#diamonds-with-classes.
Методы интерфейса против структур (закрыто)
Существует несколько неудачных взаимодействий между методами интерфейса по умолчанию и структурами.
interface IA
{
public void M() { }
}
struct S : IA
{
}
Обратите внимание, что члены интерфейса не наследуются:
var s = default(S);
s.M(); // error: 'S' does not contain a member 'M'
Следовательно, клиент должен выполнить боксинг структуры для вызова методов интерфейса.
IA s = default(S); // an S, boxed
s.M(); // ok
Бокс таким образом нивелирует основные преимущества типа struct
. Кроме того, любые методы мутации не будут иметь видимого эффекта, так как они работают на упакованной копии структуры.
interface IB
{
public void Increment() { P += 1; }
public int P { get; set; }
}
struct T : IB
{
public int P { get; set; } // auto-property
}
T t = default(T);
Console.WriteLine(t.P); // prints 0
(t as IB).Increment();
Console.WriteLine(t.P); // prints 0
закрытая проблема: Что можно сделать с этим:
- Запретить объекту
struct
наследовать реализацию по умолчанию. Все методы интерфейса рассматриваются как абстрактные вstruct
. Затем мы можем занять некоторое время позже, чтобы решить, как сделать его более эффективным.- Придумать какую-то стратегию создания кода, которая избегает бокса. Внутри метода, например
IB.Increment
, типthis
может быть похожим на параметр типа, ограниченныйIB
. В связи с этим, чтобы не ограничивать вызывающий объект, неабстрактные методы будут наследоваться от интерфейсов. Это может значительно усложнить работу компилятора и реализацию CLR.- Не беспокойтесь об этом и просто оставьте его в покое как бородавку.
- Другие идеи?
решение: не беспокойтесь об этом и просто оставьте его как бородавку. См. https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#structs-and-default-implementations.
Вызовы базового интерфейса (закрытые)
Это решение не было реализовано в C# 8. Синтаксис base(Interface).M()
не реализован.
Проект спецификации предлагает синтаксис для вызовов базового интерфейса, вдохновленных Java: Interface.base.M()
. Нам нужно выбрать синтаксис, по крайней мере, для первоначального прототипа. Мой любимый — это base<Interface>.M()
.
Закрытый вопрос: Что такое синтаксис вызова базового члена?
решение: синтаксис base(Interface).M()
. См. https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#base-invocation. Этот интерфейс должен быть базовым интерфейсом, но не должен быть прямым базовым интерфейсом.
Открытая проблема: Следует ли разрешать вызовы базового интерфейса в членах классов?
Решение: Да. https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#base-invocation
Переопределение элементов интерфейса, не являющихся открытыми (закрытыми)
В интерфейсе недоступные члены из базовых интерфейсов переопределяются с помощью модификатора override
. Если это "явное" переопределение, которое указывает интерфейс, содержащий элемент, модификатор доступа опущен.
закрытая задача: Если это неявное переопределение, которое не называет интерфейс, должен ли модификатор доступа соответствовать?
Решение: только публичные члены могут быть неявно переопределены, и уровень доступа должен соответствовать. См. https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-a-non-public-interface-member-not-in-list.
Открыть задачу: Требуется ли модификатор доступа, он может быть необязателен или опущен при явном переопределении, например
override void IB.M() {}
?
открытая проблема: является ли
override
обязательным, необязательным или опущенным при явной замене, напримерvoid IB.M() {}
?
Как реализовать непубличный член интерфейса в классе? Возможно, это должно быть сделано явно?
interface IA
{
internal void MI();
protected void MP();
}
class C : IA
{
// are these implementations? Decision: NO
internal void MI() {}
protected void MP() {}
}
Закрытая проблема: Как реализовать недоступный член интерфейса в классе?
Решение: Вы можете явно реализовать только непубличные члены интерфейса. См. https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-a-non-public-interface-member-not-in-list.
решение: ключевое слово override
не разрешено для элементов интерфейса. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#does-an-override-in-an-interface-introduce-a-new-member
Двоичная совместимость 2 (закрыта)
Рассмотрим следующий код, в котором каждый тип находится в отдельной сборке
interface I1
{
void M() { Impl1 }
}
interface I2 : I1
{
override void M() { Impl2 }
}
interface I3 : I1
{
}
class C : I2, I3
{
}
Мы понимаем, что реализация I1.M
в C
I2.M
. Что делать, если сборка, содержащая I3
, изменяется следующим образом и перекомпилируется
interface I3 : I1
{
override void M() { Impl3 }
}
но C
не перекомпилируется. Что происходит при запуске программы? Вызов (C as I1).M()
- Выполняется
I1.M
- Выполняется
I2.M
- Выполняется
I3.M
- 2 или 3, детерминированно
- Создает исключение среды выполнения
решение: выбросить исключение (5). См. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#issues-in-default-interface-methods.
Разрешение partial
в интерфейсе? (закрыто)
Учитывая, что интерфейсы могут использоваться таким образом, как используются абстрактные классы, может быть полезно объявить их partial
. Это было бы особенно полезно в случае генераторов.
Предложение : Удалить языковое ограничение, согласно которому интерфейсы и члены интерфейсов не могут быть объявлены
partial
.
решение: Да. См. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#permit-partial-in-interface.
Main
в интерфейсе? (закрыто)
Открытая задача: Является ли метод
static Main
в интерфейсе кандидатом на роль точки входа программы?
Решение: Да. См. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#main-in-an-interface.
Подтвердите намерение поддерживать общедоступные невиртуальные методы (закрыто)
Можем ли мы подтвердить (или отменить) наше решение разрешить не виртуальные общедоступные методы в интерфейсе?
interface IA
{
public sealed void M() { }
}
Semi-Closed Проблема: (2017-04-18) Мы думаем, что это будет полезно, но мы вернёмся к нему. Это камень преткновения для ментальной модели.
Вводит ли override
в интерфейсе новый член? (закрыто)
Существует несколько способов проверить, вводит ли объявление переопределения новый член или нет.
interface IA
{
void M(int x) { }
}
interface IB : IA
{
override void M(int y) { } // 'override' not permitted
}
interface IC : IB
{
static void M2()
{
M(y: 3); // permitted? Decision: No.
}
override void IB.M(int z) { } // permitted? What does it override? Decision: No.
}
Открытая проблема: Объявление переопределения в интерфейсе представляет новый член? (закрыто)
В классе переопределённый метод является "видимым" в некотором смысле. Например, имена его параметров имеют приоритет над именами параметров в переопределяемом методе. Это поведение можно дублировать в интерфейсах, так как всегда существует самое конкретное переопределение. Но мы хотим дублировать это поведение?
Можно ли также "переопределить" уже переопределённый метод? [Спорный]
решение: ключевое слово override
не разрешено для элементов интерфейса.
https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#does-an-override-in-an-interface-introduce-a-new-member.
Свойства с частным методом доступа (закрытым)
Мы говорим, что частные члены не являются виртуальными, а сочетание виртуальных и частных запрещено. Но как насчет свойства с частным методом доступа?
interface IA
{
public virtual int P
{
get => 3;
private set { }
}
}
Разрешено ли это? Здесь есть доступ set
или virtual
? Можно ли переопределить это там, где есть к нему доступ? Реализовывает ли следующий код неявно только аксессор get
?
class C : IA
{
public int P
{
get => 4;
set { }
}
}
Представляет ли нижеизложенное ошибку, потому что IA.P.set не является виртуальным и недоступен?
class C : IA
{
int IA.P
{
get => 4;
set { } // Decision: Not valid
}
}
решение: первый пример выглядит допустимым, а последний — нет. Это решается по аналогии с тем, как это уже работает в C#. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#properties-with-a-private-accessor
Вызовы базового интерфейса, раунд 2 (закрыто)
Это не было реализовано в C# 8.
Наше предыдущее "решение" о том, как обрабатывать базовые вызовы, на самом деле не обеспечивает достаточной экспрессивности. Оказывается, что в C# и CLR, в отличие от Java, необходимо указать интерфейс, содержащий объявление метода, и расположение реализации, которую требуется вызвать.
Я предлагаю следующий синтаксис для базовых вызовов в интерфейсах. Я не влюблен в него, но он иллюстрирует то, что любой синтаксис должен иметь возможность выразить:
interface I1 { void M(); }
interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I4 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3, I4
{
void I1.M()
{
base<I3>(I1).M(); // calls I3's implementation of I1.M
base<I4>(I1).M(); // calls I4's implementation of I1.M
}
void I2.M()
{
base<I3>(I2).M(); // calls I3's implementation of I2.M
base<I4>(I2).M(); // calls I4's implementation of I2.M
}
}
Если неоднозначности нет, вы можете написать его проще
interface I1 { void M(); }
interface I3 : I1 { void I1.M() { } }
interface I4 : I1 { void I1.M() { } }
interface I5 : I3, I4
{
void I1.M()
{
base<I3>.M(); // calls I3's implementation of I1.M
base<I4>.M(); // calls I4's implementation of I1.M
}
}
Или
interface I1 { void M(); }
interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3
{
void I1.M()
{
base(I1).M(); // calls I3's implementation of I1.M
}
void I2.M()
{
base(I2).M(); // calls I3's implementation of I2.M
}
}
Или
interface I1 { void M(); }
interface I3 : I1 { void I1.M() { } }
interface I5 : I3
{
void I1.M()
{
base.M(); // calls I3's implementation of I1.M
}
}
решение: решил base(N.I1<T>).M(s)
, согласившись, что если у нас есть привязка вызова, в дальнейшем может возникнуть проблема. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-11-14.md#default-interface-implementations
Предупреждение о том, что структура не реализует метод по умолчанию? (закрыто)
@vancem утверждает, что следует серьезно рассмотреть возможность создания предупреждения, если объявление типа значения не переопределяет некоторый метод интерфейса, даже если он наследует реализацию этого метода из интерфейса. Потому что это вызывает бокс и подрывает ограниченные вызовы.
решение: это кажется чем-то более подходящим для аналитика. Похоже, что это предупреждение может быть шумным, так как он будет работать даже в том случае, если метод интерфейса по умолчанию никогда не вызывается, а бокс никогда не будет происходить. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#warning-for-struct-not-implementing-default-method
Статические конструкторы интерфейса (закрытые)
Когда выполняются статические конструкторы интерфейса? В текущем проекте интерфейса командной строки предлагается, что оно возникает при доступе к первому статическому методу или полю. Если нет ни того, ни другого, возможно, это никогда не будет запущено?
[2018-10-09 Команда CLR предлагает "Привести к тому же, что мы делаем для типов значений (проверка cctor при доступе к каждому методу экземпляра)"]
решение: статические конструкторы также выполняются при входе в методы экземпляра, если статический конструктор не был beforefieldinit
, в этом случае статические конструкторы выполняются перед доступом к первому статическому полю. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#when-are-interface-static-constructors-run
Совещания по дизайну
2017-03-08 заметки о собрании LDM2017-03-21 заметки о собрании LDM2017-03-23 собрание "Поведение CLR для методов интерфейса по умолчанию"2017-04-05 заметки о собрании LDM2017-04-11 заметки о собрании LDM2017-04-18 заметки о собрании LDM2017-04-19 заметки о собрании LDM2017-05-17 заметки о собрании LDM2017-05-31 заметки о собрании LDM2017-06-14 заметки о собрании LDM2018-10-17 заметки о собрании LDM2018-11-14 заметки о собрании LDM
C# feature specifications