Condividi tramite


Membri dell'estensione

Annotazioni

Questo articolo è una specifica delle funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.

Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tali differenze vengono riportate nelle note pertinenti della riunione di progettazione linguistica (LDM) .

Ulteriori dettagli sul processo di adozione delle specifiche di funzionalità nello standard del linguaggio C# sono disponibili nell'articolo sulle specifiche .

Questione prioritaria: https://github.com/dotnet/csharplang/issues/8697

Dichiarazione

Sintassi

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

Le dichiarazioni di estensione devono essere dichiarate solo in classi statiche non generiche e non annidate.
Si tratta di un errore per il nome extensiondi un tipo .

Regole di ambito

I parametri di tipo e il parametro ricevitore di una dichiarazione di estensione si trovano nell'ambito all'interno del corpo della dichiarazione di estensione. Si tratta di un errore per fare riferimento al parametro ricevitore dall'interno di un membro statico, tranne all'interno di un'espressione nameof . È un errore per i membri dichiarare parametri o parametri di tipo (nonché variabili locali e funzioni locali direttamente all'interno del corpo del membro) con lo stesso nome di un parametro di tipo o di un parametro ricevitore della dichiarazione di estensione.

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

Non è un errore che i membri abbiano lo stesso nome dei parametri di tipo o del parametro ricevitore della dichiarazione di estensione contenente. I nomi dei membri non vengono trovati direttamente in una ricerca di nome semplice dall'interno della dichiarazione di estensione; La ricerca troverà quindi il parametro di tipo o il parametro ricevitore di tale nome, anziché il membro.

I membri portano alla dichiarazione di metodi statici direttamente nella classe statica contenitore, e questi possono essere trovati tramite una semplice ricerca per nome; tuttavia, verrà trovato per primo un parametro di dichiarazione di estensione o un parametro ricevitore con lo stesso nome.

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

Classi statiche come contenitori di estensioni

Le estensioni vengono dichiarate all'interno di classi statiche non generiche di primo livello, proprio come i metodi di estensione oggi, e possono quindi coesistere con i metodi di estensione classici e i membri statici non di estensione:

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

Dichiarazioni di estensione

Una dichiarazione di estensione è anonima e fornisce una specifica del ricevitore con tutti i parametri e i vincoli di tipo associati, seguiti da un set di dichiarazioni di membri di estensione. La specifica del ricevitore può essere sotto forma di parametro oppure, se vengono dichiarati solo membri di estensione statici, un tipo:

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

Il tipo nella specifica del ricevitore viene definito tipo di ricevitore e il nome del parametro, se presente, viene definito parametro ricevitore.

Se il parametro del ricevitore è denominato, il tipo di ricevitore potrebbe non essere statico.
Il parametro receiver non è autorizzato ad avere modificatori se non è nominato e, in caso contrario, può avere solo i modificatori di riflessività elencati di seguito e scoped.
Il parametro ricevitore presenta le stesse restrizioni del primo parametro di un metodo di estensione classico.
L'attributo [EnumeratorCancellation] viene ignorato se viene inserito nel parametro del ricevitore.

Membri dell'estensione

Le dichiarazioni dei membri di estensione sono sintatticamente identiche a membri statici e di istanza corrispondenti nelle dichiarazioni di classe e struct (ad eccezione dei costruttori). I membri dell'istanza fanno riferimento al ricevitore con il nome del parametro del ricevitore:

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

È un errore specificare un membro di estensione di istanza se la dichiarazione di estensione non specifica un parametro ricevitore.

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

È un errore specificare i seguenti modificatori per un membro di una dichiarazione di estensione: abstract, virtual, override, new, sealed, partial, e protected (e i modificatori di accessibilità correlati).
Si tratta di un errore per specificare il readonly modificatore in un membro di una dichiarazione di estensione.
Le proprietà nelle dichiarazioni di estensione potrebbero non avere init funzioni di accesso.
I membri dell'istanza non sono consentiti se il parametro receiver è privo di nome.

Tutti i membri devono avere nomi diversi dal nome della classe di inclusione statica e dal nome del tipo esteso, se presente.

È un errore decorare un membro di estensione con l'attributo [ModuleInitializer].

Refness

Per impostazione predefinita, il ricevitore viene passato ai membri dell'estensione dell'istanza in base al valore, proprio come gli altri parametri. Tuttavia, un ricevitore di dichiarazione di estensione nel formato del parametro può specificare ref, ref readonly e in, purché il tipo di ricevitore sia noto come tipo valore.

Valori Null e attributi

I tipi di ricevitore possono essere o contenere tipi riferimento nullable e specifiche del ricevitore sotto forma di parametri possono specificare attributi:

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

Compatibilità con i metodi di estensione classici

I metodi di estensione dell'istanza generano artefatti corrispondenti a quelli prodotti dai metodi di estensione classici.

In particolare, il metodo statico generato ha gli attributi, i modificatori e il nome del metodo di estensione dichiarato, nonché l'elenco di parametri di tipo, l'elenco di parametri e gli elenchi di vincoli concatenati dalla dichiarazione di estensione e dalla dichiarazione del metodo in tale ordine:

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

Operatori

Anche se gli operatori di estensione hanno tipi operandi espliciti, devono comunque essere dichiarati all'interno di una dichiarazione di estensione:

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

In questo modo è possibile dichiarare e dedurre i parametri di tipo ed è analogo al modo in cui un normale operatore definito dall'utente deve essere dichiarato all'interno di uno dei relativi tipi di operando.

Checking

