Freigeben über


Erweiterungsmember

Hinweis

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 relevanten Sprachentwurfsbesprechungen (LDM)-Notizen erfasst.

Weitere Informationen zum Einführen von Featurespezifikationen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.

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

Erklärung

Syntax

class_body
    : '{' class_member_declaration* '}' ';'?
    | ';'
    ;

class_member_declaration
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | finalizer_declaration
    | static_constructor_declaration
    | type_declaration
    | extension_declaration // add
    ;

extension_declaration // add
    : 'extension' type_parameter_list? '(' receiver_parameter ')' type_parameter_constraints_clause* extension_body
    ;

extension_body // add
    : '{' extension_member_declaration* '}' ';'?
    ;

extension_member_declaration // add
    : method_declaration
    | property_declaration
    | operator_declaration
    ;

receiver_parameter // add
    : attributes? parameter_modifiers? type identifier?
    ;

Erweiterungsdeklarationen dürfen nur in nicht generischen, nicht geschachtelten statischen Klassen deklariert werden.
Es ist ein Fehler, einen Typ extension zu benennen.

Bereichsregeln

Die Typparameter und der Empfängerparameter einer Erweiterungserklärung sind im Geltungsbereich des Körpers der Erweiterungserklärung vorhanden. Es ist ein Fehler, auf den Empfängerparameter innerhalb eines statischen Elements zu verweisen, mit Ausnahme eines Ausdrucks nameof . Es ist ein Fehler, wenn ein Mitglied Typparameter oder Parameter (sowie lokale Variablen und lokale Funktionen direkt im Mitgliedskörper) mit demselben Namen wie ein Typparameter oder ein Receiver-Parameter der Erweiterungsdeklaration deklariert.

public static class E
{
    extension<T>(T[] ts)
    {
        public bool M1(T t) => ts.Contains(t);        // `T` and `ts` are in scope
        public static bool M2(T t) => ts.Contains(t); // Error: Cannot refer to `ts` from static context
        public void M3(int T, string ts) { }          // Error: Cannot reuse names `T` and `ts`
        public void M4<T, ts>(string s) { }           // Error: Cannot reuse names `T` and `ts`
    }
}

Es ist kein Fehler für die Member selbst, denselben Namen wie die Typparameter oder den Empfängerparameter der eingeschlossenen Erweiterungsdeklaration zu haben. Mitgliedsnamen werden nicht direkt durch eine einfache Namenssuche innerhalb der Erweiterungsdeklaration gefunden; deshalb wird der Typparameter oder der Empfängerparameter dieses Namens anstelle des Mitglieds gefunden.

Mitglieder geben Anlass zu statischen Methoden, die direkt in der umgebenden statischen Klasse deklariert werden, und diese können durch einfache Namenssuche gefunden werden; aber ein Typparameter der Erweiterungsdeklaration oder ein Empfängerparameter mit demselben Namen wird zuerst gefunden.

public static class E
{
    extension<T>(T[] ts)
    {
        public void T() { M(ts); } // Generated static method M<T>(T[]) is found
        public void M() { T(ts); } // Error: T is a type parameter
    }
}

Statische Klassen als Erweiterungscontainer

Erweiterungen werden innerhalb nicht generischer statischer Klassen auf oberster Ebene deklariert, genau wie Erweiterungsmethoden heute, und können somit mit klassischen Erweiterungsmethoden und statischen Membern ohne Erweiterung koexistieren:

public static class Enumerable
{
    // New extension declaration
    extension(IEnumerable source) { ... }
    
    // Classic extension method
    public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source) { ... }
    
    // Non-extension member
    public static IEnumerable<int> Range(int start, int count) { ... } 
}

Erweiterungsdeklarationen

Eine Erweiterungsdeklaration ist anonym und stellt eine Empfängerspezifikation mit allen zugeordneten Typparametern und Einschränkungen bereit, gefolgt von einer Reihe von Erweiterungsmememerdeklarationen. Bei der Empfängerspezifikation kann es sich um einen Parameter, oder – wenn nur statische Erweiterungsmember deklariert werden – einen Typ handeln:

public static class Enumerable
{
    extension(IEnumerable source) // extension members for IEnumerable
    {
        public bool IsEmpty { get { ... } }
    }
    extension<TSource>(IEnumerable<TSource> source) // extension members for IEnumerable<TSource>
    {
        public IEnumerable<T> Where(Func<TSource, bool> predicate) { ... }
        public IEnumerable<TResult> Select<TResult>(Func<TSource, TResult> selector) { ... }
    }
    extension<TElement>(IEnumerable<TElement>) // static extension members for IEnumerable<TElement>
        where TElement : INumber<TElement>
    {
        public static IEnumerable<TElement> operator +(IEnumerable<TElement> first, IEnumerable<TElement> second) { ... }
    }
}

Der Typ in der Empfängerspezifikation wird als Empfängertyp und der Parametername (sofern vorhanden) als Empfängerparameter bezeichnet.

Wenn der Empfängerparameter benannt ist, ist der Empfängertyp möglicherweise nicht statisch.
Der Empfängerparameter darf keine Modifizierer haben, wenn er nicht benannt ist, und er darf nur die Abweichungsmodifizierer unten und scoped andernfalls enthalten.
Der Empfängerparameter trägt die gleichen Einschränkungen wie der erste Parameter einer klassischen Erweiterungsmethode.
Das [EnumeratorCancellation] Attribut wird ignoriert, wenn es im Empfängerparameter platziert wird.

Erweiterungsmember

Erweiterungsmemberdeklarationen sind syntaktisch identisch mit entsprechenden Instanzen und statischen Membern in Klassen- und Strukturdeklarationen (mit Ausnahme von Konstruktoren). Instanzmember verweisen auf den Empfänger mit dem Namen des Empfängerparameters:

public static class Enumerable
{
    extension(IEnumerable source)
    {
        // 'source' refers to receiver
        public bool IsEmpty => !source.GetEnumerator().MoveNext();
    }
}

Fehler beim Angeben eines Instanzerweiterungsmitglieds, wenn die eingeschlossene Erweiterungsdeklaration keinen Empfängerparameter angibt:

public static class Enumerable
{
    extension(IEnumerable) // No parameter name
    {
        public bool IsEmpty => true; // Error: instance extension member not allowed
    }
}

Es ist ein Fehler, die folgenden Modifizierer für ein Element einer Erweiterungsdeklaration anzugeben: abstract, , virtual, , override, new, sealed, partialund protected (und verwandte Barrierefreiheitsmodifizierer).
Fehler beim Angeben des readonly Modifizierers für ein Element einer Erweiterungsdeklaration.
Eigenschaften in Erweiterungsdeklarationen verfügen möglicherweise nicht über init Accessoren.
Die Instanzmitglieder sind unzulässig, wenn der Empfängerparameter unbenannt ist.

Alle Mitglieder müssen Namen haben, die sich von dem Namen der statischen umschließenden Klasse und dem Namen des erweiterten Typs unterscheiden, falls ein solcher vorhanden ist.

Fehler beim Dekorieren eines Erweiterungselements mit dem [ModuleInitializer] Attribut.

Refness

Standardmäßig wird der Empfänger wie andere Parameter an Instanzerweiterungsmember nach Wert übergeben. Ein Erweiterungsdeklarationsempfänger im Parameterformular kann jedoch ref, ref readonly und in angeben, solange der Empfängertyp als Werttyp bekannt ist.

NULL-Zulässigkeit und Attribute

Empfängertypen können Verweistypen sein oder enthalten, die Nullwerte zulassen, und Empfängerspezifikationen in Form von Parametern können Attribute angeben:

public static class NullableExtensions
{
    extension(string? text)
    {
        public string AsNotNull => text is null ? "" : text;
    }
    extension([NotNullWhen(false)] string? text)
    {
        public bool IsNullOrEmpty => text is null or [];
    }
    extension<T> ([NotNull] T t) where T : class?
    {
        public void ThrowIfNull() => ArgumentNullException.ThrowIfNull(t);
    }
}

Kompatibilität mit klassischen Erweiterungsmethoden

Instanzenerweiterungsmethoden generieren Artefakte, die mit den klassischen Erweiterungsmethoden übereinstimmen.

Insbesondere die generierte statische Methode verfügt über die Attribute, Modifizierer und den Namen der deklarierten Erweiterungsmethode sowie Typparameterliste, Parameterliste und Einschränkungsliste, die aus der Erweiterungsdeklaration und der Methodendeklaration in dieser Reihenfolge zusammengesetzt sind:

