Ковариант возвращается
Заметка
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Это включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия фиксируются в соответствующем совещании по проектированию языка (LDM) .
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Сводка
Поддержка ковариантных возвращаемых типов . В частности, разрешить переопределение метода, чтобы объявлять более производный тип возвращаемого значения, чем метод, который он переопределяет, и аналогично разрешить переопределение свойства только для чтения, чтобы объявлять более производный тип. Объявления переопределения, появляющиеся в более производных типах, должны предоставлять возвращаемый тип, по крайней мере, такой же конкретный, как в переопределениях в его базовых типах. Вызывающие метод или свойство будут статически получать более точный тип возвращаемого значения из вызова.
Мотивация
Это распространённая практика в коде, когда необходимо изобретать разные имена методов, чтобы обойти языковое ограничение, требующее, чтобы возвращаемый тип при переопределении совпадал с типом переопределяемого метода.
Это полезно в шаблоне фабрики. Например, в базе кода Roslyn мы бы имели
class Compilation ...
{
public virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
public override CSharpCompilation WithOptions(Options options)...
}
Подробный дизайн
Это спецификация для ковариантных возвращаемых типов в C#. Наша цель заключается в том, чтобы разрешить переопределение метода возвращать более производный тип возвращаемого значения, чем метод, который он переопределяет, и аналогично разрешить переопределение свойства только для чтения, чтобы вернуть более производный тип возвращаемого значения. Вызывающие метод или свойство будут статически получать более уточненный тип возвращаемого значения из вызова, и переопределения в более производных типах должны предоставлять возвращаемый тип, по крайней мере, такой же конкретный, как в переопределениях в его базовых типах.
Переопределение метода класса
Существующее ограничение на переопределение класса (методы §15.6.5)
- Метод переопределения и переопределенный базовый метод имеют одинаковый тип возвращаемого значения.
изменено на
- Метод переопределения должен иметь возвращаемый тип, который преобразуется преобразованием удостоверений или (если метод имеет возвращаемое значение - не возвращаемый возвращаемый параметр возвращаемого значения см. §13.1.0.5 неявное преобразование ссылок на возвращаемый тип переопределенного базового метода.
К списку добавляются следующие дополнительные требования:
- Метод переопределения должен иметь возвращаемый тип, который преобразуется преобразованием удостоверений или (если метод имеет возвращаемое значение - не возвращаемого возвращаемого значения, §13.1.0.5) неявное преобразование ссылок в тип неявной ссылки для каждого переопределенного базового метода, объявленного в базовом типе (прямого или косвенного) метода переопределения метода переопределения.
- Возвращаемый тип метода переопределения должен быть по крайней мере настолько же доступен, как в методе переопределения (домены доступности — §7.5.3).
Это ограничение позволяет методу переопределения в классе private
иметь тип возвращаемого значения private
. Однако для этого требуется метод переопределения public
в типе public
, чтобы иметь тип возвращаемого public
.
Переопределение свойства класса и индексатора
Существующее ограничение на переопределение свойств класса (§15.7.6)
Объявление переопределяющего свойства должно указывать те же модификаторы доступа и имя, что и у унаследованного свойства, и между типом переопределяющего и унаследованного свойствадолжно быть
тождественное преобразование. Если унаследованное свойство имеет только один метод доступа (т. е. если унаследованное свойство доступно только для чтения или только для записи), то переопределяющее свойство должно включать только этот метод доступа. Если унаследованное свойство включает оба метода доступа (т. е. если унаследованное свойство является чтением и записью), то переопределяющее свойство может включать один метод доступа или оба метода доступа.
изменено на
Объявление свойства переопределения должно указывать точно те же модификаторы специальных возможностей и имя, что и унаследованное свойство, и должно быть преобразование идентификаторов
или (если унаследованное свойство доступно только для чтения и имеет возвращаемое значение , . Если унаследованное свойство имеет только один метод доступа (т. е. если унаследованное свойство доступно только для чтения или только для записи), то переопределяющее свойство должно включать только этот метод доступа. Если унаследованное свойство включает оба метода доступа (т. е. если унаследованное свойство является чтением и записью), то переопределяющее свойство может включать один метод доступа или оба метода доступа. Тип переопределяющего свойства должен быть по крайней мере так же доступен, как переопределяемое свойство (домены доступности — §7.5.3).а не возвращаемое значение возвращаемого значения §13.1.0.5 ) неявное преобразование ссылок из типа переопределяющего свойства в тип унаследованного свойства
Оставшаяся часть приведенной ниже спецификации проекта предлагает дополнительное расширение для ковариантных возвратов методов интерфейса, которые будут рассматриваться позже.
Метод интерфейса, свойство и переопределение индексатора
Добавляя к типам членов, разрешенным в интерфейсе, с внедрением функции DIM в C# 8.0, мы дополнительно добавляем поддержку для участников override
вместе с ковариантными возвращаемыми значениями. Эти правила следуют правилам членов override
, указанным для классов, но с со следующими различиями.
Следующий текст в классах:
Метод, который переопределяется через объявление о переопределении, называется переопределённым базовым методом. Для метода переопределения
M
, объявленного в классеC
, переопределенный базовый метод определяется путем изучения каждого базового классаC
, начиная с прямого базового классаC
и продолжая каждым последующим прямым базовым классом, пока в заданном типе базового класса не будет найден по крайней мере один доступный метод с тем же сигнатурой, что и уM
после замены аргументов типа.
указана соответствующая спецификация для интерфейсов:
Метод, который переопределяется объявлением переопределения, известен как переопределенный базовый метод. Для метода переопределения
M
, объявленного в интерфейсеI
, переопределенный базовый метод определяется путем изучения каждого прямого или косвенного базового интерфейсаI
, собирая набор интерфейсов, объявляющих доступный метод, имеющий ту же сигнатуру, что иM
после подстановки аргументов типа. Если этот набор интерфейсов имеет наиболее производный тип , к которому имеется идентичность или неявное преобразование ссылок из каждого типа этого набора, и этот тип содержит уникальное объявление такого метода, то это переопределенный базовый метод .
Мы также разрешаем override
свойства и индексаторы в интерфейсах, так же, как это указано для классов в §15.7.6 виртуальных, закрытых, переопределенных и абстрактных акцессоров.
Поиск имен
Поиск имен в условиях наличия объявлений класса override
в настоящее время изменяет результат поиска, накладывая на найденного члена сведения из самого производного объявления override
в иерархии классов, начиная с типа квалификатора идентификатора (или this
, если квалификатор отсутствует). Например, в §12.6.2.2 соответствующие параметры у нас есть
Для виртуальных методов и индексаторов, определенных в классах, список параметров выбирается из первого объявления или переопределения элемента функции, найденного при запуске статического типа приемника, и выполняется поиск по базовым классам.
к этому мы добавим
Для виртуальных методов и индексаторов, определенных в интерфейсах, список параметров выбирается из объявления или переопределения элемента функции, найденного в наиболее производном типе среди этих типов, содержащих объявление переопределения элемента функции. Это ошибка на этапе компиляции, если такой уникальный тип не существует.
Для типа результата доступа к свойству или индексатору существующий текст
- Если
I
идентифицирует свойство экземпляра, результатом является доступ к свойству с соответствующим выражением экземпляраE
и связанным типом свойства. ЕслиT
является типом класса, соответствующий тип выбирается из первого объявления свойства или его переопределения, найденного при запуске сT
и продолжая поиск по его базовым классам.
дополнено чем-либо
Если
T
является типом интерфейса, связанный тип выбирается из объявления или переопределения свойства, найденного в наиболее производном отT
или его прямых или косвенных базовых интерфейсов. Это ошибка на этапе компиляции, если не существует такого уникального типа.
Аналогичное изменение должно быть внесено в §12.8.12.3 доступ индексатора
В §12.8.10 "Выражения вызова" мы расширим существующий текст.
- В противном случае результатом является значение, связанное с типом возвращаемого типа метода или делегата. Если вызов является методом экземпляра, а приемник имеет тип класса
T
, ассоциированный тип выбирается из первого объявления или переопределения метода, обнаруженного, начиная сT
и выполняя поиск по его базовым классам.
с
Если вызов имеет метод экземпляра, а приемник имеет тип интерфейса
T
, связанный тип выбирается из объявления или переопределения метода, найденного в наиболее производном интерфейсе изT
и его прямых и косвенных базовых интерфейсов. Это ошибка во время компиляции, если не существует уникального такого типа.
Неявные реализации интерфейса
Этот раздел спецификации
В целях сопоставления интерфейсов элемент класса
A
соответствует элементу интерфейсаB
, когда:
A
иB
являются методами, а имена, тип и формальные списки параметровA
иB
идентичны.A
иB
являются свойствами, имя и типA
иB
идентичны, аA
имеют те же методы доступа, что иB
(A
разрешено иметь дополнительные методы доступа, если он не является явной реализацией члена интерфейса).A
иB
являются событиями, а имя и типA
иB
идентичны.A
иB
— индексаторы, списки типов и формальных параметровA
иB
идентичны, аA
имеют те же методы доступа, что иB
(A
разрешено иметь дополнительные методы доступа, если он не является явной реализацией члена интерфейса).
изменяется следующим образом:
В целях сопоставления интерфейсов элемент класса
A
соответствует элементу интерфейсаB
, когда:
A
иB
являются методами, а списки имен и формальных параметровA
иB
идентичны, и возвращаемый типA
может быть преобразован в возвращаемый типB
через идентичность неявного преобразования ссылок к возвращаемому типуB
.A
иB
являются свойствами, имяA
иB
одинаковое,A
имеет те же аксессоры, что иB
(A
разрешено иметь дополнительные аксессоры, если он не является явной реализацией элемента интерфейса), а типA
может быть преобразован в возвращаемый типB
через тождественное преобразование или, еслиA
является свойством только для чтения, неявное преобразование ссылок.A
иB
являются событиями, а имя и типA
иB
идентичны.A
иB
являются индексаторами, формальные параметры списковA
иB
идентичны,A
имеет те же методы доступа, что иB
(A
разрешено иметь дополнительные методы доступа, если он не является явной реализацией элемента интерфейса), а типA
может быть преобразован в возвращаемый типB
посредством идентичного преобразования или, еслиA
является индексатором только для чтения, посредством неявного преобразования ссылки.
Это технически является важным изменением, поскольку сегодня программа ниже выводит "C1.M", но будет выводить "C2.M" в соответствии с предлагаемой редакцией.
using System;
interface I1 { object M(); }
class C1 : I1 { public object M() { return "C1.M"; } }
class C2 : C1, I1 { public new string M() { return "C2.M"; } }
class Program
{
static void Main()
{
I1 i = new C2();
Console.WriteLine(i.M());
}
}
В связи с этим критическим изменением мы можем отказаться от поддержки ковариантных типов возврата в неявных реализациях.
Ограничения реализации интерфейса
Нам понадобится правило, согласно которому явная реализация интерфейса должна объявлять тип возвращаемого значения, не менее производный, чем тип возвращаемого значения, объявленный в любом переопределении его базовых интерфейсов.
Последствия совместимости API
TBD
Открытые проблемы
Спецификация не говорит, как вызывающий получает более точный тип возвращаемого значения. Предположительно, это будет сделано так же, как вызывающие получают доступ к спецификациям параметров наиболее производного переопределения.
Если у нас есть следующие интерфейсы:
interface I1 { I1 M(); }
interface I2 { I2 M(); }
interface I3: I1, I2 { override I3 M(); }
Обратите внимание, что в I3
методы I1.M()
и I2.M()
были "объединены". При реализации I3
необходимо внедрять оба компонента вместе.
Как правило, для ссылки на исходный метод требуется явная реализация. Вопрос в классе
class C : I1, I2, I3
{
C IN.M();
}
Что это значит здесь? Какой должен быть N?
Я предлагаю реализовать либо I1.M
, либо I2.M
(но не оба), и рассматривать это как реализацию обоих.
Недостатки
- [ ] Каждое изменение языка должно оправдывать себя.
- [ ] Мы должны убедиться, что производительность является разумной, даже в случае глубоких иерархий наследования
- [ ] Мы должны убедиться, что артефакты стратегии перевода не влияют на семантику языка, даже при использовании нового IL из старых компиляторов.
Альтернативы
Мы могли бы немного расслабить языковые правила в источнике, чтобы разрешить,
// Possible alternative. This was not implemented.
abstract class Cloneable
{
public abstract Cloneable Clone();
}
class Digit : Cloneable
{
public override Cloneable Clone()
{
return this.Clone();
}
public new Digit Clone() // Error: 'Digit' already defines a member called 'Clone' with the same parameter types
{
return this;
}
}
Неразрешенные вопросы
- [ ] Как API,скомпилированные для использования этой функции, работают в более старых версиях языка?
Дизайнерские совещания
- некоторые обсуждения на https://github.com/dotnet/roslyn/issues/357.
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-08.md
- Обсуждение в оффлайн-режиме по решению о поддержке переопределения методов классов только в C# 9.0.
C# feature specifications