Inferribilità: Per ogni membro dell'estensione non-metodo, tutti i parametri di tipo del suo blocco di estensione devono essere utilizzati nell'insieme combinato di parametri dell'estensione e del membro.

Unicità: All'interno di una determinata classe statica contenitore, il set di dichiarazioni di membri di estensione con lo stesso tipo di ricevitore (conversione di identità modulo e sostituzione del nome del parametro di tipo) viene considerato come un singolo spazio di dichiarazione simile ai membri all'interno di una dichiarazione di classe o struct e viene trattato secondo le stesse regole sull'univocità.

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

L'applicazione di questa regola di univocità include metodi di estensione classici all'interno della stessa classe statica. Ai fini del confronto con i metodi all'interno delle dichiarazioni di estensione, il this parametro viene considerato come una specifica del ricevitore insieme a qualsiasi parametro di tipo indicato in tale tipo di ricevitore e i parametri del metodo rimanenti vengono usati per la firma del metodo:

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

Consumo

Quando viene tentata la ricerca di un membro di estensione, tutte le dichiarazioni di estensione all'interno di usingclassi statiche importate contribuiscono ai membri come candidati, indipendentemente dal tipo di ricevitore. Solo come parte della risoluzione sono candidati con tipi ricevitori incompatibili rimossi.
Viene tentata un'inferenza completa del tipo generico tra il tipo degli argomenti (incluso il ricevitore effettivo) e qualsiasi parametro di tipo (combinando quelli nella dichiarazione di estensione e nella dichiarazione del membro di estensione).
Quando vengono forniti argomenti di tipo espliciti, vengono usati per sostituire i parametri di tipo della dichiarazione di estensione e la dichiarazione del membro di estensione.

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

Analogamente ai metodi di estensione classici, i metodi di implementazione generati possono essere richiamati in modo statico.
Ciò consente al compilatore di disambiguare tra i membri di estensione con lo stesso nome e la stessa arità.

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

I metodi di estensione statici verranno risolti come i metodi di estensione dell'istanza (si considererà un argomento aggiuntivo del tipo di ricevitore).
Le proprietà di estensione verranno risolte come i metodi di estensione, con un singolo parametro (il parametro ricevitore) e un singolo argomento (il valore ricevitore effettivo).

Direttiveusing static

Un using_static_directive rende disponibili i membri dei blocchi di estensione nella dichiarazione di tipo per l'accesso all'estensione.

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

Come in precedenza, è possibile fare riferimento direttamente ai membri statici accessibili (ad eccezione dei metodi di estensione) contenuti direttamente nella dichiarazione del tipo specificato.
Ciò significa che i metodi di implementazione (ad eccezione di quelli che sono metodi di estensione) possono essere usati direttamente come metodi statici:

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

Un using_static_directive non importa ancora metodi di estensione direttamente come metodi statici, pertanto il metodo di implementazione per i metodi di estensione non statici non può essere richiamato direttamente come metodo statico.

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

I membri di estensione all'interno di una classe statica di inclusione sono soggetti alla definizione delle priorità in base ai valori ORPA. La classe statica di contenimento è considerata il "tipo contenitore" preso in considerazione dalle regole ORPA.
Qualsiasi attributo ORPA presente in una proprietà di estensione viene copiato nei metodi di implementazione per gli accessori della proprietà, in modo che la priorità venga rispettata quando tali accessori vengono usati tramite sintassi di disambiguazione.

Punti di ingresso

I metodi dei blocchi di estensione non sono idonei come candidati al punto di ingresso (vedere "7.1 Avvio dell'applicazione"). Nota: il metodo potrebbe comunque essere ancora candidato all'implementazione.

Abbassamento

La strategia di riduzione per le dichiarazioni di estensione non è una decisione a livello di linguaggio. Tuttavia, oltre all'implementazione della semantica del linguaggio, deve soddisfare determinati requisiti:

  • Il formato di tipi generati, membri e metadati deve essere specificato chiaramente in tutti i casi in modo che altri compilatori possano utilizzarli e generarlo.
  • Gli artefatti generati devono essere stabili, nel senso che le modifiche successive ragionevoli non devono interrompere i consumer che sono stati compilati in base alle versioni precedenti.

Questi requisiti devono essere più perfezionati man mano che l'implementazione avanza e potrebbero essere compromessi in casi di angolo per consentire un approccio di implementazione ragionevole.

Metadati per le dichiarazioni

Obiettivi

La progettazione seguente consente:

  • roundtripping dei simboli di dichiarazione delle estensioni tramite metadata (assembly completi e di riferimento),
  • riferimenti stabili ai membri dell'estensione (documentazione xml),
  • determinazione locale dei nomi emessi (utile per EnC),
  • rilevamento API pubblico.

Per la documentazione xml, il docID per un membro di estensione è il docID per il membro dell'estensione nei metadati. Ad esempio, il docID usato in cref="Extension.extension(object).M(int)" è M:Extension.<>E__ExtensionGroupingTypeNameForObject.M(System.Int32) e che docID è stabile tra ri-compilazioni e riordinamento dei blocchi di estensione. Idealmente, resterebbe stabile anche quando i vincoli sul blocco di estensione cambiano, ma non è stato trovato un progetto che avrebbe raggiunto tale risultato senza effetti negativi sulla progettazione del linguaggio per i conflitti dei membri.