public static class Enumerable
{
    extension<TSource>(IEnumerable<TSource> source) // Generate compatible extension methods
    {
        public IEnumerable<TSource> Where(Func<TSource, bool> predicate) { ... }
        public IEnumerable<TSource> Select<TResult>(Func<TSource, TResult> selector)  { ... }
    }
}

Generates:

[Extension]
public static class Enumerable
{
    [Extension]
    public static IEnumerable<TSource> Where<TSource>(IEnumerable<TSource> source, Func<TSource, bool> predicate) { ... }

    [Extension]
    public static IEnumerable<TSource> Select<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, TResult> selector)  { ... }
}

Betriebspersonal

Erweiterungsoperatoren verfügen zwar über explizite Operandentypen, müssen aber dennoch innerhalb einer Erweiterungsdeklaration deklariert werden:

public static class Enumerable
{
    extension<TElement>(IEnumerable<TElement>) where TElement : INumber<TElement>
    {
        public static IEnumerable<TElement> operator *(IEnumerable<TElement> vector, TElement scalar) { ... }
        public static IEnumerable<TElement> operator *(TElement scalar, IEnumerable<TElement> vector) { ... }
    }
}

Dies ermöglicht die Deklaration und Ableitung von Typparametern und entspricht der Deklaration eines normalen benutzerdefinierten Operators innerhalb eines seiner Operandentypen.

Checking

Ermittelbarkeit: Für jedes Nicht-Methoden-Erweiterungsmitglied müssen alle Typparameter des Erweiterungsblocks in der kombinierten Gruppe von Parametern aus der Erweiterung und dem Mitglied verwendet werden.

Eindeutigkeit: Innerhalb einer bestimmten eingeschlossenen statischen Klasse werden die Deklarationen von Erweiterungsmembern mit demselben Empfängertyp (Modulo Identity Conversion and Type Parameter Name Substitution) als ein einzelnes Deklarationsraum behandelt, der den Membern innerhalb einer Klasse oder Struct-Deklaration ähnelt, und unterliegen den gleichen Regeln zur Eindeutigkeit.

public static class MyExtensions
{
    extension<T1>(IEnumerable<int>) // Error! T1 not inferrable
    {
        ...
    }
    extension<T2>(IEnumerable<T2>)
    {
        public bool IsEmpty { get ... }
    }
    extension<T3>(IEnumerable<T3>?)
    {
        public bool IsEmpty { get ... } // Error! Duplicate declaration
    }
}

Die Anwendung dieser Eindeutigkeitsregel umfasst klassische Erweiterungsmethoden innerhalb derselben statischen Klasse. Für den Vergleich mit Methoden innerhalb von Erweiterungsdeklarationen wird der this-Parameter zusammen mit den in diesem Empfängertyp genannten Typparametern als Empfängerspezifikation behandelt, und die übrigen Typ- und Methodenparameter werden für die Methodensignatur verwendet:

public static class Enumerable
{
    public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source) { ... }
    
    extension(IEnumerable source) 
    {
        IEnumerable<TResult> Cast<TResult>() { ... } // Error! Duplicate declaration
    }
}

Verbrauch

Bei dem Versuch, eine Erweiterungsmembersuche durchzuführen, bringen alle Erweiterungsdeklarationen innerhalb statischer Klassen, die durch using importiert werden, ihre Member als Kandidaten ein, unabhängig vom Empfängertyp. Nur im Rahmen der Auflösung werden Kandidaten mit inkompatiblen Empfängertypen verworfen.
Es wird versucht, eine vollständige generische Typinferenz zwischen den Typen der Argumente (einschließlich des tatsächlichen Empfängers) und allen Typparametern (zusammengefasst in der Erweiterungsdeklaration und in der Erweiterungsmemberdeklaration) durchzuführen.
Wenn explizite Typargumente angegeben werden, werden sie verwendet, um die Typparameter der Erweiterungsdeklaration und die Erweiterungsmemmdeklaration zu ersetzen.

string[] strings = ...;

var query = strings.Select(s => s.Length); // extension invocation
var query2 = strings.Select<string, int>(s => s.Length); // ... with explicit full set of type arguments

var query3 = Enumerable.Select(strings, s => s.Length); // static method invocation
var query4 = Enumerable.Where<string, int>(strings, s => s.Length); // ... with explicit full set of type arguments
 
public static class Enumerable
{
    extension<TSource>(IEnumerable<TSource> source)
    {
        public IEnumerable<TResult> Select<TResult>(Func<T, TResult> predicate) { ... }
    }
}

Ähnlich wie bei klassischen Erweiterungsmethoden können die ausgegebenen Implementierungsmethoden statisch aufgerufen werden.
Auf diese Weise kann der Compiler zwischen Erweiterungsmitgliedern mit demselben Namen und derselben Arität unterscheiden.

object.M(); // ambiguous
E1.M();

new object().M2(); // ambiguous
E1.M2(new object());

_ = _new object().P; // ambiguous
_ = E1.get_P(new object());

static class E1
{
    extension(object)
    {
        public static void M() { }
        public void M2() { }
        public int P => 42;
    }
}

static class E2
{
    extension(object)
    {
        public static void M() { }
        public void M2() { }
        public int P => 42;
    }
}

Statische Erweiterungsmethoden werden wie Instanzenerweiterungsmethoden aufgelöst (wir betrachten ein zusätzliches Argument des Empfängertyps).
Erweiterungseigenschaften werden wie Erweiterungsmethoden mit einem einzelnen Parameter (dem Empfängerparameter) und einem einzelnen Argument (dem tatsächlichen Empfängerwert) aufgelöst.

using static-Anweisungen

Ein using_static_directive stellt Member von Erweiterungsblöcken in der Typdeklaration für den Erweiterungszugriff zur Verfügung.

using static N.E;

new object().M();
object.M2();

_ = new object().Property;
_ = object.Property2;

C c = null;
_ = c + c;
c += 1;

namespace N
{
    static class E
    {
        extension(object o)
        {
            public void M() { }
            public static void M2() { }
            public int Property => 0;
            public static int Property2 => 0;
        }

        extension(C c)
        {
            public static C operator +(C c1, C c2) => throw null;
            public void operator +=(int i) => throw null;
        }
    }
}

class C { } 

Wie zuvor können direkt in der Deklaration des angegebenen Typs enthaltene statische Member (mit Ausnahme von Erweiterungsmethoden) direkt referenziert werden.
Dies bedeutet, dass Implementierungsmethoden (mit Ausnahme von Erweiterungsmethoden) direkt als statische Methoden verwendet werden können:

using static E;

M();
System.Console.Write(get_P());
set_P(43);
_ = op_Addition(0, 0);
_ = new object() + new object();

static class E
{
    extension(object)
    {
        public static void M() { }
        public static int P { get => 42; set { } }
        public static object operator +(object o1, object o2) { return o1; }
    }
}

Ein using_static_directive importiert Erweiterungsmethoden immer noch nicht direkt als statische Methoden, sodass die Implementierungsmethode für nicht statische Erweiterungsmethoden nicht direkt als statische Methode aufgerufen werden kann.

using static E;

M(1); // error: The name 'M' does not exist in the current context

static class E
{
    extension(int i)
    {
        public void M() { }
    }
}

OverloadResolutionPriorityAttribute

Erweiterungsmitglieder innerhalb einer umgebenden statischen Klasse unterliegen der Priorisierung gemäß ORPA-Werten. Die eingeschlossene statische Klasse wird als "enthaltender Typ" betrachtet, den ORPA-Regeln berücksichtigen.
Jedes ORPA-Attribut, das für eine Erweiterungseigenschaft vorhanden ist, wird in die Implementierungsmethoden für die Accessoren der Eigenschaft kopiert, sodass die Priorisierung berücksichtigt wird, wenn diese Accessoren über die Mehrdeutigkeitssyntax verwendet werden.

Einstiegspunkte

Methoden von Erweiterungsblöcken gelten nicht als Kandidaten für Einstiegspunkte (siehe "7.1 Anwendungsstart"). Hinweis: Die Implementierungsmethode kann immer noch ein Kandidat sein.

Reduzierung

Die Herabsetzungsstrategie für Erweiterungsdeklarationen ist keine Entscheidung auf Sprachebene. Über die Implementierung der Sprachsemantik hinaus muss sie jedoch bestimmte Anforderungen erfüllen:

  • Das Format der generierten Typen, Member und Metadaten sollte in allen Fällen eindeutig angegeben werden, damit andere Compiler sie nutzen und generieren können.
  • Die generierten Artefakte sollten stabil sein, in dem Sinne, dass vernünftige spätere Änderungen die Consumer, die mit früheren Versionen kompiliert wurden, nicht unterbrechen sollten.

