Freigeben über


Standardschnittstellenmethoden

Hinweis

Dieser Artikel ist eine Feature-Spezifikation. 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 Feature-Spezifikation 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.

Champion Issue: https://github.com/dotnet/csharplang/issues/52

Zusammenfassung

Fügen Sie Unterstützung für virtuelle Erweiterungsmethoden hinzu, Methoden in Schnittstellen mit konkreten Implementierungen. Eine Klasse oder Struktur, die eine solche Schnittstelle implementiert, muss eine einzelne spezifischste Implementierung für die Schnittstellenmethode aufweisen, die entweder von der Klasse oder Struktur implementiert oder von den Basisklassen oder Schnittstellen geerbt wird. Virtuelle Erweiterungsmethoden ermöglichen es einem API-Autor, in zukünftigen Versionen Methoden zu einer Schnittstelle hinzuzufügen, ohne die Quellcode- oder Binärkompatibilität mit bestehenden Implementierungen dieser Schnittstelle fehlerhaft zu machen.

Sie ähneln den "Standardmethoden" von Java.

(Basierend auf der wahrscheinlichen Implementierungstechnik) erfordert diese Funktion eine entsprechende Unterstützung in der CLI/CLR. Programme, die sich diese Funktion zunutze machen, können nicht auf früheren Versionen der Plattform ausgeführt werden.

Motivation

Die prinzipiellen Beweggründe für diese Funktion sind

  • Standardschnittstellenmethoden ermöglichen es einem API-Autor, in zukünftigen Versionen Methoden zu einer Schnittstelle hinzuzufügen, ohne die Quellcode- oder Binärdateienkompatibilität mit bestehenden Implementierungen dieser Schnittstelle fehlerhaft zu machen.
  • Das Feature ermöglicht es C#, mit APIs zu interoperieren, die auf Android (Java) und iOS (Swift)abzielen, welche ähnliche Features unterstützen.
  • Wie sich herausstellt, werden beim Hinzufügen von Standardschnittstellenimplementierungen die Elemente des traits-Sprachfeatures bereitgestellt (https://en.wikipedia.org/wiki/Trait_(computer_programming)). Traits haben sich als leistungsstarke Programmiertechnik erwiesen (http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf).

Detailliertes Design

Die Syntax für eine Schnittstelle wird erweitert, um Folgendes zu ermöglichen

  • Memberdeklarationen, die Konstanten, Operatoren, statische Konstruktoren und geschachtelte Typen deklarieren
  • ein Textkörper für eine Methode oder einen Indexer, eine Eigenschaft oder ein Ereignisaccessor (d. h. eine „Standardimplementierung“)
  • Memberdeklarationen, die statische Felder, Methoden, Eigenschaften, Indexer und Ereignisse deklarieren
  • Memberdeklarationen, die die Syntax der expliziten Schnittstellenimplementierung verwenden
  • Explizite Zugriffsmodifizierer (der Standardzugriff ist public)

Member mit Text ermöglichen der Schnittstelle, eine „Standardimplementierung“ für die Methode in Klassen und Strukturen bereitzustellen, die keine eigene Implementierung bereitstellen.

Schnittstellen dürfen keinen Instanzstatus enthalten. Obwohl statische Felder jetzt zulässig sind, sind Instanzfelder in Schnittstellen nicht zulässig. Automatische Eigenschaften von Instanzen werden in Schnittstellen nicht unterstützt, da sie implizit ein ausgeblendetes Feld deklarieren würden.

Statische und private Methoden ermöglichen ein nützliches Refactoring und die Organisation des Codes, der zur Implementierung der öffentlichen API der Schnittstelle verwendet wird.

Eine Methode, die in einer Schnittstelle überschrieben wird, muss die explizite Syntax für die Implementierung der Schnittstelle verwenden.

Es ist ein Fehler, einen Klassentyp, einen Strukturtyp oder einen Enumerationstyp innerhalb des Bereichs eines Typparameters zu deklarieren, der mit variance_annotation deklariert wurde. Die Deklaration von C unten ist zum Beispiel ein Fehler.

interface IOuter<out T>
{
    class C { } // error: class declaration within the scope of variant type parameter 'T'
}

Konkrete Methoden in Schnittstellen

Die einfachste Form dieses Features ist die Möglichkeit, eine konkrete Methode in einer Schnittstelle zu deklarieren, bei der es sich um eine Methode mit einem Textkörper handelt.

interface IA
{
    void M() { WriteLine("IA.M"); }
}

Eine Klasse, die diese Schnittstelle implementiert, muss ihre konkrete Methode nicht implementieren.

class C : IA { } // OK

IA i = new C();
i.M(); // prints "IA.M"

Die endgültige Außerkraftsetzung für IA.M in der Klasse C ist die konkrete Methode M, die in IA deklariert ist. Beachten Sie, dass eine Klasse keine Member von ihren Schnittstellen erbt, und dieses Verhalten wird durch dieses Feature nicht geändert.

new C().M(); // error: class 'C' does not contain a member 'M'

Innerhalb eines Instanzmembers einer Schnittstelle weist this den Typ der einschließenden Schnittstelle auf.

Modifizierer in Schnittstellen

Die Syntax für eine Schnittstelle wird erweitert, um Modifizierer für ihre Member zuzulassen. Erlaubt sind die folgenden: private, protected, internal, public, virtual, abstract, sealed, static, externund partial.

Ein Schnittstellenelement, dessen Deklaration einen Rumpf enthält, ist ein virtual-Element, es sei denn, dass der sealed- oder private-Modifizierer verwendet wird. Der Modifizierer virtual kann für ein Funktionsmember verwendet werden, das andernfalls implizit virtual wäre. Auch wenn abstract die Standardeinstellung für Schnittstellenmember ohne Textkörper ist, kann dieser Modifizierer explizit angegeben werden. Ein nicht-virtuelles Mitglied kann mit dem Schlüsselwort sealed deklariert werden.

Es ist ein Fehler, wenn ein private- oder sealed-Funktionsmember einer Schnittstelle keinen Textkörper hat. Ein private-Funktionsmember darf nicht den Modifizierer sealed haben.

Zugriffsmodifizierer können für Schnittstellenmember aller Arten von Membern verwendet werden, die zulässig sind. Die Zugriffsebene public ist der Standard, kann aber explizit angegeben werden.

Offenes Problem: Wir müssen die genaue Bedeutung der Zugriffsmodifizierer wie protected und internal angeben und festlegen, welche Deklarationen sie (in einer abgeleiteten Schnittstelle) überschreiben oder (in einer Klasse, die die Schnittstelle implementiert) implementieren.

Schnittstellen können static-Member deklarieren, einschließlich geschachtelter Typen, Methoden, Indexer, Eigenschaften, Ereignisse und statischer Konstruktoren. Die Standardzugriffsebene für alle Schnittstellenmitglieder ist public.

Interfaces dürfen keine Instanz-Konstruktoren, Destruktoren oder Felder deklarieren.

Geschlossenes Issue: Sollte die Deklaration von Operatoren in einer Schnittstelle erlaubt sein? Wahrscheinlich keine Konvertierungsoperatoren, aber wie sieht es mit anderen aus? Entscheidung: Operatoren sind zulässig, außer für Konvertierungs-, Gleichheits- und Ungleichheitsoperatoren.

Geschlossenes Problem: Sollte new in Schnittstellenmemberdeklarationen zulässig sein, die Member in Basisschnittstellen ausblenden? Entscheidung: Ja

Geschlossenes Problem: partial ist derzeit für eine Schnittstelle oder ihre Member nicht zulässig. Das würde einen separaten Vorschlag erfordern. Entscheidung: Ja https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#permit-partial-in-interface

Explizite Implementierung in Schnittstellen

Explizite Implementierungen ermöglichen es Programmierenden, eine spezifischste Implementierung eines virtuellen Members in einer Schnittstelle bereitzustellen, bei der der Compiler oder die Laufzeit andernfalls keins finden würde. Eine Implementierungsdeklaration darf explizit eine bestimmte Basisschnittstellenmethode implementieren, indem die Deklaration mit dem Schnittstellennamen qualifiziert wird (in diesem Fall ist kein Zugriffsmodifizierer zulässig). Implizite Implementierungen sind nicht erlaubt.

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
}

