Covariante Rückgaben
Anmerkung
Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.
Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.
Weitere Informationen zum Prozess für die Aufnahme von Funktions-Speclets in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.
Zusammenfassung
Unterstützung kovarianter Rückgabetypen. Erlauben Sie insbesondere die Überschreibung einer Methode, um einen stärker abgeleiteten Rückgabetyp zu deklarieren als die Methode, die sie überschreibt, und ebenso die Überschreibung einer schreibgeschützten Eigenschaft, um einen stärker abgeleiteten Typ zu deklarieren. Deklarationen, die in abgeleiteten Typen überschrieben werden, müssen einen Rückgabetyp angeben, der mindestens so spezifisch ist wie der in den Basistypen angegebenen Überschreibungen. Aufrufer der Methode oder Eigenschaft erhalten auf statische Weise den verfeinerten Rückgabetyp bei einem Aufruf.
Motivation
Es ist ein gängiges Muster im Code, dass verschiedene Methodennamen erfunden werden müssen, um die Spracheinschränkung zu umgehen, dass Überschreibungen denselben Typ wie die überschriebene Methode zurückgeben müssen.
Dies wäre im Factory-Pattern nützlich. Beispiel: In der Roslyn-Codebasis hätten wir
class Compilation ...
{
public virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
public override CSharpCompilation WithOptions(Options options)...
}
Detailliertes Design
Dies ist eine Spezifikation für kovariante Rückgabetypen in C#. Unsere Absicht ist es, die Überschreibung einer Methode zu erlauben, um einen stärker abgeleiteten Rückgabetyp zurückzugeben als die Methode, die sie überschreibt, und ebenso die Überschreibung einer schreibgeschützten Eigenschaft zu erlauben, um einen stärker abgeleiteten Rückgabetyp zurückzugeben. Aufrufer der Methode oder Eigenschaft erhalten den verfeinerten Rückgabetyp direkt aus einem Aufruf. Überschreibungen in abgeleiteten Typen müssen einen Rückgabetyp bereitstellen, der mindestens so spezifisch ist wie der in den Überschreibungen ihrer Basistypen angegebene.
Überschreiben der Klassenmethode
Die bestehende Einschränkung der Überschreibung von Klassen (§15.6.5)
- Die Überschreibungsmethode und die überschriebene Basismethode weisen den gleichen Rückgabetyp auf.
wird geändert in
- Die Überschreibungsmethode muss einen Rückgabetyp haben, der durch eine Identitätskonvertierung konvertierbar ist oder (wenn die Methode einen Wert zurückgibt, nicht eine Ref-Rückgabe), siehe §13.1.0.5 als implizite Verweiskonvertierung in den Rückgabetyp der überschriebenen Basismethode.
Außerdem werden die folgenden zusätzlichen Anforderungen an diese Liste angefügt:
- Die Überschreibungsmethode muss einen Rückgabetyp haben, der durch eine Identitätskonvertierung umgewandelt werden kann oder (falls die Methode einen Wert zurückgibt – keine Ref-Rückgabe, §13.1.0.5) eine implizite Verweiskonvertierung in den Rückgabetyp jeder Überschreibung der überschriebenen Basismethode, die in einem (direkten oder indirekten) Basistyp der Überschreibungsmethode deklariert ist, ermöglicht.
- Der Rückgabetyp der Überschreibungsmethode muss mindestens genauso zugänglich sein wie die Überschreibungsmethode (Zugänglichkeitsbereiche – §7.5.3).
Diese Einschränkung erlaubt es, dass eine Überschreibungsmethode in einer private
Klasse einen private
Rückgabetyp haben kann. Es ist jedoch eine public
Überschreibungsmethode in einem public
Typ erforderlich, damit ein public
Rückgabetyp vorhanden ist.
Klasseneigenschaft und Indexer-Überschreibung
Die bestehende Einschränkung der Überschreibung von Klassen (§15.7.6) Eigenschaften
Eine überschreibende Eigenschaftsdeklaration muss genau die gleichen Zugriffsmodifizierer und denselben Namen wie die geerbte Eigenschaft angeben, und es muss eine Identitätskonvertierung
zwischen dem Typ der überschreibenden und der geerbten Eigenschaftbestehen. Wenn die geerbte Eigenschaft nur über einen einzelnen Accessor verfügt (d. h., wenn die geerbte Eigenschaft entweder schreibgeschützt oder nur schreibend ist), darf die überschriebene Eigenschaft nur diesen Accessor enthalten. Wenn die geerbte Eigenschaft beide Accessoren enthält (d. h., wenn die geerbte Eigenschaft Lese- und Schreibzugriff hat), kann die überschreibende Eigenschaft entweder einen einzelnen Accessor oder beide Accessoren enthalten.
wird geändert in
Eine überschreibende Eigenschaftsdeklaration muss genau dieselben Zugriffsmodifizierer und denselben Namen wie die geerbte Eigenschaft aufweisen, und es muss eine Identitätskonvertierung oder (wenn die geerbte Eigenschaft schreibgeschützt ist und einen Wert zurückgibt – kein Ref-Rückgabe§13.1.0.5) eine implizite Verweiskonvertierung vom Typ der überschreibenden Eigenschaft in den Typ der geerbten Eigenschaft geben. Wenn die geerbte Eigenschaft nur über einen einzelnen Accessor verfügt (d. h., wenn die geerbte Eigenschaft entweder schreibgeschützt oder nur schreibend ist), darf die überschriebene Eigenschaft nur diesen Accessor enthalten. Wenn die geerbte Eigenschaft beide Accessoren enthält (d. h., wenn die geerbte Eigenschaft Lese- und Schreibzugriff hat), kann die überschreibende Eigenschaft entweder einen einzelnen Accessor oder beide Accessoren enthalten. Der Typ der überschreibenden Eigenschaft muss mindestens so zugänglich sein wie die überschreibende Eigenschaft (Zugänglichkeitsbereiche – §7.5.3).
Der rest der nachstehenden Entwurfsspezifikation schlägt eine weitere Erweiterung der kovarianten Rückgaben von Schnittstellenmethoden vor, die später berücksichtigt werden sollen.
Interface-Methode, -Eigenschaft und -Indexer-Überschreibung
Durch das Hinzufügen der DIM-Funktion in C# 8.0 zu den in einer Schnittstelle zulässigen Member-Typen unterstützen wir auch override
-Member zusammen mit kovarianten Rückgaben. Diese Regeln folgen den Regeln der override
-Member, wie für Klassen festgelegt, mit den folgenden Unterschieden:
Der folgende Text in den Klassen:
Die Methode, die durch eine Überschreibungsdeklaration überschrieben wird, wird als die überschriebene Basismethode bezeichnet. Für eine in einer Klasse
M
deklarierten ÜberschreibungsmethodeC
wird die überschriebene Basismethode bestimmt, indem jede Basisklasse vonC
untersucht wird. Dabei beginnt man mit der direkten BasisklasseC
und setzt die Untersuchung mit jeder darauffolgenden direkten Basisklasse fort, bis in einem bestimmten Basisklassentyp mindestens eine zugängliche Methode gefunden wird, die nach Ersetzung der Typargumente dieselbe Signatur wieM
aufweist.
erhält die entsprechende Spezifikation für Schnittstellen:
Die Methode, die durch eine Überschreibungsdeklaration überschrieben wird, wird als die überschriebene Basismethode bezeichnet. Bei einer in einer Schnittstelle
M
deklarierten ÜberschreibungsmethodeI
wird die überschreibende Basismethode bestimmt, indem jede direkte oder indirekte Basisschnittstelle vonI
untersucht wird und dabei die Menge der Schnittstellen erfasst wird, die eine zugängliche Methode deklarieren, die nach Substitution der Typargumente dieselbe Signatur wieM
aufweist. Wenn dieser Satz von Schnittstellen über einen abgeleiteten Typ verfügt, auf den eine Identitäts- oder implizite Verweiskonvertierung von jedem Typ in diesem Satz vorhanden ist und dieser Typ eine eindeutige solche Methodendeklaration enthält, dann ist dies die überschriebene Basismethode.
Ebenso erlauben wir override
-Eigenschaften und Indexer in Schnittstellen, wie für Klassen in §15.7.6 virtuelle, versiegelte, überschreibende und abstrakte Zugriffsmodifikatoren angegeben.
Namenssuche
Die Namenssuche bei Klassen-override
-Deklarationen ändert derzeit das Ergebnis der Namenssuche, indem sie die gefundenen Member-Details aus der am stärksten abgeleiteten override
-Deklaration in der Klassenhierarchie startend vom Typ des Qualifizierers des Bezeichners (oder this
, wenn kein Qualifizierer vorhanden ist) aufzwingt. Zum Beispiel haben wir in §12.6.2.2 Entsprechende Parameter
Bei virtuellen Methoden und Indexern, die in Klassen definiert sind, wird die Parameterliste aus der ersten Deklaration oder Überschreibung des Funktions-Members ausgewählt, die ausgehend vom statischen Typ des Empfängers und beim Durchsuchen der Basisklassen gefunden wird.
zu diesem fügen wir hinzu
Bei virtuellen Methoden und Indexern, die in Schnittstellen definiert sind, wird die Parameterliste aus der Deklaration oder der Überschreibung des Funktions-Members ausgewählt, das im am meisten abgeleiteten Typ unter denen enthalten ist, die die Deklaration oder die Überschreibung des Funktions-Members enthalten. Es handelt sich um einen Kompilierungszeitfehler, wenn kein eindeutiger solcher Typ vorhanden ist.
Für den Ergebnistyp einer Eigenschaft oder eines Indexerzugriffs den vorhandenen Text
- Wenn
I
eine Instanzeigenschaft identifiziert, dann ist das Ergebnis ein Eigenschaftszugriff mit einem verbundenen Instanzausdruck vonE
und einem verbundenen Typ, der dem Typ der Eigenschaft entspricht. WennT
ein Klassentyp ist, wird der zugeordnete Typ aus der ersten Deklaration oder Überschreibung der Eigenschaft gewählt, die gefunden wird, indem beiT
begonnen und die Basisklassen durchsucht werden.
wird erweitert mit
Wenn
T
ein Schnittstellentyp ist, wird der zugeordnete Typ aus der Deklaration oder Überschreibung der Eigenschaft ausgewählt, die in dem am meisten abgeleitetenT
oder seinen direkten oder indirekten Basisschnittstellen gefunden wurde. Es handelt sich um einen Kompilierungszeitfehler, wenn kein eindeutiger solcher Typ vorhanden ist.
Es sollte eine ähnliche Änderung in §12.8.12.3 Indexer-Zugriff vorgenommen werden.
In §12.8.10 Aufrufausdrücke erweitern wir den vorhandenen Text
- Andernfalls ist das Ergebnis ein Wert mit einem zugeordneten Typ des Rückgabetyps der Methode oder des Delegaten. Wenn es sich bei dem Aufruf um eine Instanzmethode handelt und der Empfänger vom Klassentyp
T
ist, wird der zugeordnete Typ aus der ersten Deklaration oder Überschreibung der Methode ausgewählt, die beginnend mitT
gefunden wird, während die Basisklassen durchsucht werden.
durch
Wenn es sich um den Aufruf einer Instanzmethode handelt und der Empfänger vom Schnittstellentyp
T
ist, wird der zugehörige Typ aus der Deklaration oder Überschreibung der gefundenen Methode ausgewählt, die in der am stärksten abgeleiteten Schnittstelle ausT
und ihren direkten und indirekten Basisschnittstellen gefunden wird. Es handelt sich um einen Kompilierungszeitfehler, wenn kein eindeutiger solcher Typ vorhanden ist.
Implizite Schnittstellenimplementierungen
Dieser Abschnitt der Spezifikation
Für die Zwecke der Schnittstellenzuordnung entspricht ein Klassenmitglied
A
einem SchnittstellenmitgliedB
, wenn:
A
undB
sind Methoden, und die Namen-, Typ- und formalen Parameterlisten vonA
undB
sind identisch.A
undB
sind Eigenschaften, der Name und der Typ vonA
undB
sind identisch, undA
hat die gleichen Accessoren wieB
(A
darf zusätzliche Accessoren haben, wenn es sich nicht um eine explizite Schnittstellenmitgliedimplementierung handelt).A
undB
sind Ereignisse, und der Name und der Typ vonA
undB
sind identisch.A
undB
sind Indexer, die Typ- und formale Parameterlisten vonA
undB
sind identisch, undA
verfügt über dieselben Zugriffsmethoden wieB
(A
darf zusätzliche Zugriffsmethoden haben, wenn es sich nicht um eine explizite Schnittstellenmitgliedimplementierung handelt).
wird wie folgt geändert:
Für die Zwecke der Schnittstellenzuordnung entspricht ein Klassenmitglied
A
einem SchnittstellenmitgliedB
in folgenden Fällen:
A
undB
sind Methoden, und die Namen- und formalen Parameterlisten vonA
undB
sind identisch, und der Rückgabetyp vonA
wird über eine Identität der impliziten Verweiskonvertierung in den RückgabetypB
in den Rückgabetyp vonB
konvertiert.A
undB
sind Eigenschaften, der Name vonA
undB
ist identisch,A
hat die gleichen Accessoren wieB
(A
darf zusätzliche Accessoren haben, wenn es sich nicht um eine explizite Schnittstellen-Member-Implementierung handelt), und der Typ vonA
wird über eine Identitätskonvertierung in den Rückgabetyp vonB
konvertiert oder, wennA
eine schreibgeschützte Eigenschaft ist, eine implizite Verweiskonvertierung.A
undB
sind Ereignisse, und der Name und der Typ vonA
undB
sind identisch.A
undB
sind Indexer, die formelle Parameterliste vonA
undB
ist identisch,A
hat die gleichen Accessoren wieB
(A
darf zusätzliche Accessoren haben, wenn es sich nicht um eine explizite Schnittstellen-Member-Implementierung handelt), und der Typ vonA
wird über eine Identitätskonvertierung in den Rückgabetyp vonB
konvertiert oder, wennA
ein schreibgeschützter Indexer ist, eine implizite Verweiskonvertierung.
Dies ist technisch eine einschneidende Änderung, da das folgende Programm heute „C1.M” druckt, aber unter der vorgeschlagenen Überarbeitung „C2.M” drucken würde.
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());
}
}
Aufgrund dieser bahnbrechenden Änderung sollten wir möglicherweise keine kovarianten Rückgabetypen für implizite Implementierungen unterstützen.
Einschränkungen bei der Schnittstellenimplementierung
Wir benötigen eine Regel, dass eine explizite Schnittstellenimplementierung einen Rückgabetyp deklarieren muss, der mindestens genauso abgeleitet ist wie der Rückgabetyp, der in einer Überschreibung in deren Basisschnittstellen deklariert wurde.
API-Kompatibilitätsauswirkungen
TBD
Offene Probleme
Die Spezifikation sagt nicht, wie der Aufrufer den verfeinerten Rückgabetyp erhält. Vermutlich würde dies so erfolgen, wie Aufrufer die Parameterspezifikationen der meist abgeleiteten Überschreibung erhalten.
Wenn wir über die folgenden Schnittstellen verfügen:
interface I1 { I1 M(); }
interface I2 { I2 M(); }
interface I3: I1, I2 { override I3 M(); }
Beachten Sie, dass in I3
die Methoden I1.M()
und I2.M()
"zusammengeführt" wurden. Bei der Implementierung von I3
ist es notwendig, beide gleichzeitig zu implementieren.
Im Allgemeinen ist eine explizite Implementierung erforderlich, um auf die ursprüngliche Methode zu verweisen. Die Frage ist, ob in einer Klasse
class C : I1, I2, I3
{
C IN.M();
}
Was bedeutet das hier? Was sollte N sein?
Ich schlage vor, dass wir die Implementierung von entweder I1.M
oder I2.M
(aber nicht beide) zulassen und dies als Implementierung beider behandeln.
Nachteile
- [ ] Jede Sprachänderung muss sich lohnen.
- [ ] Wir sollten sicherstellen, dass die Leistung angemessen ist, auch bei tiefen Vererbungshierarchien
- [ ] Wir sollten sicherstellen, dass Artefakte der Übersetzungsstrategie nicht die Sprachsemantik beeinflussen, selbst wenn neue IL durch alte Compiler verarbeitet werden.
Alternativen
Wir könnten die Sprachregeln leicht lockern, um im Quelltext zuzulassen,
// 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;
}
}
Ungelöste Fragen
- [ ] Wie funktionieren APIs, die kompiliert wurden, um dieses Feature in älteren Versionen der Sprache zu verwenden?
Designbesprechungen
- einige Diskussionen bei https://github.com/dotnet/roslyn/issues/357.
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-08.md
- Offline-Diskussion über eine Entscheidung zur Unterstützung der Überschreibung von Klassenmethoden nur in C# 9.0.
C# feature specifications