Diese Anforderungen müssen im Laufe der Umsetzung weiter verfeinert werden, und in einigen Fällen müssen möglicherweise Kompromisse eingegangen werden, um einen vernünftigen Implementierungsansatz zu ermöglichen.

Metadaten für Deklarationen

Ziele

Das folgende Design ermöglicht Folgendes:

  • roundtripping von Erweiterungsdeklarationssymbolen über Metadaten (vollständige und Referenz-Assemblies),
  • stabile Verweise auf Erweiterungsmitglieder (XML-Dokumente),
  • lokale Ermittlung der ausgegebenen Namen (hilfreich für EnC)
  • Öffentliche API-Nachverfolgung.

Für XML-Dokumente ist die DocID eines Erweiterungsmitglieds die DocID des Erweiterungsmitglieds in den Metadaten. Die docID, die in cref="Extension.extension(object).M(int)" verwendet wird, ist M:Extension.<>E__ExtensionGroupingTypeNameForObject.M(System.Int32) und diese docID ist beispielsweise stabil bei erneuten Kompilierungen und bei Neuordnungen von Erweiterungsblöcken. Im Idealfall würde es auch stabil bleiben, wenn sich die Einschränkungen im Erweiterungsblock ändern, aber wir haben kein Design gefunden, das dies erreichen würde, ohne das Sprachdesign für Konflikte bei Mitgliedern negativ zu beeinflussen.

Für EnC ist es nützlich, lokal (einfach durch Betrachten eines geänderten Erweiterungselements) zu wissen, wo das aktualisierte Erweiterungselement in den Metadaten ausgegeben wird.

Für die Nachverfolgung öffentlicher APIs verringern stabilere Namen das Rauschen. Technisch sollten die Erweiterungsgruppierungstypnamen in solchen Szenarien jedoch nicht ins Spiel kommen. Beim Betrachten des Erweiterungsmitglieds M spielt es keine Rolle, was der Name des Erweiterungsgruppierungstyps ist; wichtig ist die Signatur des Erweiterungsblocks, zu dem er gehört. Die öffentliche API-Signatur sollte nicht als Extension.<>E__ExtensionGroupingTypeNameForObject.M(System.Int32) angesehen werden, sondern als Extension.extension(object).M(int). Mit anderen Worten: Erweiterungsmitglieder sollten als hätten sie zwei Sätze von Typenparametern und zwei Sätze von Parametern betrachtet werden.

Überblick

Erweiterungsblöcke werden nach ihrer CLR-Ebenensignatur gruppiert. Jede CLR-Äquivalenzgruppe wird als Erweiterungsgruppierungstyp mit einem inhaltsbasierten Namen ausgegeben. Erweiterungsblöcke innerhalb einer CLR-Äquivalenzgruppe werden dann nach C#-Äquivalenz gruppiert. Jede C#-Äquivalenzgruppe wird als Erweiterungsmarkierungstyp mit einem inhaltsbasierten Namen ausgegeben, der in seinem entsprechenden Erweiterungsgruppierungstyp geschachtelt ist. Ein Erweiterungsmarkertyp enthält eine einzelne Erweiterungsmarkiermethode , die einen Erweiterungsparameter codiert. Die Erweiterungsmarkierungsmethode mit dem enthaltenden Erweiterungsmarkertyp codiert die Signatur eines Erweiterungsblocks mit voller Genauigkeit. Die Deklaration jedes Erweiterungsmitglieds wird im richtigen Erweiterungsgruppierungstyp ausgegeben, verweist anhand seines Namens über ein Attribut auf einen Erweiterungsmarkertyp und wird von einer statischen Implementierungsmethode der obersten Ebene mit einer geänderten Signatur begleitet.

Hier ist eine schematisierte Übersicht über die Metadatencodierung:

[Extension]
static class EnclosingStaticClass
{
    [Extension]
    public sealed class ExtensionGroupingType1 // has type parameters with minimal constraints sufficient to keep extension member declarations below valid
    {
        public static class ExtensionMarkerType1 // has re-declared type parameters with full fidelity of C# constraints
        {
            public static void <Extension>$(... extension parameter ...) // extension marker method
        }
        ... ExtensionMarkerType2, etc ...

        ... extension members for ExtensionGroupingType1, each points to its corresponding extension marker type ...
    }

    ... ExtensionGroupingType2, etc ...

    ... implementation methods ...
}

Die eingeschlossene statische Klasse wird mit einem [Extension] Attribut ausgegeben.

Signatur auf CLR-Ebene im Vergleich zur Signatur auf C#-Ebene

Die Signatur auf CLR-Ebene eines Erweiterungsblocks ergibt sich aus:

  • Normalisieren von Typparameternamen in T0, , T1usw.
  • Entfernen von Attributen
  • Löschen des Parameternamens
  • Löschen von Parametermodifizierern (wie ref, , in, scoped...)
  • Tupelnamen löschen
  • Löschen von Nullbarkeitsannotationen
  • notnull Entfernen von Einschränkungen

Hinweis: Andere Einschränkungen werden beibehalten, wie new(), struct, class, allows ref struct, unmanaged und Typbeschränkungen.

Erweiterungsgruppierungstypen

Ein Erweiterungsgruppierungstyp wird für jeden Satz von Erweiterungsblöcken in der Quelle mit derselben CLR-Ebensignatur an die Metadaten ausgegeben.

  • Der Name ist nicht aussprechbar und wird anhand des Inhalts der Signatur auf CLR-Ebene festgelegt. Weitere Details unten.
  • Seine Typparameter haben normalisierte Namen (T0, T1, ...) und haben keine Attribute.
  • Es ist öffentlich und versiegelt.
  • Sie ist mit der specialname Kennzeichnung und einem [Extension] Attribut gekennzeichnet.

Der inhaltsbasierte Name des Erweiterungsgruppierungstyps basiert auf der Signatur auf CLR-Ebene und umfasst Folgendes:

  • Der vollqualifizierte CLR-Name des Typs des Erweiterungsparameters.
    • Referenzierte Typparameternamen werden auf T0, T1, usw. normalisiert, basierend auf der Reihenfolge, in der sie in der Typdeklaration erscheinen.
    • Der vollständig qualifizierte Name enthält nicht die enthaltene Assembly. Es ist üblich, dass Typen zwischen Assemblys verschoben werden, und das sollte die XML-Dokumentverweise nicht unterbrechen.
  • Einschränkungen von Typparametern werden eingeschlossen und sortiert, sodass die Neuanordnung in Quellcode den Namen nicht ändert. Konkret:
    • Typparametereinschränkungen werden in der Deklarationsreihenfolge aufgeführt. Die Einschränkungen für den Nth-Typparameter treten vor dem Nth+1-Typparameter auf.
    • Typbeschränkungen werden durch Vergleich der vollständigen Namen in numerischer Reihenfolge sortiert.
    • Nichttypeinschränkungen werden deterministisch sortiert und behandelt, um Mehrdeutigkeiten oder Kollisionen mit Typeinschränkungen zu vermeiden.
  • Da dies keine Attribute enthält, ignoriert es absichtlich C#-isms wie Tupelnamen, Nullbarkeit usw.

Hinweis: Der Name bleibt garantiert stabil bei erneuten Kompilierungen, Neuordnungen und Änderungen von C#-isms (d. h., die sich nicht auf die Signatur auf CLR-Ebene auswirken).

Erweiterungsmarkertypen

Der Markierungstyp deklariert die Typparameter des enthaltenden Gruppierungstyps (einen Erweiterungsgruppierungstyp) erneut, um die volle Treue der C#-Ansicht von Erweiterungsblöcken zu erhalten.

Ein Erweiterungsmarkierungstyp wird für jeden Satz von Erweiterungsblöcken in der Quelle mit derselben C#-Signatur in die Metadaten ausgegeben.

  • Der Name ist unsagbar und wird basierend auf dem Inhalt der Signatur auf C#-Ebene des Erweiterungsblocks bestimmt. Weitere Details unten.
  • Die Typparameter für den enthaltenen Gruppierungstyp werden neu deklariert, um denen in der Quelle deklarierten (einschließlich Name und Attribute) zu entsprechen.
  • Es ist öffentlich und statisch.
  • Sie ist mit der specialname Kennzeichnung gekennzeichnet.