Explizite Implementierungen in Schnittstellen können nicht als sealed deklariert werden.

Öffentliche virtual-Funktionsmember in einer Schnittstelle können nur explizit in einer abgeleiteten Schnittstelle implementiert werden (durch das Qualifizieren des Namens in der Deklaration mit dem Schnittstellentyp, der die Methode ursprünglich deklariert hat, und das Weglassen eines Zugriffsmodifizierers). Das Member muss dort zugänglich sein, wo es implementiert wird.

Erneute Abstraktion

Eine in einer Schnittstelle deklarierte virtuelle (konkrete) Methode kann in einer abgeleiteten Schnittstelle reabstrahiert werden.

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

Der abstract-Modifizierer ist in der Deklaration von IB.M erforderlich, um anzugeben, dass IA.M erneut abstrahiert wird.

Dies ist in abgeleiteten Schnittstellen nützlich, wenn die Standardimplementierung einer Methode unangemessen ist und eine geeignetere Implementierung durch implementierende Klassen bereitgestellt werden sollte.

Die Regel der spezifischsten Implementierung

Es ist erforderlich, dass jede Schnittstelle und jede Klasse eine spezifischste Implementierung für jedes virtuelle Member in den Implementierungen haben, die im Typ oder seinen direkten und indirekten Schnittstellen vorkommen. Die spezifischste Implementierung ist eine eindeutige Implementierung, die spezifischer ist als jede andere Implementierung. Wenn keine Implementierung vorhanden ist, wird das Mitglied selbst als die spezifischste Implementierung betrachtet.

Die Implementierung M1 wird als spezifischer als eine andere Implementierung M2 betrachtet, wenn M1 für den Typ T1 und M2 für den Typ T2 deklariert wird, und eine der folgenden Bedingungen zutrifft:

  1. T1 enthält T2 in den direkten oder indirekten Schnittstellen.
  2. T2 ist ein Schnittstellentyp, aber T1 ist kein Schnittstellentyp.

Zum Beispiel:

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'

Die spezifischste Implementierungsregel stellt sicher, dass ein Konflikt (d. h. eine Mehrdeutigkeit, die sich aus der rautenförmigen Vererbung ergibt) explizit von den Programmierenden genau an der Stelle gelöst wird, wo der Konflikt entsteht.

Da wir explizite Reabstraktionen in Schnittstellen unterstützen, könnten wir dies auch in Klassen tun

abstract class E : IA, IB, IC // ok
{
    abstract void IA.M();
}