Per EnC, è utile conoscere localmente (semplicemente esaminando un membro dell'estensione modificato) dove viene generato nei metadati il membro dell'estensione aggiornato.

Per il rilevamento delle API pubbliche, i nomi più stabili riducono il rumore. Tecnicamente, tuttavia, i nomi dei tipi di raggruppamento delle estensioni non dovrebbero entrare in gioco in tali scenari. Quando si esamina il membro Mdi estensione , non è importante qual è il nome del tipo di raggruppamento delle estensioni, ciò che conta è la firma del blocco di estensione a cui appartiene. La firma dell'API pubblica non deve essere vista come Extension.<>E__ExtensionGroupingTypeNameForObject.M(System.Int32) ma piuttosto come Extension.extension(object).M(int). In altre parole, i membri di estensione si considerano come se avessero due insiemi di parametri di tipo e due insiemi di parametri.

Informazioni generali

I blocchi di estensione vengono raggruppati in base alla firma a livello di CLR. Ogni gruppo di equivalenze CLR viene generato come tipo di raggruppamento di estensioni con un nome basato sul contenuto. I blocchi di estensione all'interno di un gruppo di equivalenze CLR vengono quindi raggruppati in base all'equivalente C#. Ogni gruppo di equivalenze C# viene generato come tipo di marcatore di estensione con un nome basato sul contenuto, annidato nel tipo di raggruppamento delle estensioni corrispondente. Un tipo di marcatore di estensione contiene un singolo metodo di marcatore di estensione che codifica un parametro di estensione. Il metodo dell'indicatore di estensione con il tipo di marcatore di estensione che lo contiene codifica la firma di un blocco di estensione con fedeltà totale. La dichiarazione di ogni membro di estensione viene generata nel tipo di raggruppamento di estensioni corretto, fa riferimento a un tipo di marcatore di estensione in base al nome tramite un attributo ed è accompagnato da un metodo di implementazione statico di primo livello con una firma modificata.

Ecco una panoramica schematizzata della codifica dei metadati:

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

La classe di inclusione statica viene emessa con un attributo [Extension].

Firma a livello di CLR e firma a livello di C#

La firma a livello CLR di un blocco di estensione restituisce i risultati seguenti:

  • normalizzazione dei nomi dei parametri di tipo in T0, T1e così via...
  • rimozione degli attributi
  • cancellazione del nome del parametro
  • cancellazione dei modificatori di parametri (ad esempio ref, inscoped, , ...)
  • cancellazione dei nomi delle tuple
  • cancellazione delle annotazioni di nullità
  • cancellazione dei notnull vincoli

Nota: altri vincoli vengono mantenuti, ad esempio new(), struct, classallows ref struct, unmanaged, e vincoli di tipo.

Tipi di raggruppamento di estensioni

Un tipo di raggruppamento di estensioni viene generato nei metadati per ogni insieme di blocchi di estensione nel codice sorgente con la medesima firma a livello di CLR.

  • Il suo nome è indicibile e determinato dai contenuti della firma a livello CLR. Altri dettagli di seguito.
  • I parametri di tipo hanno nomi normalizzati (T0, T1, ...) e non hanno attributi.
  • È pubblico e sigillato.
  • È contrassegnato con il specialname flag e un [Extension] attributo.

Il nome basato sul contenuto del tipo di raggruppamento delle estensioni si basa sulla firma a livello di CLR e include quanto segue:

  • Nome CLR completo del tipo del parametro di estensione.
    • I nomi dei parametri di tipo a cui si fa riferimento verranno normalizzati in T0, e T1così via... in base all'ordine in cui vengono visualizzati nella dichiarazione di tipo.
    • Il nome completamente qualificato non includerà l'assembly contenitore. È comune che i tipi vengano spostati tra assembly senza che ciò interrompa i riferimenti dei documenti XML.
  • I vincoli dei parametri di tipo verranno inclusi e ordinati in modo che riordinarli nel codice sorgente non modifichi il nome. In particolare:
    • I vincoli dei parametri di tipo verranno elencati nell'ordine di dichiarazione. I vincoli per il parametro di tipo Nth si verificheranno prima del parametro di tipo Nth+1.
    • I vincoli di tipo verranno ordinati confrontando i nomi completi in modo ordinale.
    • I vincoli non di tipo vengono ordinati in modo deterministico e vengono gestiti in modo da evitare ambiguità o collisioni con vincoli di tipo.
  • Poiché questo non include attributi, ignora intenzionalmente peculiarità del C# come nomi delle tuple, nullabilità e così via...

Nota: il nome rimane stabile tra ri-compilazioni, riordinazioni e modifiche di elementi specifici di C#, cioè che non influiscono sulla firma a livello di CLR.

Tipi di marcatori di estensione

Il tipo marcatore ridefinisce i parametri di tipo del suo tipo di raggruppamento contenitore (un tipo di raggruppamento di estensione) per ottenere un'accurata rappresentazione dei blocchi di estensione nella visualizzazione C#.

Un tipo di marcatore di estensione viene generato nei metadati per ogni set di blocchi di estensione nel codice sorgente con la stessa firma a livello di linguaggio C#.

  • Il nome è indicibile e determinato in base al contenuto della firma a livello C# del blocco di estensione. Altri dettagli di seguito.
  • Ridefinisce i parametri di tipo per il tipo a cui appartiene, facendoli corrispondere a quelli originariamente dichiarati nella fonte (inclusi nome e attributi).
  • È pubblico e statico.
  • È contrassegnato con il specialname flag .

Il nome basato sul contenuto del tipo di marcatore di estensione si basa sul seguente:

  • I nomi dei parametri di tipo verranno inclusi nell'ordine in cui vengono visualizzati nella dichiarazione di estensione
  • Gli attributi dei parametri di tipo verranno inclusi e ordinati in modo che riordinarli nel codice sorgente non modifichi il nome.
  • I vincoli dei parametri di tipo verranno inclusi e ordinati in modo che riordinarli nel codice sorgente non modifichi il nome.
  • Nome completamente qualificato C# del tipo esteso
    • Questo includerà elementi come annotazioni annullabili, nomi delle tuple e altro ancora...
    • Il nome qualificato completo non includerà l'assembly contenitore
  • Nome del parametro di estensione
  • Modificatori del parametro di estensione (ref, ref readonly, scoped, ...) in ordine deterministico
  • Il nome completo e gli argomenti dell'attributo per tutti gli attributi applicati al parametro di estensione in un ordine deterministico

Nota: il nome rimane stabile tra ri-compilazioni e riordinamento.
Nota: i tipi di marcatori di estensione e i metodi dell'indicatore di estensione vengono generati come parte degli assembly di riferimento.

Metodo dell'indicatore di estensione

Lo scopo del metodo marcatore è codificare il parametro di estensione del blocco di estensione. Poiché è un membro del tipo di marcatore di estensione, può fare riferimento ai parametri di tipo nuovamente dichiarati del tipo di marcatore di estensione.

Ogni tipo di marcatore di estensione contiene un singolo metodo, ovvero il metodo dell'indicatore di estensione.

  • È statico, non generico, che restituisce void e viene chiamato <Extension>$.
  • Il singolo parametro ha gli attributi, il riferimento, il tipo e il nome del parametro di estensione.
    Se il parametro di estensione non specifica un nome, il nome del parametro è vuoto.
  • È contrassegnato con il specialname flag .

L'accessibilità del metodo marcatore sarà quella meno restrittiva tra i membri dichiarati delle estensioni corrispondenti; nel caso in cui nessuno sia dichiarato, viene utilizzato private.

Membri dell'estensione

Le dichiarazioni di metodo/proprietà in un blocco di estensione nell'origine vengono rappresentate come membri del tipo di raggruppamento delle estensioni nei metadati.

  • Le firme dei metodi originali vengono mantenute (inclusi gli attributi), ma i relativi corpi vengono sostituiti con throw NotImplementedException().
  • Tali riferimenti non devono essere referenziati in IL.
  • I metodi, le proprietà e gli accessori sono contrassegnati con [ExtensionMarkerName("...")], che fa riferimento al nome del tipo di marcatore di estensione corrispondente al blocco di estensione per tale membro.

Metodi di implementazione

I corpi dei metodi per le dichiarazioni di metodo/proprietà in un blocco di estensione nel codice sorgente vengono generati come metodi di implementazione statici nella classe statica a livello superiore.

  • Un metodo di implementazione ha lo stesso nome del metodo originale.
  • Dispone di parametri di tipo derivati dal blocco di estensione anteponiti ai parametri di tipo del metodo originale (inclusi gli attributi).
  • Ha gli stessi attributi e accessibilità del metodo originale.
  • Se implementa un metodo statico, ha gli stessi parametri e il tipo restituito.
  • Se implementa un metodo di istanza, ha un parametro anteporto alla firma del metodo originale. Gli attributi, il riferimento, il tipo e il nome di questo parametro derivano dal parametro di estensione dichiarato nel blocco di estensione pertinente.
  • I parametri nei metodi di implementazione fanno riferimento ai parametri di tipo di proprietà del metodo di implementazione, anziché a quelli di un blocco di estensione.
  • Se il membro originale è un metodo ordinario dell'istanza, il metodo di implementazione viene contrassegnato con un [Extension] attributo .

Attributo ExtensionMarkerName

Il tipo ExtensionMarkerNameAttribute è destinato solo all'uso del compilatore. Non è consentito nell'origine. La dichiarazione di tipo viene sintetizzata dal compilatore, se non è già inclusa nella compilazione.

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

Nota: anche se alcune destinazioni degli attributi sono incluse per anticipare cambiamenti futuri (tipi annidati di estensione, campi di estensione, eventi di estensione), AttributeTargets.Constructor non è incluso poiché i costruttori di estensione non sarebbero costruttori.

Esempio

Nota: vengono usati nomi semplificati basati sul contenuto per l'esempio, per la leggibilità. Nota: poiché C# non può rappresentare la ri-dichiarazione del parametro di tipo, il codice che rappresenta i metadati non è un codice C# valido.

Ecco un esempio che illustra il funzionamento del raggruppamento, senza membri:

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

viene generato come

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

Ecco un esempio che illustra come vengono emessi i membri:

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() { ... }
}