Der inhaltsbasierte Name des Erweiterungsmarkierungstyps basiert auf folgendem:

  • Die Namen von Typparametern werden in der Reihenfolge eingeschlossen, in der sie in der Erweiterungsdeklaration angezeigt werden.
  • Die Attribute von Typparametern werden eingeschlossen und sortiert, sodass die Neuanordnung in Quellcode den Namen nicht ändert.
  • Einschränkungen von Typparametern werden eingeschlossen und sortiert, sodass die Neuanordnung in Quellcode den Namen nicht ändert.
  • Der vollständig qualifizierte C#-Name des erweiterten Typs
    • Dies umfasst Elemente wie nullbare Anmerkungen, Tupelnamen usw. ...
    • Der vollständig qualifizierte Name enthält nicht die enthaltene Assembly
  • Der Name des Erweiterungsparameters
  • Die Modifizierer des Erweiterungsparameters (ref, ref readonly, scoped, ...) in einer deterministischen Reihenfolge
  • Die vollqualifizierten Namen- und Attributargumente für alle Attribute, die auf den Erweiterungsparameter in einer deterministischen Reihenfolge angewendet werden

Hinweis: Der Name bleibt garantiert stabil bei erneuter Kompilierung und Umsortierungen.
Hinweis: Erweiterungsmarkertypen und Erweiterungsmarkermethoden werden als Teil von Referenzassemblies ausgegeben.

Erweiterungsmarkierungsmethode

Der Zweck der Markermethode besteht darin, den Erweiterungsparameter des Erweiterungsblocks zu codieren. Da es sich um ein Element des Erweiterungsmarkertyps handelt, kann er auf die erneut deklarierten Typparameter des Erweiterungsmarkierungstyps verweisen.

Jeder Erweiterungsmarkertyp enthält eine einzelne Methode, die Erweiterungsmarkierungsmethode.

  • Es ist statisch, nicht generisch, gibt keinen Wert zurück und wird aufgerufen <Extension>$.
  • Der einzelne Parameter verfügt über die Attribute, die Referenzeigenschaft, den Typ und den Namen aus dem Erweiterungsparameter.
    Wenn der Erweiterungsparameter keinen Namen angibt, ist der Parametername leer.
  • Sie ist mit der specialname Kennzeichnung gekennzeichnet.

Die Zugänglichkeit der Markermethode wird die am wenigsten restriktive Zugänglichkeit unter den entsprechend deklarierten Erweiterungsmitgliedern sein, und es wird private verwendet, wenn keine deklariert sind.

Erweiterungsmember

Methoden-/Eigenschaftsdeklarationen in einem Erweiterungsblock in der Quelle werden als Mitglieder des Erweiterungsgruppierungstyps in Metadaten dargestellt.

  • Die Signaturen der ursprünglichen Methoden werden beibehalten (einschließlich der Attribute), aber ihre Texte werden durch throw NotImplementedException() ersetzt.
  • Auf diese sollte in IL nicht verwiesen werden.
  • Methoden, Eigenschaften und deren Zugriffsmethoden sind mit [ExtensionMarkerName("...")] gekennzeichnet, wobei sich dies auf den Namen des Erweiterungsmarkierungstyps bezieht, der dem Erweiterungsblock für dieses Element entspricht.

Implementierungsmethoden

Die Methodentexte für Methoden-/Eigenschaftsdeklarationen in einem Erweiterungsblock in der Quelle werden als statische Implementierungsmethoden in der statischen Klasse der obersten Ebene ausgegeben.

  • Eine Implementierungsmethode hat denselben Namen wie die ursprüngliche Methode.
  • Er verfügt über Typparameter, die vom Erweiterungsblock abgeleitet sind, der den Typparametern der ursprünglichen Methode (einschließlich Attributen) vorangestellt ist.
  • Sie verfügt über die gleiche Zugänglichkeit und die gleichen Attribute wie die ursprüngliche Methode.
  • Wenn sie eine statische Methode implementiert, weist sie dieselben Parameter und denselben Rückgabetyp auf.
  • Wenn sie eine Instanzmethode implementiert, verfügt sie über einen vordefinierten Parameter zur Signatur der ursprünglichen Methode. Die Attribute dieses Parameters, Refness, Typ und Name, werden von dem Erweiterungsparameter abgeleitet, der im entsprechenden Erweiterungsblock deklariert ist.
  • Die Parameter in Implementierungsmethoden beziehen sich auf Typparameter, die sich im Besitz der Implementierungsmethode befinden, statt auf die Parameter eines Erweiterungsblocks.
  • Wenn es sich bei dem ursprünglichen Element um eine instanz gewöhnliche Methode handelt, wird die Implementierungsmethode mit einem [Extension] Attribut gekennzeichnet.

ExtensionMarkerName-Attribut

Der ExtensionMarkerNameAttribute-Typ ist nur für die Compilerverwendung vorgesehen – er ist in der Quelle nicht zulässig. Die Typdeklaration wird vom Compiler synthetisiert, wenn sie noch nicht in der Kompilierung enthalten ist.

namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)]
public sealed class ExtensionMarkerNameAttribute : Attribute
{
    public ExtensionMarkerNameAttribute(string name)
        => Name = name;

    public string Name { get; }
}

Hinweis: Obwohl einige Attributziele zur Zukunftssicherung enthalten sind (erweiterte verschachtelte Typen, Erweiterungsfelder, Erweiterungsereignisse), ist AttributeTargets.Constructor nicht enthalten, da Erweiterungskonstruktoren keine Konstruktoren sind.

Beispiel

Hinweis: Wir verwenden vereinfachte inhaltsbasierte Namen für das Beispiel zur Lesbarkeit. Hinweis: Da C# keine Typparameter-erneute Deklaration darstellen kann, ist der Code, der Metadaten darstellt, kein gültiger C#-Code.

Hier ist ein Beispiel, das zeigt, wie die Gruppierung ohne Mitglieder funktioniert:

class E
{
    extension<T>(IEnumerable<T> source)
    {
        ... member in extension<T>(IEnumerable<T> source)
    }

    extension<U>(ref IEnumerable<U?> p)
    {
        ... member in extension<U>(ref IEnumerable<U?> p)
    }

    extension<T>(IEnumerable<U> source)
        where T : IEquatable<U>
    {
        ... member in extension<T>(IEnumerable<U> source) where T : IEquatable<U>
    }
}

wird wie folgt ausgegeben:

[Extension]
class E
{
    [Extension, SpecialName]
    public sealed class <>E__ContentName_For_IEnumerable_T<T0>
    {
        [SpecialName]
        public static class <>E__ContentName1 // note: re-declares type parameter T0 as T
        {
            [SpecialName]
            public static void <Extension>$(IEnumerable<T> source) { }
        }

        [SpecialName]
        public static class <>E__ContentName2 // note: re-declares type parameter T0 as U
        {
            [SpecialName]
            public static void <Extension>$(ref IEnumerable<U?> p) { }
        }

        [ExtensionMarkerName("<>E__ContentName1")]
        ... member in extension<T>(IEnumerable<T> source)

        [ExtensionMarkerName("<>E__ContentName2")]
        ... member in extension<U>(ref IEnumerable<U?> p)
    }

    [Extension, SpecialName]
    public sealed class <>ContentName_For_IEnumerable_T_With_Constraint<T0>
       where T0 : IEquatable<T0>
    {
        [SpecialName]
        public static class <>E__ContentName3 // note: re-declares type parameter T0 as U
        {
            [SpecialName]
            public static void <Extension>$(IEnumerable<U> source) { }
        }

        [ExtensionMarkerName("ContentName3")]
        public static bool IsPresent(U value) => throw null!;
    }

    ... implementation methods
}

Hier ist ein Beispiel, das veranschaulicht, wie Mitglieder ausgegeben werden:

static class IEnumerableExtensions
{
    extension<T>(IEnumerable<T> source) where T : notnull
    {
        public void Method() { ... }
        internal static int Property { get => ...; set => ...; }
        public int Property2 { get => ...; set => ...; }
    }

    extension(IAsyncEnumerable<int> values)
    {
        public async Task<int> SumAsync() { ... }
    }

    public static void Method2() { ... }
}

wird wie folgt ausgegeben:

[Extension]
static class IEnumerableExtensions
{
    [Extension, SpecialName]
    public sealed class <>E__ContentName_For_IEnumerable_T<T0>
    {
        // Extension marker type is emitted as a nested type and re-declares its type parameters to include C#-isms
        // In this example, the type parameter `T0` is re-declared as `T` with a `notnull` constraint:
        // .class <>E__IEnumerableOfT<T>.<>E__ContentName_For_IEnumerable_T_Source
        // .typeparam T
        //     .custom instance void NullableAttribute::.ctor(uint8) = (...)
        [SpecialName]
        public static class <>E__ContentName_For_IEnumerable_T_Source
        {
            [SpecialName]
            public static <Extension>$(IEnumerable<T> source) => throw null;
        }