Geschlossene Frage: Sollten wir explizite abstrakte Implementierungen von Schnittstellen in Klassen unterstützen? Entscheidung: NEIN

Darüber hinaus ist es ein Fehler, wenn in einer Klassendeklaration die spezifischste Implementierung einer Schnittstellenmethode eine abstrakte Implementierung ist, die in einer Schnittstelle deklariert wurde. Dies ist eine bestehende Regel, die unter Verwendung der neuen Terminologie neu formuliert wurde.

interface IF
{
    void M();
}
abstract class F : IF { } // error: 'F' does not implement 'IF.M'

Es ist möglich, dass eine in einer Schnittstelle deklarierte virtuelle Eigenschaft über eine spezifischste Implementierung für den get-Accessor in einer Schnittstelle und eine spezifischste Implementierung für den set-Accessor in einer anderen Schnittstelle verfügt. Dies wird als Verstoß gegen die Regel der spezifischsten Implementierung angesehen und verursacht einen Compilerfehler.

Die Methoden static und private

Da Schnittstellen nun ausführbaren Code enthalten können, ist es sinnvoll, gemeinsamen Code in private und statische Methoden zu abstrahieren. Wir gestatten diese nun in Schnittstellen.

Geschlossene Frage: Sollten wir private Methoden unterstützen? Sollten wir statische Methoden unterstützen? Entscheidung: JA

Offenes Problem: Sollen Schnittstellenmethoden als protected oder internal oder mit einem anderen Zugriff zulässig sein? Falls ja, was bedeutet das? Sind sie standardmäßig virtual? Wenn ja, gibt es eine Möglichkeit, sie nicht-virtuell zu machen?

Geschlossene Frage: Wenn wir statische Methoden unterstützen, sollten wir dann auch (statische) Operatoren unterstützen? Entscheidung: JA

Aufruf von Basisschnittstellen

Die Syntax in diesem Abschnitt ist noch nicht implementiert worden. Es bleibt ein aktiver Vorschlag.

Code in einem Typ, der von einer Schnittstelle mit einer Standardmethode abgeleitet wird, kann explizit die Basisimplementierung dieser Schnittstelle aufrufen.

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(); }
}

Eine (nicht statische) Instanzmethode darf die Implementierung einer zugänglichen Instanzmethode in einer direkten Basisschnittstelle auf nicht virtuelle Weise aufrufen, indem sie anhand der Syntax base(Type).M benannt wird. Dies ist nützlich, wenn eine Außerkraftsetzung, die aufgrund der rautenförmigen Vererbung erforderlich ist, durch Delegieren an eine bestimmte Basisimplementierung aufgelöst wird.

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(); }
}

Wenn auf ein virtual- oder abstract-Member mithilfe der Syntax base(Type).M zugegriffen wird, ist es erforderlich, dass Type eine eindeutige spezifischste Außerkraftsetzung für M enthält.

Verbindliche Basisklauseln

Schnittstellen enthalten jetzt Typen. Diese Typen können in der Basisklausel als Basisschnittstellen verwendet werden. Beim Binden einer Basisklausel müssen wir möglicherweise die Basisschnittstellen zum Binden dieser Typen kennen (z. B. zum Suchen und zum Auflösen des geschützten Zugriffs). Die Bedeutung der Basisklausel einer Schnittstelle ist somit zirkulär definiert. Um den Zyklus zu unterbrechen, fügen wir eine neue Sprachregel hinzu, die einer ähnlichen Regel entspricht, die bereits für Klassen gilt.

Beim Bestimmen der Bedeutung von interface_base einer Schnittstelle wird vorübergehend angenommen, dass die Basisschnittstellen leer sind. Intuitiv wird dadurch sichergestellt, dass die Bedeutung einer Basisklausel nicht rekursiv von sich selbst abhängen kann.

Früher galten die folgenden Regeln:

„Wenn eine Klasse B von einer Klasse A abgeleitet wird, tritt ein Kompilierungsfehler auf, wenn A von B abhängig ist. Eine Klasse ist direkt abhängig von ihrer direkten Basisklasse (sofern vorhanden) und direkt abhängig von der Klasse, in der sie unmittelbar geschachtelt ist (sofern vorhanden). Angesichts dieser Definition ist der vollständige Satz von Klassen, von denen eine Klasse abhängt, der reflexive und transitive Abschluss der Beziehung direkt abhängig von.“

Es ist ein Kompilierungsfehler, wenn eine Schnittstelle direkt oder indirekt von sich selbst erbt. Die Basisschnittstellen einer Schnittstelle sind die expliziten Basisschnittstellen sowie ihre Basisschnittstellen. Anders ausgedrückt: Der Satz von Basisschnittstellen ist der vollständige transitive Abschluss der expliziten Basisschnittstellen, ihrer expliziten Basisschnittstellen usw.

Wir passen sie wie folgt an:

Wenn eine Klasse B von einer Klasse A abgeleitet wird, tritt ein Kompilierungsfehler auf, wenn A von B abhängig ist. Eine Klasse ist direkt abhängig von ihrer direkten Basisklasse (sofern vorhanden) und direkt abhängig von dem Typ, in dem sie unmittelbar geschachtelt ist (sofern vorhanden).

Wenn eine Schnittstelle IB eine Schnittstelle IA erweitert, ist es ein Kompilierungszeitfehler, wenn IA von IB abhängig ist. Eine Schnittstelle ist direkt abhängig von ihren direkten Basisschnittstellen (sofern vorhanden) und direkt abhängig von dem Typ, in dem sie unmittelbar geschachtelt ist (sofern vorhanden).