viene generato come

[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() { ... }
}

Ogni volta che i membri dell'estensione vengono usati nell'origine, verranno generati come riferimento ai metodi di implementazione. Ad esempio, una chiamata di enumerableOfInt.Method() viene generata come chiamata statica a IEnumerableExtensions.Method<int>(enumerableOfInt).

Documentazione XML

I commenti del documento sul blocco di estensione vengono emessi per il tipo di marcatore (il DocID per il blocco di estensione è E.<>E__MarkerContentName_For_ExtensionOfT'1 nell'esempio sottostante).
È consentito fare riferimento rispettivamente al parametro di estensione e ai parametri di tipo usando <paramref> e <typeparamref> .
Nota: non è possibile documentare il parametro di estensione o i parametri di tipo (con <param> e <typeparam>) in un membro di estensione.

Se due blocchi di estensione vengono generati come un tipo di marcatore, vengono uniti anche i commenti del documento.

Gli strumenti che usano i documenti xml sono responsabili di copiare il <param> e <typeparam> dal blocco di estensione ai membri dell'estensione come appropriato (ad esempio, le informazioni sui parametri devono essere copiate solo per i membri dell'istanza).

Un <inheritdoc> viene emesso sui metodi di implementazione e si riferisce al membro di estensione pertinente con un cref. Ad esempio, il metodo di implementazione per un getter fa riferimento alla documentazione della proprietà di estensione. Se il membro dell'estensione non ha commenti doc, l'oggetto <inheritdoc> viene omesso.

