Freigeben über


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 Eigenschaft bestehen. 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 Überschreibungsmethode C wird die überschriebene Basismethode bestimmt, indem jede Basisklasse von C untersucht wird. Dabei beginnt man mit der direkten Basisklasse C 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 wie M 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 Überschreibungsmethode I wird die überschreibende Basismethode bestimmt, indem jede direkte oder indirekte Basisschnittstelle von I untersucht wird und dabei die Menge der Schnittstellen erfasst wird, die eine zugängliche Methode deklarieren, die nach Substitution der Typargumente dieselbe Signatur wie M 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 von E und einem verbundenen Typ, der dem Typ der Eigenschaft entspricht. Wenn T ein Klassentyp ist, wird der zugeordnete Typ aus der ersten Deklaration oder Überschreibung der Eigenschaft gewählt, die gefunden wird, indem bei T 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 abgeleiteten T 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 mit T 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 aus T 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 Schnittstellenmitglied B, wenn:

  • A und B sind Methoden, und die Namen-, Typ- und formalen Parameterlisten von A und B sind identisch.
  • A und B sind Eigenschaften, der Name und der Typ von A und B sind identisch, und A hat die gleichen Accessoren wie B (A darf zusätzliche Accessoren haben, wenn es sich nicht um eine explizite Schnittstellenmitgliedimplementierung handelt).
  • A und B sind Ereignisse, und der Name und der Typ von A und B sind identisch.
  • A und B sind Indexer, die Typ- und formale Parameterlisten von A und B sind identisch, und A verfügt über dieselben Zugriffsmethoden wie B (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 Schnittstellenmitglied B in folgenden Fällen:

  • A und B sind Methoden, und die Namen- und formalen Parameterlisten von A und B sind identisch, und der Rückgabetyp von A wird über eine Identität der impliziten Verweiskonvertierung in den Rückgabetyp B in den Rückgabetyp von B konvertiert.
  • A und B sind Eigenschaften, der Name von A und B ist identisch, A hat die gleichen Accessoren wie B (A darf zusätzliche Accessoren haben, wenn es sich nicht um eine explizite Schnittstellen-Member-Implementierung handelt), und der Typ von A wird über eine Identitätskonvertierung in den Rückgabetyp von B konvertiert oder, wenn A eine schreibgeschützte Eigenschaft ist, eine implizite Verweiskonvertierung.
  • A und B sind Ereignisse, und der Name und der Typ von A und B sind identisch.
  • A und B sind Indexer, die formelle Parameterliste von A und B ist identisch, A hat die gleichen Accessoren wie B (A darf zusätzliche Accessoren haben, wenn es sich nicht um eine explizite Schnittstellen-Member-Implementierung handelt), und der Typ von A wird über eine Identitätskonvertierung in den Rückgabetyp von B konvertiert oder, wenn A 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 I3die 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