Angesichts dieser Definitionen ist der vollständige Satz von Typen, von denen ein Typ abhängt, der reflexive und transitive Abschluss der Beziehung direkt abhängig von.

Gültig bis für bestehende Programme

Die hier vorgestellten Regeln sollen keine Auswirkungen auf die Bedeutung bestehender Programme haben.

Beispiel 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
}

Beispiel 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
}

Die gleichen Regeln führen zu ähnlichen Ergebnissen in der analogen Situation mit Standardschnittstellenmethoden:

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
}

Geschlossenes Issue: Bestätigen der Tatsache, dass dies eine beabsichtigte Folge der Spezifikation ist. Entscheidung: JA

Auflösung der Laufzeitmethode

Geschlossenes Problem: Die Spezifikation sollte den Algorithmus zur Auflösung der Laufzeitmethoden angesichts der Schnittstellenstandardmethoden beschreiben. Wir müssen sicherstellen, dass die Semantik konsistent mit der Sprachsemantik ist, zum Beispiel, welche deklarierten Methoden eine internal-Methode überschreiben oder implementieren, und welche nicht.

CLR-Support-API

Damit Compiler erkennen können, dass sie gerade für eine Laufzeitumgebung kompilieren, die dieses Feature unterstützt, werden die Bibliotheken dieser Laufzeitumgebungen so angepasst, dass sie dies über die in https://github.com/dotnet/corefx/issues/17116erläuterte API anzeigen. Wir fügen Folgendes hinzu:

namespace System.Runtime.CompilerServices
{
    public static class RuntimeFeature
    {
        // Presence of the field indicates runtime support
        public const string DefaultInterfaceImplementation = nameof(DefaultInterfaceImplementation);
    }
}

Offenes Problem: Ist das der beste Name für die CLR-Funktion? Die CLR-Funktion hat viel mehr Funktionen (z. B. Lockern von Schutzeinschränkungen, Unterstützung von Außerkraftsetzungen in Schnittstellen usw.). Vielleicht sollte es so etwas wie "konkrete Methoden in Schnittstellen" oder "Traits" genannt werden?

Weitere zu spezifizierende Bereiche

  • Es wäre nützlich, die Arten von Kompatibilitätseffekten im Quell- und Binärcode zu katalogisieren, die durch das Hinzufügen von Standardschnittstellenmethoden und Überschreibungen zu vorhandenen Schnittstellen verursacht werden.

Nachteile

Dieser Vorschlag erfordert eine koordinierte Aktualisierung der CLR-Spezifikation (zur Unterstützung konkreter Methoden in Schnittstellen und der Methodenauflösung). Es ist daher ziemlich aufwendig, und es kann sich lohnen, diesen Schritt in Kombination mit anderen Features auszuführen, bei denen ebenfalls CLR-Änderungen erwartet werden.

Alternativen

Keine.

Ungelöste Fragen

  • Offene Fragen sind oben im Vorschlag hervorgehoben.
  • Siehe auch https://github.com/dotnet/csharplang/issues/406 für eine Liste der offenen Fragen.
  • Die detaillierte Spezifikation muss den Auflösungsmechanismus beschreiben, der zur Laufzeit verwendet wird, um die genaue Methode auszuwählen, die aufgerufen werden soll.
  • Die Interaktion von Metadaten, die von neuen Compilern erzeugt und von älteren Compilern konsumiert werden, muss im Detail ausgearbeitet werden. Wir müssen zum Beispiel sicherstellen, dass die von uns verwendete Metadaten-Darstellung nicht dazu führt, dass die Hinzufügung einer Standardimplementierung in einer Schnittstelle eine bestehende Klasse, die diese Schnittstelle implementiert, fehlerhaft macht, wenn sie von einem älteren Compiler kompiliert wird. Dies kann sich auf die Darstellung der Metadaten auswirken, die wir verwenden können.
  • Der Entwurf muss die Interoperabilität mit anderen Sprachen und bestehenden Compilern für andere Sprachen berücksichtigen.

Gelöste Fragen

Abstrakte Außerkraftsetzung

Die frühere Entwurfsspezifikation enthielt die Möglichkeit, eine geerbte Methode erneut zu abstrahieren.

interface IA
{
    void M();
}
interface IB : IA
{
    override void M() { }
}
interface IC : IB
{
    override void M(); // make it abstract again
}

Aus meinen Anmerkungen vom 20.03.2017 geht hervor, dass wir beschlossen haben, dies nicht zuzulassen. Es gibt jedoch mindestens zwei Anwendungsfälle dafür:

  1. Die Java-APIs, mit denen einige Benutzer dieser Funktion zu interagieren hoffen, hängen von dieser Funktion ab.
  2. Dies ist für die Programmierung mit Traits vorteilhaft. Die erneute Abstraktion ist eins der Elemente des traits-Sprachfeatures (https://en.wikipedia.org/wiki/Trait_(computer_programming)). Folgendes ist bei Klassen zulässig:
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
}

Leider kann dieser Code nicht als Set von Schnittstellen (Traits) refaktorisiert werden, wenn dies nicht erlaubt ist. Gemäß dem Jared-Prinzip der Gier (Jared principle of greed) sollte es erlaubt sein.