Per i blocchi di estensione e i membri dell'estensione, non viene attualmente visualizzato un avviso se:

  • il parametro di estensione è documentato, ma i parametri del membro dell'estensione non lo sono.
  • o viceversa
  • o negli scenari equivalenti con parametri di tipo non documentati

Ad esempio, i commenti del documento seguenti:

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

produrre il codice XML seguente:

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

Riferimenti CREF

È possibile trattare blocchi di estensione come tipi annidati, che possono essere risolti dalla firma (come se fosse un metodo con un singolo parametro di estensione). Esempio: E.extension(ref int).M().

Ma un cref non può risolvere un blocco di estensione stesso. E.extension(int) può fare riferimento a un metodo denominato "extension" nel tipo E.

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

La ricerca sa come esaminare tutti i blocchi di estensione corrispondenti.
Poiché non consentiamo riferimenti non qualificati ai membri dell'estensione, anche cref non li consentirebbe.

La sintassi sarà:

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
  ;

È un errore utilizzare extension_member_cref a livello superiore (extension(int).M) o incapsulato in un'altra estensione (E.extension(int).extension(string).M).

Modifiche radicali

I tipi e gli alias non possono essere denominati "estensione".

Problemi aperti

Sezione temporanea del documento relativo a problemi aperti, tra cui la discussione sulla sintassi non finalizzata e le progettazioni alternative
  • Quando si accede a un membro dell'estensione, è necessario modificare i requisiti del ricevitore? (commento)
  • Confermare extension o extensions come parola chiave (risposta: extension, LDM 2025-03-24)
  • Confermare che vogliamo vietare [ModuleInitializer] (risposta: sì, vietare, LDM 2025-06-11)
  • Confermare che è possibile scartare i blocchi di estensione come candidati al punto di ingresso (risposta: sì, scartare, LDM 2025-06-11)
  • Confermare la logica di LangVer (ignorare le nuove estensioni e prendere in considerazione e segnalarle quando selezionata) (risposta: binding incondizionato e segnalare l'errore LangVer ad eccezione dei metodi di estensione dell'istanza, LDM 2025-06-11)
  • Deve partial essere richiesto per i blocchi di estensione che si uniscono e i cui commenti di documentazione vengono uniti? (risposta: i commenti del documento vengono uniti automaticamente quando i blocchi vengono uniti, non partial sono necessari, confermati tramite posta elettronica 2025-09-03)
  • Assicurarsi che i membri non debbano essere denominati in base ai tipi contenenti o estesi. (risposta: sì, confermato tramite posta elettronica 2025-09-03)

Rivedere le regole di raggruppamento/conflitto alla luce del problema di portabilità: https://github.com/dotnet/roslyn/issues/79043

(risposta: questo scenario è stato risolto come parte della nuova progettazione di metadati con nomi di tipi basati sul contenuto, è consentito)

La logica corrente consiste nel raggruppare blocchi di estensione con lo stesso tipo di ricevitore. Questo non tiene conto dei vincoli. Questo causa un problema di portabilità con questo scenario:

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

La proposta consiste nell'usare la stessa logica di raggruppamento che stiamo pianificando per il design del tipo di raggruppamento delle estensioni, vale a dire tenere conto dei vincoli a livello di CLR, ovvero ignorando notnull, tuple names, annotazioni di nullabilità.

Il riferimento deve essere codificato nel nome del tipo di raggruppamento?

  • Rivedere la proposta che ref non è inclusa nel nome del tipo di raggruppamento di estensioni (richiede ulteriore discussione dopo che WG rivede le regole di raggruppamento/conflitti, LDM 2025-06-23) (risposta: confermata dal messaggio di posta elettronica 2025-09-03)
public static class E
{
  extension(ref int)
  {
    public static void M()
  }
}

Viene emesso come:

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

E i riferimenti CREF di terze parti per E.extension(ref int).M vengono emessi come M:E.<>ExtensionGroupingTypeXYZ.M() Se ref viene rimosso o aggiunto a un parametro di estensione, probabilmente non si vuole che i CREF si rompano.

Non ci importa molto di questo scenario, poiché qualsiasi utilizzo come estensione sarebbe un'ambiguità.

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

Tuttavia, ci interessa questo scenario (per portabilità e utilità) e questo dovrebbe funzionare con la progettazione dei metadati proposta dopo aver modificato le regole dei conflitti:

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

Non tenere conto del refness ha un aspetto negativo, poiché perdiamo portabilità in questo scenario:

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