        [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
        public void Method() => throw null;

        [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
        internal static int Property
        {
            [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
            get => throw null;
            [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
            set => throw null;
        }

        [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
        public int Property2
        {
            [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
            get => throw null;
            [ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
            set => throw null;
        }
    }

    [Extension, SpecialName]
    public sealed class <>E__ContentName_For_IAsyncEnumerable_Int
    {
        [SpecialName]
        public static class <>E__ContentName_For_IAsyncEnumerable_Int_Values
        {
            [SpecialName]
            public static <Extension>$(IAsyncEnumerable<int> values) => throw null;
        }

        [ExtensionMarkerName("<>E__ContentName_For_IAsyncEnumerable_Int_Values")]
        public Task<int> SumAsync() => throw null;
    }

    // Implementation for Method
    [Extension]
    public static void Method<T>(IEnumerable<T> source) { ... }

    // Implementation for Property
    internal static int get_Property<T>() { ... }
    internal static void set_Property<T>(int value) { ... }

    // Implementation for Property2
    public static int get_Property2<T>(IEnumerable<T> source) { ... }
    public static void set_Property2<T>(IEnumerable<T> source, int value) { ... }

    // Implementation for SumAsync
    [Extension]
    public static int SumAsync(IAsyncEnumerable<int> values) { ... }

    public static void Method2() { ... }
}

Immer wenn Erweiterungsmember in der Quelle verwendet werden, geben wir diese als Verweis auf Implementierungsmethoden aus. Beispiel: Ein Aufruf von enumerableOfInt.Method() wird als statischer Aufruf von IEnumerableExtensions.Method<int>(enumerableOfInt) ausgegeben.

XML-Dokumente

Die Dokumentkommentare zum Erweiterungsblock werden für den Markierungstyp ausgegeben (die DocID für den Erweiterungsblock ist E.<>E__MarkerContentName_For_ExtensionOfT'1 im folgenden Beispiel).
Sie dürfen auf den Erweiterungsparameter und die Typparameter unter Verwendung von <paramref> bzw. <typeparamref> verweisen.
Hinweis: Sie können den Erweiterungsparameter oder Typparameter (mit <param> und <typeparam>) nicht für ein Erweiterungselement dokumentieren.

Wenn zwei Erweiterungsblöcke als ein Markertyp emittiert werden, werden auch ihre Doc-Kommentare zusammengeführt.

Tools, die die XML-Dokumente verwenden, sind dafür verantwortlich, den Erweiterungsblock <param> und <typeparam> nach Bedarf in die Erweiterungsmember zu kopieren (d.h. die Parameterinformationen sollten nur für Instanzmember kopiert werden).

Ein <inheritdoc> wird für Implementierungsmethoden ausgegeben und bezieht sich auf das relevante Erweiterungsmember mit einem cref. Die Implementierungsmethode für einen Getter bezieht sich beispielsweise auf die Dokumentation der Erweiterungseigenschaft. Wenn das Erweiterungsmember keine Dokumentkommentare enthält, wird das <inheritdoc> weggelassen.

Bei Erweiterungsblöcken und Erweiterungsmitgliedern erhalten wir gegenwärtig keine Warnungen, wenn:

  • Der Erweiterungsparameter ist dokumentiert, aber die Parameter für das Erweiterungselement sind nicht vorhanden.
  • oder umgekehrt
  • oder in den entsprechenden Szenarien mit nicht dokumentierten Typparametern

Beispielsweise die folgenden Dokumentkommentare:

/// <summary>Summary for E</summary>
static class E
{
    /// <summary>Summary for extension block</summary>
    /// <typeparam name="T">Description for T</typeparam>
    /// <param name="t">Description for t</param>
    extension<T>(T t)
    {
        /// <summary>Summary for M, which may refer to <paramref name="t"/> and <typeparamref name="T"/></summary>
        /// <typeparam name="U">Description for U</typeparam>
        /// <param name="u">Description for u</param>
        public void M<U>(U u) => throw null!;

        /// <summary>Summary for P</summary>
        public int P => 0;
    }
}

den folgenden XML-Code liefern:

<?xml version="1.0"?>
<doc>
    <assembly>
        <name>Test</name>
    </assembly>
    <members>
        <member name="T:E">
            <summary>Summary for E</summary>
        </member>
        <member name="T:E.&lt;&gt;E__MarkerContentName_For_ExtensionOfT`1">
            <summary>Summary for extension block</summary>
            <typeparam name="T">Description for T</typeparam>
            <param name="t">Description for t</param>
        </member>
        <member name="M:E.&lt;&gt;E__MarkerContentName_For_ExtensionOfT`1.M``1(``0)">
            <summary>Summary for M, which may refer to <paramref name="t"/> and <typeparamref name="T"/></summary>
            <typeparam name="U">Description for U</typeparam>
            <param name="u">Description for u</param>
        </member>
        <member name="P:E.&lt;&gt;E__MarkerContentName_For_ExtensionOfT`1.P">
            <summary>Summary for P</summary>
        </member>
        <member name="M:E.M``2(``0,``1)">
            <inheritdoc cref="M:E.&lt;&gt;E__MarkerContentName_For_ExtensionOfT`1.M``1(``0)"/>
        </member>
        <member name="M:E.get_P``1(``0)">
            <inheritdoc cref="P:E.&lt;&gt;E__MarkerContentName_For_ExtensionOfT`1.P"/>
        </member>
    </members>
</doc>

CREF-Referenzen

Wir können Erweiterungsblöcke wie geschachtelte Typen behandeln, die durch ihre Signatur adressiert werden können (als wäre es eine Methode mit einem einzelnen Erweiterungsparameter). Beispiel: E.extension(ref int).M().

Ein Cref kann jedoch keinen Erweiterungsblock selbst adressieren. E.extension(int) kann auf eine Methode namens "extension" im Typ Everweisen.

static class E
{
  extension(ref int i)
  {
    void M() { } // can be addressed by cref="E.extension(ref int).M()" or cref="extension(ref int).M()" within E, but not cref="M()"
  }
  extension(ref  int i)
  {
    void M(int i2) { } // can be addressed by cref="E.extension(ref int).M(int)" or cref="extension(ref int).M(int)" within E
  }
}

Die Suche weiß, in allen passenden Erweiterungsblöcken nachzuschlagen.
Da wir unqualifizierte Verweise auf Erweiterungsmitglieder verbieten, wird cref dies ebenfalls tun.

Die Syntax wäre:

member_cref
  : conversion_operator_member_cref
  | extension_member_cref // added
  | indexer_member_cref
  | name_member_cref
  | operator_member_cref
  ;

extension_member_cref // added
 : 'extension' type_argument_list? cref_parameter_list '.' member_cref
 ;

qualified_cref
  : type '.' member_cref
  ;

cref
  : member_cref
  | qualified_cref
  | type_cref
  ;

Es ist ein Fehler, extension_member_cref auf oberster Ebene (extension(int).M) oder in einer anderen Erweiterung verschachtelt (E.extension(int).extension(string).M) zu verwenden.

Bahnbrechende Änderungen

Typen und Aliase werden möglicherweise nicht als "Erweiterung" bezeichnet.

Offene Probleme

Temporärer Abschnitt des Dokuments im Zusammenhang mit offenen Problemen, einschließlich Diskussion über nicht definierte Syntax und alternative Designs
  • Sollten wir die Empfängeranforderungen beim Zugriff auf ein Erweiterungsmitglied anpassen? (Kommentar)
  • Bestätigen: extension oder extensions als Schlüsselwort (Antwort: extension, LDM 2025-03-24)
  • Bestätigen, dass wir [ModuleInitializer] ablehnen möchten (Antwort: Ja, ablehnen, LDM 2025-06-11)
  • Vergewissern Sie sich, dass es in Ordnung ist, Erweiterungsblöcke als Einstiegspunktkandidaten zu verwerfen (Antwort: Ja, Verwerfen, LDM 2025-06-11)
  • Bestätigen Sie die LangVer-Logik (überspringen Sie neue Erweiterungen, und melden Sie sie bei auswahl) (Antwort: Bedingungslos binden und LangVer-Fehler melden, mit Ausnahme von Erweiterungsmethoden, LDM 2025-06-11)
  • Sollte partial für Erweiterungsblöcke erforderlich sein, die verschmelzen und deren Dokumentkommentare ebenfalls zusammengeführt werden? (Antwort: Dokumentkommentare werden im Hintergrund zusammengeführt, wenn Blöcke zusammengeführt werden, nicht partial erforderlich, bestätigt durch E-Mail 2025-09-03)
  • Vergewissern Sie sich, dass Elemente nicht nach den enthaltenden oder erweiterten Typen benannt werden sollen. (Antwort: Ja, bestätigt per E-Mail 2025-09-03)

Revidieren von Gruppierungs-/Konfliktregeln im Lichte der Portabilitätsfrage: https://github.com/dotnet/roslyn/issues/79043

(Antwort: Dieses Szenario wurde im Rahmen des neuen Metadatendesigns mit inhaltsbasierten Typnamen behoben, es ist zulässig)

Die aktuelle Logik besteht darin, Erweiterungsblöcke mit demselben Empfängertyp zu gruppieren. Dies berücksichtigt keine Einschränkungen. Dies führt zu einem Portabilitätsproblem mit diesem Szenario:

static class E
{
   extension<T>(ref T) where T : struct
      void M()
   extension<T>(T) where T : class
      void M()
}

Es wird vorgeschlagen, die gleiche Gruppierungslogik zu verwenden, die wir für das Design der Erweiterung Gruppierungstyp planen, nämlich die Berücksichtigung von Einschränkungen auf CLR-Ebene (d.h. Ignorieren von notnull, Tupelnamen, Annotationen zur Nullbarkeit).

Sollte refness im Namen des Gruppierungstyps kodiert werden?

  • Überprüfungsvorschlag, der ref nicht in den Namen der Erweiterung gruppierungstyp aufgenommen werden soll (bedarf weiterer Diskussion nach WG-Überarbeitungen gruppieren/Konfliktregeln, LDM 2025-06-23) (Antwort: bestätigt durch E-Mail 2025-09-03)
public static class E
{
  extension(ref int)
  {
    public static void M()
  }
}

Es wird ausgegeben als:

public static class E
{
  public static class <>ExtensionTypeXYZ
  {
    .. marker method ...
    void M()
  }
}

Und CREF-Referenzen von Dritten für E.extension(ref int).M werden als M:E.<>ExtensionGroupingTypeXYZ.M() ausgegeben. Wenn ref aus einem Erweiterungsparameter entfernt oder hinzugefügt wird, wollen wir wahrscheinlich nicht, dass der CREF fehlerhaft wird.

Wir halten nicht viel von diesem Szenario, da jede Verwendung als Erweiterung eine Mehrdeutigkeit darstellen würde.

public static class E
{
  extension(ref int)
    static void M()
  extension(int)
    static void M()
}

Aber wir kümmern uns um dieses Szenario (für Portabilität und Nützlichkeit), und dies sollte mit dem vorgeschlagenen Metadatenentwurf funktionieren, nachdem wir Konfliktregeln angepasst haben:

static class E
{
   extension<T>(ref T) where T : struct
      void M()
   extension<T>(T) where T : class
      void M()
}

Die Nichtberücksichtigung der Referenz hat einen Nachteil, da wir in diesem Szenario die Portabilität verlieren:

static class E
{
   extension<T>(ref T)
      void M()
   extension<T>(T)
      void M()
}
// portability issue: since we're grouping without accounting for refness, the emitted extension members conflict (not implementation members). Mitigation: keep as classic extensions or split to another static class

Name Of

  • Sollten wir Erweiterungseigenschaften in nameof so wie klassische und neue Erweiterungsmethoden nicht zulassen? (Antwort: wir möchten 'nameof(EnclosingStaticClass.ExtensionMember) verwenden. Designbedingt, wahrscheinlich Punt von .NET 10. LDM 2025-06-11)

musterbasierte Konstrukte

Methodik

  • Wo sollen neue Erweiterungsmethoden ins Spiel kommen? (Antwort: dieselben Orte, an denen klassische Erweiterungsmethoden ins Spiel kommen, LDM 2025-05-05)

Dazu gehören:

  • GetEnumerator / GetAsyncEnumerator in foreach
  • Deconstruct in Dekonstruktion, in Positionsmustern und Vorgabe
  • Add in Sammlungsinitialisierern
  • GetPinnableReference in fixed
  • GetAwaiter in await

Dies schließt Folgendes aus:

  • Dispose / DisposeAsync in using und foreach
  • MoveNext / MoveNextAsync in foreach
  • Slice und int Indexer in impliziten Indexern (und eventuell Listenmustern?)
  • GetResult in await

Eigenschaften und Indexer

  • Wo sollen Erweiterungseigenschaften und Indexer ins Spiel kommen? (Antwort: Beginnen wir mit den vier, LDM 2025-05-05)

Wir fügen Folgendes ein:

  • Object Initializer: new C() { ExtensionProperty = ... }
  • Wörterbuchinitialisierer: new C() { [0] = ... }
  • with: x with { ExtensionProperty = ... }
  • Eigenschaftenmuster: x is { ExtensionProperty: ... }

Wir schließen Folgendes aus:

  • Current in foreach
  • IsCompleted in await
  • Count / Length Eigenschaften und Indexer im Listenmuster
  • Count / Length Eigenschaften und Indexer in impliziten Indexern
Rückgabeeigenschaften für Delegaten
  • Stellen Sie sicher, dass die Erweiterungseigenschaften dieses Shapes nur in LINQ-Abfragen relevant sind, ähnlich wie die Eigenschaften einer Instanz. (Antwort: sinnvoll, LDM 2025-04-06)
Listen- und Verteilungsmuster
  • Vergewissern Sie sich, dass Erweiterungsindexer Index/Range in Listenmustern wiedergegeben werden sollen (Antwort: nicht relevant für C# 14)
Revidieren, wo Count/Length Erweiterungseigenschaften ins Spiel kommen

Sammlungsausdrücke

  • Erweiterung Add funktioniert
  • Erweiterungsarbeiten GetEnumerator für Spread
  • Die Erweiterung GetEnumerator wirkt sich nicht auf die Bestimmung des Elementtyps aus (muss Instanz sein)
  • Statische Create Erweiterungsmethoden sollten nicht als eine gesegnete Erstellungsmethode zählen
  • Sollten erweiterbare zählbare Eigenschaften Sammlungsausdrücke beeinflussen?

params Collections

  • Erweiterungen Add wirken sich nicht auf die zulässigen Typen aus params

Wörterbuchausdrücke

  • Vergewissern Sie sich, dass Erweiterungsindexer nicht in Wörterbuchausdrücken verwendet werden, da das Vorhandensein des Indexers ein integraler Bestandteil der Definition eines Wörterbuchtyps ist. (Antwort: nicht relevant für C# 14)

extern

Benennungs-/Nummerierungsschema für Erweiterungstyp

Problem
Das aktuelle Nummerierungssystem verursacht Probleme mit der Validierung öffentlicher APIs, wodurch sichergestellt wird, dass öffentliche APIs zwischen Referenzassemblys und Implementierungsassemblys übereinstimmen.

Sollten wir eine der folgenden Änderungen vornehmen? (Antwort: Wir übernehmen ein inhaltsbasiertes Benennungsschema, um die Stabilität der öffentlichen API zu erhöhen, und die Tools müssen weiterhin aktualisiert werden, um Markermethoden zu berücksichtigen)

  1. Anpassen des Tools
  2. Verwenden eines inhaltsbasierten Benennungsschemas (TBD)
  3. Lassen Sie den Namen über eine Syntax steuern

Die neue generische Erweiterungsmethode Cast funktioniert in LINQ immer noch nicht.

Problem
In früheren Entwürfen von Rollen/Erweiterungen war es möglich, nur die Typargumente der Methode explizit anzugeben.
Da wir uns jedoch auf den scheinbarlosen Übergang von klassischen Erweiterungsmethoden konzentrieren, müssen alle Typargumente explizit angegeben werden.
Dies behebt kein Problem bei der Verwendung der Cast-Methode in LINQ.

Sollten wir eine Änderung des Erweiterungsfeatures vornehmen, um dieses Szenario zu berücksichtigen? (Antwort: Nein, dies führt nicht dazu, dass wir den Entwurf zur Erweiterungsauflösung, LDM 2025-05-05, überarbeiten.)

Einschränken des Erweiterungsparameters für ein Erweiterungselement

Sollten wir Folgendes zulassen? (Antwort: Nein, dies könnte später hinzugefügt werden)

static class E
{
    extension<T>(T t)
    {
        public void M<U>(U u) where T : C<U>  { } // error: 'E.extension<T>(T).M<U>(U)' does not define type parameter 'T'
    }
}

public class C<T> { }

NULL-Zulässigkeit

  • Bestätigen Sie den aktuellen Entwurf, dh maximale Portabilität/Kompatibilität (Antwort: Ja, LDM 2025-04-17)
    extension([System.Diagnostics.CodeAnalysis.DoesNotReturnIf(false)] bool b)
    {
        public void AssertTrue() => throw null!;
    }
    extension([System.Diagnostics.CodeAnalysis.NotNullIfNotNull("o")] ref int? i)
    {
        public void M(object? o)  => throw null!;
    }

Metadaten

  • Sollten Skelettmethoden NotSupportedException oder eine andere Standardausnahme werfen (momentan throw null;)? (Antwort: Ja, LDM 2025-04-17)
  • Sollten wir mehr als einen Parameter in der Markermethode in Metadaten akzeptieren (falls neue Versionen weitere Informationen hinzufügen)? (Antwort: wir können streng bleiben, LDM 2025-04-17)
  • Soll die Erweiterungsmarkierung oder die sprechenden Implementierungsmethoden mit einem speziellen Namen gekennzeichnet werden? (Antwort: Die Markermethode sollte mit besonderem Namen gekennzeichnet werden, und wir sollten sie überprüfen, aber keine Implementierungsmethoden, LDM 2025-04-17)
  • Sollten wir ein Attribut zu der statischen Klasse [Extension] hinzufügen, auch wenn keine Instanzerweiterungsmethode darin vorhanden ist? (Antwort: Ja, LDM 2025-03-10)
  • Bestätigen Sie, dass wir das Attribut [Extension] auch zu den Implementierungs-Gettern und -Settern hinzufügen sollten. (Antwort: nein, LDM 2025-03-10)
  • Bestätigen Sie, dass die Erweiterungstypen mit einem besonderen Namen gekennzeichnet werden sollen und dass der Compiler dieses Flag in Metadaten erfordert (dies ist ein Breaking Change im Vergleich zur Vorschau) (Antwort: genehmigt, LDM 2025-06-23)

Szenario für statische Factory

  • Was sind die Konfliktregeln für statische Methoden? (Antwort: Verwenden vorhandener C#-Regeln für den eingeschlossenen statischen Typ, keine Lockerung, LDM 2025-03-17)

Suche

  • Wie können Sie Instanzmethodenaufrufe jetzt auflösen, da wir über sprechende Implementierungsnamen verfügen? Wir bevorzugen die Skelettmethode gegenüber der entsprechenden Implementierungsmethode.
  • Wie behebt man statische Erweiterungsmethoden? (Antwort: genau wie Instanzerweiterungsmethoden, LDM 2025-03-03)
  • Wie können Sie Eigenschaften auflösen? (antwortet in breiten Strichen LDM 2025-03-03, aber bedarf der Nachverfolgung zur Verbesserung)
  • Gültigkeitsbereichs- und Überschattungsregeln für Erweiterungsparameter und Typparameter (Antwort: im Gültigkeitsbereich des Erweiterungsblocks, Überschattung nicht zulässig, LDM 2025-03-10)
  • Wie sollte ORPA auf neue Erweiterungsmethoden angewendet werden? (Antwort: Erweiterungsblöcke sollten als transparent behandelt werden, der "enthaltende Typ" für ORPA ist die umschließende statische Klasse, LDM 2025-04-17)
public static class Extensions
{
    extension(Type1)
    {
        [OverloadResolutionPriority(1)]
        public void Overload(...)
    }
    extension(Type2)
    {
        public void Overload(...)
    }
}
  • Sollte ORPA auf neue Erweiterungseigenschaften angewendet werden? (Antwort: Ja, und ORPA sollen auf die Implementierungsmethoden kopiert werden, LDM 2025-04-23)
public static class Extensions
{
    extension(int[] i)
    {
        public P { get => }
    }
    extension(ReadOnlySpan<int> r)
    {
       [OverloadResolutionPriority(1)]
       public P { get => }
    }
}
  • Wie kann man die klassischen Regeln zur Erweiterungsauflösung retkonisieren? Machen wir?
    1. aktualisieren Sie den Standard für klassische Erweiterungsmethoden, und verwenden Sie diese, um auch neue Erweiterungsmethoden zu beschreiben,
    2. Behalten Sie die bestehende Sprache für klassische Erweiterungsmethoden bei und verwenden Sie diese auch, um neue Erweiterungsmethoden zu beschreiben, wobei es für beide eine bekannte Spezifikationsabweichung geben soll.
    3. die vorhandene Sprache für klassische Erweiterungsmethoden beibehalten, aber für neue Erweiterungsmethoden andere Sprache verwenden und nur eine bekannte Spezifikationsabweichung für klassische Erweiterungsmethoden haben?
  • Vergewissern Sie sich, dass explizite Typargumente für einen Eigenschaftszugriff nicht zulässig sein sollen (Antwort: kein Eigenschaftszugriff mit expliziten Typargumenten, die in der WG erläutert werden)
string s = "ran";
_ = s.P<object>; // error

static class E
{
    extension<T>(T t)
    {
        public int P => 0;
    }
}
  • Bestätigen Sie, dass Verbesserungsregeln auch dann angewendet werden sollen, wenn der Empfänger ein Typ ist (Antwort: Ein reiner Typ-Erweiterungsparameter sollte bei der Auflösung statischer Erweiterungsmember berücksichtigt werden, LDM 2025-06-23)
int.M();

static class E1
{
    extension(int)
    {
        public static void M() { }
    }
}
static class E2
{
    extension(in int i)
    {
        public static void M() => throw null;
    }
}
  • Bestätigen, dass es für uns in Ordnung ist, eine Mehrdeutigkeit zu haben, wenn sowohl Methoden als auch Eigenschaften anwendbar sind (Antwort: wir sollten einen Vorschlag entwerfen, der besser ist als der Status Quo, Punting aus .NET 10, LDM 2025-06-23)
  • Bestätigen, dass wir nicht wollen, dass alle Member besser sind, bevor wir die beste Member-Art bestimmen (Antwort: punting out of .NET 10, WG 2025-07-02)
string s = null;
s.M(); // error

static class E
{
    extension(string s)
    {
        public System.Action M => throw null;
    }
    extension(object o)
    {
        public string M() => throw null;
    }
}
  • Haben wir einen impliziten Empfänger innerhalb von Erweiterungsdeklarationen? (Antwort: Nein, wurde zuvor in LDM diskutiert)
static class E
{
    extension(object o)
    {
        public void M() 
        {
            M2();
        }
        public void M2() { }
    }
}
  • Soll die Suche nach typparametern zugelassen werden? (Diskussion) (Antwort: Nein, wir warten auf Feedback, LDM 2025-04-16)

Zugänglichkeit

  • Was bedeutet die Barrierefreiheit in einer Erweiterungserklärung? (Antwort: Erweiterungsdeklarationen zählen nicht als Barrierefreiheitsbereich, LDM 2025-03-17)
  • Sollten wir die "uneinheitliche Zugänglichkeit" auf den Empfängerparameter anwenden, auch für statische Elemente? (Antwort: Ja, LDM 2025-04-17)
public static class Extensions
{
    extension(PrivateType p)
    {
        // We report inconsistent accessibility error, 
        //   because we generate a `public static void M(PrivateType p)` implementation in enclosing type
        public void M() { } 

        public static void M2() { } // should we also report here, even though not technically necessary?
    }

    private class PrivateType { }
}

Überprüfung von Erweiterungsdeklarationen

  • Sollten wir die Typparameterüberprüfung entspannen (Schlussfolgerung: Alle Typparameter müssen im Typ des Erweiterungsparameters angezeigt werden), wo nur Methoden vorhanden sind? (Antwort: Ja, LDM 2025-04-06) Dies würde das Portieren von 100% klassischer Erweiterungsmethoden ermöglichen.
    Wenn Sie TResult M<TResult, TSource>(this TSource source) haben, könnten Sie es als extension<TResult, TSource>(TSource source) { TResult M() ... } übertragen.

  • Bestätigen Sie, ob init-only-Accessoren in Erweiterungen zulässig sein sollen (Antwort: okay, jetzt nicht zulassen, LDM 2025-04-17)

  • Sollte der einzige Unterschied im Bezug auf 'Ref-Ness' des Empfängers erlaubt sein extension(int receiver) { public void M2() {} }extension(ref int receiver) { public void M2() {} }? (Antwort: Nein, angegebene Regel beibehalten, LDM 2025-03-24)

  • Sollten wir uns über einen Konflikt wie diesen extension(object receiver) { public int P1 => 1; }extension(object receiver) { public int P1 {set{}} }beschweren? (Antwort: Ja, Regel beibehalten, LDM 2025-03-24)

  • Sollten wir uns über Konflikte zwischen Gerüstmethoden beschweren, die nicht als Konflikte zwischen Implementierungsmethoden gelten? (Antwort: Ja, Regel beibehalten, LDM 2025-03-24)

static class E
{
    extension(object)
    {
        public void Method() {  }
        public static void Method() { }
    }
}

Die aktuellen Konfliktregeln sind: 1. Überprüfen Sie keinen Konflikt innerhalb ähnlicher Erweiterungen mithilfe von Klassen-/Strukturregeln, 2. Überprüfen Sie keinen Konflikt zwischen Implementierungsmethoden in verschiedenen Erweiterungsdeklarationen.

  • Brauchen wir den ersten Teil der Regeln? (Antwort: Ja, wir behalten diese Struktur, da sie die Nutzung der APIs erleichtert, LDM 2025-03-24)

XML-Dokumente

  • Wird der Empfängerparameter paramref für Erweiterungsmitglieder unterstützt? Sogar statisch? Wie wird sie in der Ausgabe codiert? Wahrscheinlich würde die Standardmethode <paramref name="..."/> für einen Menschen funktionieren, aber möglicherweise sind einige vorhandene Tools nicht zufrieden, wenn sie es nicht unter den Parametern der API finden. (Antwort: Ja, parameterreferenz zum Erweiterungsparameter ist bei Erweiterungselementen erlaubt, LDM 2025-05-05)
  • Sollen wir Doc-Kommentare in die Implementierungsmethoden mit sprechenden Namen kopieren? (Antwort: kein Kopieren, LDM 2025-05-05)
  • Soll das <param>-Element, das dem Empfängerparameter entspricht, aus dem Erweiterungscontainer für Instanzmethoden kopiert werden? Soll alles andere von den Containern in die Implementierungsmethoden (<typeparam> usw.) kopiert werden? (Antwort: kein Kopieren, LDM 2025-05-05)
  • Sollte <param> als Überschreibung bei Erweiterungsmitgliedern für Erweiterungsparameter zulässig sein? (Antwort: Nein, vorerst, LDM 2025-05-05)
  • Wird die Zusammenfassung von Erweiterungsblöcken irgendwo angezeigt werden?

CREF

  • Syntax bestätigen (Antwort: Vorschlag ist gut, LDM 2025-06-09)
  • Sollte es möglich sein, auf einen Erweiterungsblock (E.extension(int))zu verweisen? (Antwort: nein, LDM 2025-06-09)
  • Sollte es möglich sein, auf ein Mitglied mit einer nicht qualifizierten Syntax zu verweisen: extension(int).Member? (Antwort: Ja, LDM 2025-06-09)
  • Sollten wir unterschiedliche Zeichenfolgen für unaussprechliche Namen verwenden, um XML-Escapezeichen zu vermeiden? (Antwort: der WG überlassen, LDM 2025-06-09)
  • Vergewissern Sie sich, dass sowohl Verweise auf Skelett- als auch Implementierungsmethoden möglich sind: E.M vs. E.extension(int).M Beide scheinen notwendig (Erweiterungseigenschaften und Portabilität klassischer Erweiterungsmethoden). (Antwort: Ja, LDM 2025-06-09)
  • Sind Erweiterungsmetadatennamen für versionsverwaltungsdokumente problematisch? (Antwort: Ja, wir werden von Ordnungszahlen wegkommen und ein inhaltsbasiertes stabiles Benennungsschema verwenden)

Support für weitere Memberarten hinzufügen

Wir müssen nicht alles auf einmal umsetzen, sondern können jeweils eine oder einige wenige Memberarten hinzufügen. Basierend auf bekannten Szenarien in unseren Kernbibliotheken ist folgende Reihenfolge zu berücksichtigen:

  1. Eigenschaften und Methoden (Instanz und statisch)
  2. Betriebspersonal
  3. Indexer (Instanz und statisch, können opportunistisch zu einem früheren Zeitpunkt durchgeführt werden)
  4. Irgendetwas anderes

Inwieweit sollte der Entwurf für andere Arten von Membern vorverlagert werden?

extension_member_declaration // add
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | finalizer_declaration
    | static_constructor_declaration
    | type_declaration
    ;

Geschachtelte Typen

Wenn wir uns entscheiden, mit verschachtelten Erweiterungstypen fortzufahren, finden Sie hier einige Anmerkungen aus früheren Diskussionen:

  • Es wäre ein Konflikt, wenn zwei Erweiterungsdeklarationen geschachtelte Erweiterungstypen mit denselben Namen und Zugehörigkeit deklariert haben. Wir haben keine Lösung, um dies in den Metadaten darzustellen.
  • Der grobe Ansatz, den wir für Metadaten diskutiert haben:
    1. Wir würden einen geschachtelten Gerüsttyp mit ursprünglichen Typparametern und ohne Member ausgeben.
    2. wir würden einen geschachtelten Implementierungstyp mit vordefinierten Typparametern aus der Erweiterungsdeklaration und allen Memberimplementierungen ausgeben, wie sie in der Quelle angezeigt werden (Moduloverweise auf Typparameter)

Erbauer

Konstruktoren werden im Allgemeinen als Instanzmember in C# beschrieben, da ihr Text über das this-Schlüsselwort Zugriff auf den neu erstellten Wert hat. Dies funktioniert jedoch nicht gut für den parameterbasierten Ansatz für Instanzerweiterungsmember, da es keinen vorherigen Wert gibt, der als Parameter übergeben werden soll.

Stattdessen funktionieren Erweiterungskonstruktoren eher wie statische Factorymethoden. Sie gelten als statische Mitglieder in dem Sinne, dass sie nicht von einem Empfängerparameternamen abhängen. Ihre Texte müssen das Konstruktionsergebnis explizit erstellen und zurückgeben. Das Member selbst wird weiterhin mit der Konstruktorsyntax deklariert, kann aber nicht über this- oder base-Initialisierer verfügen und ist nicht davon abhängig, ob der Empfängertyp zugängliche Konstruktoren hat.

Dies bedeutet auch, dass Erweiterungskonstruktoren für Typen deklariert werden können, die keine eigenen Konstruktoren haben, z. B. Schnittstellen und Enumerationstypen:

public static class Enumerable
{
    extension(IEnumerable<int>)
    {
        public static IEnumerable(int start, int count) => Range(start, count);
    }
    public static IEnumerable<int> Range(int start, int count) { ... } 
}

Allows:

var range = new IEnumerable<int>(1, 100);

Kürzere Formen

Der vorgeschlagene Entwurf vermeidet die Wiederholung der Empfängerspezifikationen pro Member, führt jedoch dazu, dass Erweiterungsmember zweimal in einer statischen Klasse und Erweiterungsdeklaration geschachtelt werden. Es wird wahrscheinlich vorkommen, dass statische Klassen nur eine Erweiterungsdeklaration enthalten oder dass Erweiterungsdeklarationen nur ein Member enthalten, und es scheint plausibel, dass wir syntaktische Abkürzungen dieser Fälle zulassen.

Zusammenführen statischer Klassen- und Erweiterungsdeklarationen:

public static class EmptyExtensions : extension(IEnumerable source)
{
    public bool IsEmpty => !source.GetEnumerator().MoveNext();
}

Dies ähnelt eher einem sogenannten „typbasierten“ Ansatz, bei dem der Container für Erweiterungsmember selbst benannt wird.

Erweiterungsdeklaration und Erweiterungselement zusammenführen:

public static class Bits
{
    extension(ref ulong bits) public bool this[int index]
    {
        get => (bits & Mask(index)) != 0;
        set => bits = value ? bits | Mask(index) : bits & ~Mask(index);
    }
    static ulong Mask(int index) => 1ul << index;
}
 
public static class Enumerable
{
    extension<TSource>(IEnumerable<TSource> source) public IEnumerable<TSource> Where(Func<TSource, bool> predicate) { ... }
}

Dies ähnelt eher einem sogenannten „Member-basierten“ Ansatz, bei dem jedes Erweiterungsmitglied seine eigene Empfängerspezifikation enthält.