Geschlossenes Issue: Sollte die Reabstraktion erlaubt sein? [JA] Meine Anmerkungen waren falsch. In den LDM-Hinweisen ist angegeben, dass eine erneute Abstraktion in einer Schnittstelle zulässig ist. In einer Klasse ist dies nicht der Fall.

Virtueller Modifizierer und versiegelter Modifizierer

Von Aleksey Tsingauz:

Wir haben beschlossen, explizit für Schnittstellenmember angegebene Modifizierer zuzulassen, es sei denn, es gibt einen Grund dafür, einige davon nicht zuzulassen. Dies wirft eine interessante Frage zum virtuellen Modifizierer auf. Ist es für Mitglieder mit Standardimplementierung erforderlich?

Wir könnten das sagen:

  • Wenn keine Implementierung vorhanden und weder „virtuell“ noch „versiegelt“ angegeben ist, wird davon ausgegangen, dass das Member abstrakt ist.
  • Wenn eine Implementierung vorhanden und weder „abstrakt“ noch „versiegelt“ angegeben ist, wird davon ausgegangen, dass das Member virtuell ist.
  • Der versiegelte Modifizierer ist erforderlich, damit eine Methode weder als virtuell noch als abstrakt festgelegt werden kann.

Alternativ könnte man sagen, dass der virtuelle Modifizierer für ein virtuelles Member erforderlich ist. Wenn also ein Member mit Implementierung nicht explizit durch den virtuellen Modifizierer gekennzeichnet ist, dann ist es weder virtuell noch abstrakt. Dieser Ansatz könnte eine bessere Erfahrung bieten, wenn eine Methode von einer Klasse zu einer Schnittstelle verschoben wird:

  • eine abstrakte Methode bleibt abstrakt.
  • eine virtuelle Methode bleibt virtuell.
  • eine Methode ohne Modifikator bleibt weder virtuell noch abstrakt.
  • Der versiegelte Modifizierer kann nicht auf eine Methode angewendet werden, die keine Außerkraftsetzung ist.

Was halten Sie davon?

Geschlossenes Problem: Sollte eine konkrete Methode (mit Implementierung) implizit virtual sein? [JA]

Entscheidungen: Getroffen in LDM am 05.04.2017:

  1. „nicht virtuell“ muss explizit durch sealed oder private ausgedrückt werden.
  2. sealed ist das Schlüsselwort, um Schnittstelleninstanzmember mit Textkörpern als nicht virtuell festzulegen.
  3. Wir möchten alle Modifikatoren in Schnittstellen zulassen
  4. Der Standardzugriff für Schnittstellenmember ist öffentlich, einschließlich geschachtelter Typen.
  5. Private Funktionsmember in Schnittstellen sind implizit versiegelt, und sealed ist für sie nicht zulässig.
  6. Private Klassen (in Schnittstellen) sind erlaubt und können versiegelt werden, und zwar versiegelt im Sinne von Klasse und versiegelt.
  7. In Ermangelung eines guten Vorschlags ist „partial“ nach wie vor nicht für Schnittstellen oder ihre Member zulässig.

Binäre Kompatibilität 1

Wenn eine Bibliothek eine Standardimplementierung bietet

interface I1
{
    void M() { Impl1 }
}
interface I2 : I1
{
}
class C : I2
{
}

Wir wissen, dass I1.M die Implementierung von I1.M in C ist. Was passiert, wenn die Assembly, die I2 enthält, wie folgt geändert und neu kompiliert wird

interface I2 : I1
{
    override void M() { Impl2 }
}

aber C wird nicht neu kompiliert. Was passiert, wenn das Programm ausgeführt wird? Ein Aufruf von (C as I1).M()

  1. führt I1.M aus.
  2. führt I2.M aus.
  3. löst eine Art Laufzeitfehler aus.

Entscheidung: Getroffen am 11.04.2017: Führt I2.M aus, was die eindeutig spezifischste Außerkraftsetzung zur Ausführungszeit ist.

Ereignisaccessoren (geschlossen)

Geschlossenes Problem: Kann ein Ereignis „stückweise“ außer Kraft gesetzt werden?

Betrachten wir diesen Fall:

public interface I1
{
    event T e1;
}
public interface I2 : I1
{
    override event T
    {
        add { }
        // error: "remove" accessor missing
    }
}

Diese „partielle“ Implementierung des Ereignisses ist nicht zulässig, da die Syntax für eine Ereignisdeklaration wie in einer Klasse keinen einzelnen Accessor zulässt. Es müssen beide (oder keiner) bereitgestellt werden. Sie können dasselbe erreichen, indem Sie durch Weglassen eines Textkörpers zulassen, dass der abstrakte remove-Accessor in der Syntax implizit abstrakt ist.

public interface I1
{
    event T e1;
}
public interface I2 : I1
{
    override event T
    {
        add { }
        remove; // implicitly abstract
    }
}

Beachten Sie, dass dies eine neue (vorgeschlagene) Syntax ist. In der aktuellen Grammatik verfügen Ereignisaccessoren über einen obligatorischen Textkörper.

Geschlossenes Problem: Kann ein Ereignisaccessor durch das Weglassen eines Textkörpers (implizit) abstrakt sein, ähnlich wie Methoden in Schnittstellen und Eigenschaftsaccessoren durch das Weglassen eines Textkörpers (implizit) abstrakt sind?