nome di

  • Dovremmo vietare le proprietà di estensione in nameof, così come facciamo con i metodi di estensione classici e nuovi? (risposta: vorremmo usare `nameof(EnclosingStaticClass.ExtensionMember). Richiede progettazione, probabilmente rimandato da .NET 10. LDM 2025-06-11)

costrutti basati su modelli

Metodi

  • Dove devono entrare in gioco nuovi metodi di estensione? (risposta: stessi luoghi in cui entra in gioco i metodi di estensione classici, LDM 2025-05-05)

Sono inclusi:

  • GetEnumerator / GetAsyncEnumerator in foreach
  • Deconstruct in decostruzione, nel modello posizionale e in foreach
  • Add negli inizializzatori di raccolta
  • GetPinnableReference in fixed
  • GetAwaiter in await

Ciò esclude:

  • Dispose / DisposeAsync in using e foreach
  • MoveNext / MoveNextAsync in foreach
  • Slice e int indicizzatori negli indicizzatori impliciti (ed eventualmente list-patterns?)
  • GetResult in await

Proprietà e indicizzatori

  • Dove devono entrare in gioco le proprietà di estensione e gli indicizzatori? (risposta: iniziamo con i quattro, LDM 2025-05-05)

Includeremo:

  • inizializzatore di oggetti: new C() { ExtensionProperty = ... }
  • intializer dizionario: new C() { [0] = ... }
  • with: x with { ExtensionProperty = ... }
  • modelli di proprietà: x is { ExtensionProperty: ... }

Escluderemmo:

  • Current in foreach
  • IsCompleted in await
  • Count / Length proprietà e indicizzatori nel modello di elenco
  • Count / Length proprietà e indicizzatori negli indicizzatori impliciti
Proprietà che restituiscono delegati
  • Verificare che le proprietà di estensione di questa forma vengano eseguite solo nelle query LINQ, in modo che corrispondano alle proprietà dell'istanza. (risposta: ha senso, LDM 2025-04-06)
Modello di elenco e diffusione
  • Verificare che gli indicizzatori di estensione Index/Range debbano essere riprodotti in list-patterns (risposta: non pertinente per C# 14)
Rivedere la posizione in cui Count/Length vengono in gioco le proprietà dell'estensione

Espressioni di raccolta

  • L'estensione Add funziona
  • L'estensione GetEnumerator funziona per la diffusione
  • L'estensione non influisce GetEnumerator sulla determinazione del tipo di elemento (deve essere un'istanza)
  • I metodi di estensione statici Create non devono essere conteggiati come metodo di creazione benedetto
  • Le proprietà conteggiabili delle estensioni devono influire sulle espressioni della raccolta?

params raccolte

  • Le estensioni Add non influiscono sui tipi consentiti con params

espressioni da dizionario

  • Verificare che gli indicizzatori di estensione non vengano utilizzati nelle espressioni di dizionario, poiché la presenza dell'indicizzatore è parte integrante di ciò che caratterizza un tipo di dizionario. (risposta: non pertinente per C# 14)

extern

Schema di denominazione/numerazione per il tipo di estensione

Problema
Il sistema di numerazione corrente causa problemi con la convalida delle API pubbliche che garantisce che le API pubbliche corrispondano tra assembly di sola riferimento e assembly di implementazione.

È necessario apportare una delle modifiche seguenti? (risposta: stiamo adottando uno schema di denominazione basato sul contenuto per aumentare la stabilità dell'API pubblica e gli strumenti dovranno comunque essere aggiornati per tenere conto dei metodi marcatori)

  1. regolare lo strumento
  2. usare uno schema di denominazione basato sul contenuto (TBD)
  3. lasciare che il nome venga controllato tramite una sintassi

Il nuovo metodo Cast di estensione generico non può ancora funzionare in LINQ

Problema
Nelle progettazioni precedenti di ruoli/estensioni, è possibile specificare solo gli argomenti di tipo del metodo in modo esplicito.
Ma ora che ci concentriamo sulla transizione apparentemente inutile dai metodi di estensione classici, tutti gli argomenti di tipo devono essere forniti in modo esplicito.
Questo non riesce ad affrontare un problema relativo all'utilizzo del metodo Cast dell'estensione in LINQ.

È necessario apportare una modifica alla funzionalità delle estensioni per supportare questo scenario? (risposta: no, questo non ci fa rivedere la progettazione della risoluzione dell'estensione, LDM 2025-05-05)

Vincolare il parametro di estensione in un membro dell'estensione

È consigliabile consentire quanto segue? (risposta: no, potrebbe essere aggiunto in un secondo momento)

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

Nullabilità

  • Confermare la progettazione corrente, ad esempio massima portabilità/compatibilità (risposta: sì, 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!;
    }

Metadati

  • I metodi skeleton dovrebbero generare NotSupportedException o un'altra eccezione standard (attualmente facciamo throw null;)? (risposta: sì, LDM 2025-04-17)
  • È consigliabile accettare più parametri nel metodo marcatore nei metadati (nel caso in cui le nuove versioni aggiungono altre informazioni)? (risposta: possiamo rimanere rigidi, LDM 2025-04-17)
  • Il marcatore di estensione o i metodi di implementazione parlabili devono essere contrassegnati con un nome speciale? (risposta: il metodo marcatore deve essere contrassegnato con un nome speciale e dobbiamo controllarlo, ma non i metodi di implementazione, LDM 2025-04-17)
  • È necessario aggiungere [Extension] attributo alla classe statica anche quando non è presente alcun metodo di estensione dell'istanza all'interno? (risposta: sì, LDM 2025-03-10)
  • Verificare che sia necessario aggiungere [Extension] l'attributo anche ai getter e ai setter di implementazione. (risposta: no, LDM 2025-03-10)
  • Verificare che i tipi di estensione debbano essere contrassegnati con un nome speciale e che il compilatore richieda questo flag nei metadati (si tratta di una modifica che causa un'interruzione dall'anteprima) (risposta: approvato, LDM 2025-06-23)

scenario di fabbrica statica

  • Quali sono le regole di conflitto per i metodi statici? (risposta: usare le regole C# esistenti per il tipo statico circostante, nessun rilassamento, LDM 2025-03-17)

Ricerca

  • Come risolvere le chiamate al metodo di istanza ora che sono disponibili nomi di implementazione parlabili? Preferiamo il metodo skeleton al metodo di implementazione corrispondente.
  • Come risolvere i metodi di estensione statici? (risposta: proprio come i metodi di estensione di un'istanza, LDM 2025-03-03)
  • Come risolvere le proprietà? (risposto in tratti ampi LDM 2025-03-03, ma ha bisogno di ulteriore verifica per ottenere un miglioramento)
  • Regole di ambito e shadowing per parametri di estensione e parametri di tipo (risposta: nell'ambito del blocco di estensione, shadowing non consentito, LDM 2025-03-10)
  • In che modo ORPA deve essere applicato ai nuovi metodi di estensione? (risposta: considerare i blocchi di estensione come trasparenti, il "tipo contenitore" per ORPA è la classe statica delimitante, LDM 2025-04-17)
public static class Extensions
{
    extension(Type1)
    {
        [OverloadResolutionPriority(1)]
        public void Overload(...)
    }
    extension(Type2)
    {
        public void Overload(...)
    }
}
  • ORPA deve essere applicato alle nuove proprietà di estensione? (risposta: sì e ORPA deve essere copiato nei metodi di implementazione, LDM 2025-04-23)
public static class Extensions
{
    extension(int[] i)
    {
        public P { get => }
    }
    extension(ReadOnlySpan<int> r)
    {
       [OverloadResolutionPriority(1)]
       public P { get => }
    }
}
  • Come retconizzare le regole di risoluzione delle estensioni classiche? Facciamo noi?
    1. aggiornare lo standard per i metodi di estensione classici e usarlo per descrivere anche i nuovi metodi di estensione,
    2. mantenere il linguaggio esistente per i metodi di estensione classici, usarlo anche per descrivere nuovi metodi di estensione, ma hanno una deviazione specifica nota per entrambi,
    3. mantenere il linguaggio esistente per i metodi di estensione classici, ma usare un linguaggio diverso per i nuovi metodi di estensione e avere solo una deviazione specifica nota per i metodi di estensione classici?
  • Confermare che vogliamo vietare argomenti di tipo esplicito su un accesso a una proprietà (risposta: nessun accesso a proprietà con argomenti di tipo esplicito, discusso nel WG)
string s = "ran";
_ = s.P<object>; // error

static class E
{
    extension<T>(T t)
    {
        public int P => 0;
    }
}
  • Verificare che si vogliano applicare regole di miglioramento anche quando il ricevitore è un tipo (risposta: è consigliabile prendere in considerazione un parametro di estensione solo tipo durante la risoluzione dei membri di estensione statici, 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;
    }
}
  • Confermare che siamo d'accordo con l'ambiguità quando sia i metodi che le proprietà sono applicabili (risposta: è necessario progettare una proposta per fare meglio dello status quo, rimandando .NET 10, LDM 2025-06-23)
  • Confermare che non vogliamo alcuni miglioramenti su tutti i membri prima di determinare la categoria di membro vincente (risposta: 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;
    }
}
  • All'interno delle dichiarazioni di estensione è presente un ricevitore implicito? (risposta: no, è stato illustrato in precedenza in LDM)
static class E
{
    extension(object o)
    {
        public void M() 
        {
            M2();
        }
        public void M2() { }
    }
}
  • È consigliabile consentire la ricerca nel parametro di tipo? (discussione) (risposta: no, attenderemo commenti e suggerimenti, LDM 2025-04-16)

Accessibilità

  • Qual è il significato dell'accessibilità all'interno di una dichiarazione di estensione? (risposta: le dichiarazioni di estensione non sono conteggiati come ambito di accessibilità, LDM 2025-03-17)
  • È necessario applicare il controllo "accessibilità incoerente" sul parametro ricevitore anche per i membri statici? (risposta: sì, 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 { }
}

Convalida della dichiarazione di estensione

  • È consigliabile ridurre la convalida dei parametri di tipo (inferrità: tutti i parametri di tipo devono essere visualizzati nel tipo del parametro di estensione) in cui sono presenti solo metodi? (risposta: sì, LDM 2025-04-06) Ciò consentirebbe la conversione di 100% dei metodi di estensione classici.
    Se si dispone di TResult M<TResult, TSource>(this TSource source), è possibile convertirlo come extension<TResult, TSource>(TSource source) { TResult M() ... }.

  • Verificare se le funzioni di accesso init-only devono essere consentite nelle estensioni (risposta: ok per non consentire per il momento, LDM 2025-04-17)

  • La sola differenza nella ref-ness del ricevitore dovrebbe essere consentita extension(int receiver) { public void M2() {} }extension(ref int receiver) { public void M2() {} }? (risposta: no, mantieni la regola specificata, LDM 2025-03-24)

  • Dobbiamo lamentarci di un conflitto come questo extension(object receiver) { public int P1 => 1; }extension(object receiver) { public int P1 {set{}} }? (risposta: sì, mantieni la regola specificata, LDM 2025-03-24)

  • È consigliabile lamentarsi dei conflitti tra i metodi di struttura che non sono conflitti tra i metodi di implementazione? (risposta: sì, mantieni la regola specificata, LDM 2025-03-24)

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

Le regole di conflitto correnti sono: 1. verificare l'assenza di conflitti all'interno di estensioni simili utilizzando le regole di classe/struttura, 2. Controlla che non ci siano conflitti tra i metodi di implementazione attraverso varie dichiarazioni di estensione.

  • Abbiamo bisogno della prima parte delle regole? (risposta: sì, stiamo mantenendo questa struttura perché aiuta a usare le API, LDM 2025-03-24)

Documentazione XML

  • Il parametro ricevitore è paramref supportato nei membri dell'estensione? Anche quando è statico? Come viene codificato nell'output? Probabilmente un modo <paramref name="..."/> standard funziona per un essere umano, ma c'è il rischio che alcuni strumenti esistenti non siano felici di non trovarlo tra i parametri nell'API. (risposta: sì, paramref al parametro di estensione è consentito per i membri dell'estensione, LDM 2025-05-05)
  • Dovremmo copiare i commenti della documentazione nei metodi di implementazione con nomi leggibili? (risposta: nessuna copia, LDM 2025-05-05)
  • Deve l'elemento corrispondente al parametro ricevitore essere copiato dal contenitore di estensione per i metodi di istanza? È necessario copiare altri elementi dal contenitore ai metodi di implementazione (<typeparam> e così via) ? (risposta: nessuna copia, LDM 2025-05-05)
  • Il parametro di estensione <param> dovrebbe essere consentito sui membri dell'estensione come override? (risposta: no, per ora, LDM 2025-05-05)
  • Verrà visualizzato il riepilogo dei blocchi di estensione ovunque?

CREF

  • Conferma sintassi (risposta: la proposta è buona, LDM 2025-06-09)
  • Dovrebbe essere possibile fare riferimento a un blocco di estensione (E.extension(int))? (risposta: no, LDM 2025-06-09)
  • È possibile fare riferimento a un membro usando una sintassi non qualificata: extension(int).Member? (risposta: sì, LDM 2025-06-09)
  • È opportuno usare caratteri diversi per il nome impronunciabile, per evitare l'escape XML? (risposta: rinvio a WG, LDM 2025-06-09)
  • Verificare che sia corretto che entrambi i riferimenti a uno scheletro e ai metodi di implementazione siano possibili: E.M e E.extension(int).M. Entrambi sembrano necessari (proprietà di estensione e portabilità dei metodi di estensione classici). (risposta: sì, LDM 2025-06-09)
  • I nomi dei metadati di estensione sono problematici per il controllo delle versioni della documentazione? (risposta: sì, ci sposteremo dagli ordinali e useremo uno schema di denominazione stabile basato sul contenuto)

Aggiunta del supporto per altri tipi di membri

Non è necessario implementare contemporaneamente tutto questo progetto, ma è possibile avvicinarlo a uno o a pochi tipi di membri alla volta. In base agli scenari noti nelle librerie principali, è consigliabile usare l'ordine seguente:

  1. Proprietà e metodi (istanza e statica)
  2. Operatori
  3. Gli indicizzatori (istanza e statica, possono essere eseguiti opportunisticamente in un punto precedente)
  4. Qualsiasi altra cosa

Quanto si vuole far fronte al carico della progettazione per altri tipi di membri?

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
    ;

Tipi annidati

Se si sceglie di procedere con i tipi annidati di estensione, ecco alcune note delle discussioni precedenti:

  • Ci sarebbe un conflitto se due dichiarazioni di estensione dichiarassero tipi di estensione annidati con gli stessi nomi e arità. Non è disponibile una soluzione per la rappresentazione nei metadati.
  • L'approccio approssimativo descritto per i metadati:
    1. generaremo uno scheletro di tipo annidato con parametri di tipo originali e nessun membro
    2. verrà generato un tipo annidato di implementazione con parametri di tipo anteporti dalla dichiarazione di estensione e tutte le implementazioni dei membri così come appaiono nell'origine (riferimenti modulo ai parametri di tipo)

Costruttori

I costruttori vengono in genere descritti come membro di istanza in C#, poiché il corpo ha accesso al valore appena creato tramite la this parola chiave . Ciò non funziona correttamente per l'approccio basato su parametri ai membri dell'estensione dell'istanza, anche se non esiste alcun valore precedente da passare come parametro.

I costruttori di estensione funzionano invece in modo più simile ai metodi factory statici. Sono considerati membri statici nel senso che non dipendono da un nome di parametro ricevitore. I loro corpi devono creare e restituire in modo esplicito il risultato della costruzione. Il membro stesso è ancora dichiarato con la sintassi del costruttore, ma non può avere this o base inizializzatori e non si basa sul tipo di ricevitore con costruttori accessibili.

Ciò significa anche che i costruttori di estensione possono essere dichiarati per i tipi che non dispongono di costruttori propri, ad esempio interfacce ed enumerazioni:

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

Forme più brevi

La progettazione proposta evita la ripetizione per membro delle specifiche del ricevitore, ma alla fine i membri di estensione risultano annidati su due livelli in una classe statica e in una dichiarazione di estensione. È probabile che le classi statiche contengano una sola dichiarazione di estensione o che le dichiarazioni di estensione contengano un solo membro e sembra plausibile consentire l'abbreviazione sintattica di tali casi.

Unire dichiarazioni di estensione e classi statiche:

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

Questo finisce per essere più simile a quello che è stato chiamato un approccio "basato sui tipi", in cui il contenitore per i membri dell'estensione è denominato.

Dichiarazione di estensione di merge e membro dell'estensione:

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

Questo finisce per essere più simile a quello che abbiamo chiamato un approccio "basato sui membri", in cui ogni membro di estensione contiene la propria specifica ricevitore.