Entscheidung: (18.04.2017) Nein, Ereignisdeklarationen erfordern beide konkrete Accessoren (oder keinen).

Erneute Abstraktion in einer Klasse (geschlossen)

Geschlossenes Issue: Wir sollten bestätigen, dass dies zulässig ist (andernfalls wäre das Hinzufügen einer Standard Implementierung eine fehlerhafte Änderung):

interface I1
{
    void M() { }
}
abstract class C : I1
{
    public abstract void M(); // implement I1.M with an abstract method in C
}

Entscheidung: (18.04.2017) Ja, das Hinzufügen eines Textkörpers zu einer Schnittstellenmemberdeklaration sollte C nicht beeinträchtigen.

Versiegelte Außerkraftsetzung (geschlossen)

Die vorherige Frage geht implizit davon aus, dass der sealed-Modifizierer auf override in einer Schnittstelle angewendet werden kann. Dies widerspricht dem Entwurf der Spezifikation. Möchten wir die Versiegelung einer Außerkraftsetzung zulassen? Die Auswirkungen der Versiegelung auf die Quell- und Binärkompatibilität müssen berücksichtigt werden.

Geschlossenes Problem: Sollten wir die Versiegelung einer Außerkraftsetzung zulassen?

Entscheidung: (18.04.2017) Wir sollten sealed bei Außerkraftsetzungen in Schnittstellen nicht zulassen. Die einzige Verwendung von sealed für Schnittstellenmember besteht darin, sie in ihrer anfänglichen Deklaration als „nicht virtuell“ festzulegen.

Rautenförmige Vererbung und Klassen (geschlossen)

Der Entwurf des Vorschlags bevorzugt Klassenüberschreibungen gegenüber Schnittstellenüberschreibungen in Szenarien mit rautenförmiger Vererbung:

Wir setzen voraus, dass jede Schnittstelle und jede Klasse für jede Schnittstellenmethode in den Außerkraftsetzungen, die im Typ oder in dessen direkten und indirekten Schnittstellen vorkommen, eine spezifischste Außerkraftsetzung haben. Die spezifischste Außerkraftsetzung ist eine einzigartig spezifische Außerkraftsetzung, die spezifischer als alle anderen Außerkraftsetzungen ist. Wenn keine Außerkraftsetzung vorhanden ist, wird die Methode selbst als die spezifischste Außerkraftsetzung betrachtet.

Die Außerkraftsetzung M1 wird als spezifischer als eine andere Außerkraftsetzung M2 betrachtet, wenn M1 für den Typ T1 und M2 für den Typ T2 deklariert wird, und eine der folgenden Bedingungen zutrifft:

  1. T1 enthält T2 in den direkten oder indirekten Schnittstellen.
  2. T2 ist ein Schnittstellentyp, aber T1 ist kein Schnittstellentyp.

Das Szenario ist folgendes

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?
    }
}

Wir sollten dieses Verhalten bestätigen (oder anders entscheiden)

Geschlossenes Problem: Überprüfen Sie die oben genannte Entwurfsspezifikation auf die spezifischste Außerkraftsetzung, da sie für gemischte Klassen und Schnittstellen gilt (eine Klasse hat Vorrang vor einer Schnittstelle). Siehe https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#diamonds-with-classes.

Interface-Methoden vs. Structs (geschlossen)

Es gibt einige unglückliche Wechselwirkungen zwischen Standard-Interface-Methoden und Structs.

interface IA
{
    public void M() { }
}
struct S : IA
{
}

Beachten Sie, dass Schnittstellenmitglieder nicht geerbt werden:

var s = default(S);
s.M(); // error: 'S' does not contain a member 'M'

Folglich muss der Client die Struktur kapseln, um die Schnittstellenmethoden aufzurufen.

IA s = default(S); // an S, boxed
s.M(); // ok

Derartiges Kapseln macht die wesentlichen Vorteile eines struct-Typs zunichte. Darüber hinaus haben Mutationsmethoden keine offensichtliche Auswirkung, da sie eine gekapselte Kopie der Struktur nutzen:

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

Geschlossenes Issue: Was können wir dagegen tun:

  1. Verhindern, dass struct eine Standardimplementierung erbt. Alle Schnittstellenmethoden werden in struct als abstrakt behandelt. Dann nehmen wir uns später Zeit, um zu überlegen, wie wir es besser funktionieren lassen können.
  2. Entwerfen einer Codegenerierungsstrategie, die Boxing vermeidet. Innerhalb einer Methode wie IB.Incrementkönnte der Typ von this vielleicht einem Typparameter ähneln, der auf IBeingeschränkt ist. Im Zusammenhang damit werden nicht abstrakte Methoden von Schnittstellen geerbt, um Boxing der aufrufenden Funktion zu vermeiden. Dies kann den Compiler- und CRL-Implementierungsaufwand erheblich erhöhen.
  3. Machen Sie sich keine Gedanken darüber, und nehmen Sie es als Schwachstelle hin.
  4. Andere Ideen?

Entscheidung: Machen Sie sich keine Gedanken darüber, und nehmen Sie es als Schwachstelle hin. Siehe https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#structs-and-default-implementations.

Aufrufe der Basisschnittstelle (geschlossen)

Diese Entscheidung wurde in C# 8 nicht implementiert. Die Syntax base(Interface).M() ist nicht implementiert.

Der Entwurf der Spezifikation schlägt eine von Java inspirierte Syntax für Basis-Interface-Aufrufe vor: Interface.base.M(). Wir müssen uns für eine Syntax entscheiden, zumindest für den ersten Prototyp. Mein Favorit ist base<Interface>.M().

Geschlossenes Problem: Wie lautet die Syntax für einen Basismemberaufruf?

Entscheidung: Die Syntax ist base(Interface).M(). Siehe https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#base-invocation. Die so benannte Schnittstelle muss eine Basisschnittstelle sein, muss aber nicht direkt eine Basisschnittstelle sein.

Offenes Problem: Sollten Aufrufe der Basisschnittstelle in Klassenmembern zulässig sein?

Entscheidung: Ja https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#base-invocation

Außerkraftsetzen nicht öffentlicher Schnittstellenmember (geschlossen)

In einer Schnittstelle werden nicht öffentliche Member aus Basisschnittstellen mithilfe des override-Modifizierers außer Kraft gesetzt. Wenn es sich um eine „explizite“ Außerkraftsetzung handelt, bei der die Schnittstelle, die das Member enthält, benannt wird, wird der Zugriffsmodifizierer weggelassen.

Geschlossenes Problem: Muss der Zugriffsmodifizierer übereinstimmen, wenn es sich um eine „implizite“ Außerkraftsetzung handelt, die die Schnittstelle nicht benennt?

Entscheidung: Nur öffentliche Member können implizit außer Kraft gesetzt werden, und der Zugriff muss übereinstimmen. Siehe https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-a-non-public-interface-member-not-in-list.

Offenes Problem: Ist der Zugriffsmodifizierer erforderlich oder optional, oder wird er bei einer expliziten Außerkraftsetzung wie override void IB.M() {} weggelassen?

Offenes Problem: Ist das override-Element erforderlich oder optional, oder wird es bei einer expliziten Außerkraftsetzung wie void IB.M() {} weggelassen?

Wie implementiert man ein nicht-öffentliches Mitglied der Schnittstelle in einer Klasse? Muss das vielleicht explizit gemacht werden?

interface IA
{
    internal void MI();
    protected void MP();
}
class C : IA
{
    // are these implementations?  Decision: NO
    internal void MI() {}
    protected void MP() {}
}

Geschlossenes Problem: Wie wird ein nicht öffentliches Schnittstellenmember in einer Klasse implementiert?

Entscheidung: Sie können nur nicht öffentliche Schnittstellenmember explizit implementieren. Siehe https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-a-non-public-interface-member-not-in-list.

Entscheidung: Das Schlüsselwort override darf nicht für Schnittstellenmember verwendet werden. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#does-an-override-in-an-interface-introduce-a-new-member

Binärkompatibilität 2 (geschlossen)

Betrachten Sie den folgenden Code, in dem sich jeder Typ in einer eigenen Assembly befindet

interface I1
{
    void M() { Impl1 }
}
interface I2 : I1
{
    override void M() { Impl2 }
}
interface I3 : I1
{
}
class C : I2, I3
{
}

Wir wissen, dass I2.M die Implementierung von I1.M in C ist. Was passiert, wenn die Assembly, die I3 enthält, wie folgt geändert und neu kompiliert wird

interface I3 : I1
{
    override void M() { Impl3 }
}

aber C wird nicht neu kompiliert. Was passiert, wenn das Programm ausgeführt wird? Ein Aufruf von (C as I1).M()

  1. führt I1.M aus.
  2. führt I2.M aus.
  3. führt I3.M aus.
  4. Entweder 2 oder 3, deterministisch
  5. löst eine Art Laufzeitausnahme aus.

Entscheidung: Auslösen einer Ausnahme (5). Siehe https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#issues-in-default-interface-methods.

Soll partial in einer Schnittstelle zugelassen werden? (geschlossen)

Angesichts der Tatsache, dass Schnittstellen auf ähnliche Weise verwendet werden können wie abstrakte Klassen, kann es sinnvoll sein, sie zu deklarieren partial. Dies wäre besonders nützlich für Generatoren.

Vorschlag: Entfernen Sie die Spracheinschränkung, dass Schnittstellen und Member von Schnittstellen nicht als partial deklariert werden dürfen.

Entscheidung: Ja Siehe https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#permit-partial-in-interface.

Main in einer Schnittstelle? (geschlossen)

Offenes Problem: Ist eine static Main-Methode in einer Schnittstelle ein Kandidat für den Einstiegspunkt des Programms?

Entscheidung: Ja Siehe https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#main-in-an-interface.

Bestätigen der Absicht, öffentliche nicht virtuelle Methoden zu unterstützen (geschlossen)

Können wir unsere Entscheidung bestätigen (oder rückgängig machen), dass nicht virtuelle öffentliche Methoden in einer Schnittstelle zugelassen werden?

interface IA
{
    public sealed void M() { }
}

Semi-Closed Issue: (2017-04-18) Wir glauben, dass es nützlich sein wird, aber wir werden darauf zurückkommen. Dies ist eine Stolperfalle.

Entscheidung: Ja https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#confirm-that-we-support-public-non-virtual-methods.

Führt override in einer Schnittstelle ein neues Member ein? (geschlossen)

Es gibt einige Möglichkeiten zu überprüfen, ob eine Außerkraftsetzungsdeklaration ein neues Member einführt oder nicht.

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

Offenes Problem: Führt eine Außerkraftsetzungsdeklaration in einer Schnittstelle ein neues Member ein? (geschlossen)

In einer Klasse ist eine Außerkraftsetzungsmethode in gewisser Weise „sichtbar“. Beispielsweise haben die Namen ihrer Parameter Vorrang vor den Namen von Parametern in der außer Kraft gesetzten Methode. Es ist möglich, dieses Verhalten in Schnittstellen zu duplizieren, da es immer eine spezifischste Außerkraftsetzung gibt. Aber wollen wir dieses Verhalten duplizieren?

Ist es außerdem möglich, eine Außerkraftsetzungsmethode „außer Kraft zu setzen“? [Strittig]

Entscheidung: Das Schlüsselwort override darf nicht für Schnittstellenmember verwendet werden. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#does-an-override-in-an-interface-introduce-a-new-member.

Eigenschaften mit einem privaten Accessor (geschlossen)

Wir sagen, dass private Mitglieder nicht virtuell sind und dass die Kombination von virtuell und privat nicht zulässig ist. Aber was ist mit einer Eigenschaft mit einem privaten Accessor?

interface IA
{
    public virtual int P
    {
        get => 3;
        private set { }
    }
}

Ist dies zulässig? Ist der set-Accessor hier auf virtual festgelegt oder nicht? Kann er außer Kraft gesetzt werden, wo er zugänglich ist? Implementiert Folgendes implizit nur den get-Accessor?

class C : IA
{
    public int P
    {
        get => 4;
        set { }
    }
}

Ergibt Folgendes vermutlich einen Fehler, weil „IA.P.set“ nicht virtuell und auch nicht zugänglich ist?

class C : IA
{
    int IA.P
    {
        get => 4;
        set { } // Decision: Not valid
    }
}

Entscheidung: Das erste Beispiel sieht gültig aus, während das letzte nicht gültig ist. Dies wird analog dazu gelöst, wie es bereits in C# funktioniert. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#properties-with-a-private-accessor

Aufrufe der Basisschnittstelle, Runde 2 (geschlossen)

Dies wurde in C# 8 nicht implementiert.

Unsere vorherige „Lösung“ zur Behandlung von Basisaufrufen ist nicht wirklich aussagekräftig genug. Es hat sich herausgestellt, dass Sie in C# und der CLR im Gegensatz zu Java sowohl die Schnittstelle, die die Methodendeklaration enthält, als auch den Speicherort der Implementierung, die Sie aufrufen möchten, angeben müssen.

Ich schlage die folgende Syntax für Basisaufrufe in Schnittstellen vor. Ich bin nicht begeistert davon, aber es veranschaulicht, was jede Syntax ausdrücken können muss.

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
    }
}

Wenn es keine Zweideutigkeit gibt, können Sie es auch einfacher schreiben

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
    }
}

Oder

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
    }
}

Oder

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
    }
}

Entscheidung: Wir haben uns für base(N.I1<T>).M(s) entschieden, wobei wir einräumen, dass es hier später Probleme geben kann, wenn eine Aufrufbindung vorliegt.https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-11-14.md#default-interface-implementations

Warnung, dass eine Struktur die Standardmethode nicht implementiert? (geschlossen)

@vancem bestätigt, dass wir ernsthaft in Erwägung ziehen sollten, eine Warnung auszugeben, wenn eine Werttypdeklaration eine Schnittstellenmethode nicht überschreibt, selbst wenn sie eine Implementierung dieser Methode von einer Schnittstelle erbt. Der Grund dafür ist, dass Boxing verursacht wird und eingeschränkte Aufrufe untergraben werden.

Entscheidung: Dies scheint besser für ein Analysetool geeignet zu sein. Es scheint auch, dass diese Warnung störend sein könnte, da sie auch dann ausgelöst wird, wenn die Standardschnittstellenmethode nie aufgerufen wird und niemals Boxing erfolgt. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#warning-for-struct-not-implementing-default-method

Statische Schnittstellenkonstruktoren (geschlossen)

Wann werden statische Schnittstellenkonstruktoren ausgeführt? Der aktuelle CLI-Entwurf schlägt vor, dass dies erfolgt, wenn auf die erste statische Methode oder das erste Feld zugegriffen wird. Wenn keins von beiden vorhanden ist, erfolgt nie eine Ausführung??

[09.10.2018 Vorschlag des CLR-Teams: „Die Vorgehensweise für Werttypen wird gespiegelt (cctor-Überprüfung bei Zugriff auf jede Instanzmethode).“]

Entscheidung: Statische Konstruktoren werden auch für den Eintrag zu Instanzmethoden ausgeführt, wenn der statische Konstruktor nicht beforefieldinit war. In diesem Fall werden statische Konstruktoren vor dem Zugriff auf das erste statische Feld ausgeführt. https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-17.md#when-are-interface-static-constructors-run

Designbesprechungen

08.03.2017 LDM-Besprechungsnotizen21.03.2017 LDM-Besprechungsnotizen23.03.2017 Besprechung „CLR-Verhalten für Standardschnittstellenmethoden“05.04.2017 LDM-Besprechungsnotizen11.04.2017 LDM-Besprechungsnotizen18.04.2017 LDM-Besprechungsnotizen19.04.2017 LDM-Besprechungsnotizen17.05.2017 LDM-Besprechungsnotizen31.05.2017 LDM-Besprechungsnotizen14.06.2017 LDM-Besprechungsnotizen17.10.2018 LDM-Besprechungsnotizen14.11.2018 LDM-Besprechungsnotizen