Priorità di risoluzione dell'overload
Nota
Questo articolo è una specifica di 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. Le differenze vengono acquisite nelle pertinenti note del language design meeting (LDM) .
Altre informazioni sul processo per l'adozione di speclet di funzionalità nello standard del linguaggio C# sono disponibili nell'articolo sulle specifiche di .
Problema del campione: https://github.com/dotnet/csharplang/issues/7706
Sommario
Viene introdotto un nuovo attributo, System.Runtime.CompilerServices.OverloadResolutionPriority
, che può essere usato dagli autori dell'API per regolare la priorità relativa degli overload all'interno di un singolo tipo come mezzo per guidare i consumer di API per l'uso di API specifiche, anche se tali API normalmente vengono considerate ambigue o altrimenti non vengono scelte dalle regole di risoluzione dell'overload di C#.
Motivazione
Gli autori di API spesso incontrano il problema di cosa fare con un membro dopo che è stato dichiarato obsoleto. Per motivi di compatibilità con le versioni precedenti, molti manterranno il membro esistente con ObsoleteAttribute
impostato su errore in perpetuità, per evitare di interrompere i clienti che eseguono l'aggiornamento dei binari durante l'esecuzione. Ciò colpisce in particolare i sistemi plug-in, in cui l'autore di un plug-in non controlla l'ambiente in cui viene eseguito il plug-in. L'autore dell'ambiente può voler mantenere presente un metodo precedente, ma bloccarne l'accesso per qualsiasi codice appena sviluppato. Tuttavia, ObsoleteAttribute
da solo non è sufficiente. Il tipo o il membro è ancora visibile nella risoluzione dell'overload e può causare fallimenti di risoluzione indesiderati quando esiste un'alternativa perfettamente buona, ma tale alternativa è ambigua con il membro obsoleto, oppure la presenza del membro obsoleto provoca la conclusione anticipata della risoluzione dell'overload, senza che il buon membro venga mai preso in considerazione. A questo scopo, si vuole avere un modo per consentire agli autori di API di guidare la risoluzione dell'overload per risolvere l'ambiguità, in modo che possano evolvere le aree di superficie dell'API e indirizzare gli utenti verso API efficienti senza dover compromettere l'esperienza utente.
Il team di librerie di classi di base (BCL) presenta diversi esempi di dove questo può rivelarsi utile. Alcuni esempi (ipotetici) sono:
- Creazione di un overload di
Debug.Assert
che usaCallerArgumentExpression
per ottenere l'espressione da asserire, in modo che possa essere inclusa nel messaggio e renderla preferibile rispetto all'overload esistente. - Rendere
string.IndexOf(string, StringComparison = Ordinal)
preferito rispetto astring.IndexOf(string)
. Questo dovrebbe essere discusso come potenziale cambiamento di rilievo, ma si pensa che sia il miglior valore predefinito e probabilmente coincide con l'intenzione dell'utente. - Una combinazione di questa proposta e
CallerAssemblyAttribute
consentirebbe ai metodi con un'identità implicita del chiamante di evitare costose ispezioni dello stack.Assembly.Load(AssemblyName)
lo fa oggi, e potrebbe essere molto più efficiente. -
Microsoft.Extensions.Primitives.StringValues
espone una conversione implicita sia instring
che instring[]
. Ciò significa che è ambiguo quando viene passato a un metodo con overload sia diparams string[]
che diparams ReadOnlySpan<string>
. Questo attributo può essere usato per classificare in ordine di priorità uno degli overload per evitare l'ambiguità.
Progettazione dettagliata
Priorità di risoluzione dell'overload
Viene definito un nuovo concetto, overload_resolution_priority, che viene usato durante il processo di risoluzione di un gruppo di metodi.
overload_resolution_priority è un valore intero a 32 bit. Per impostazione predefinita, tutti i metodi hanno un overload_resolution_priority pari a 0 e questo può essere modificato applicando OverloadResolutionPriorityAttribute
a un metodo. Aggiorniamo la sezione §12.6.4.1 della specifica C# come segue (modifiche in grassetto):
Dopo aver identificato i membri della funzione candidata e l'elenco di argomenti, la selezione del membro della funzione migliore è la stessa in tutti i casi:
- In primo luogo, il set di membri della funzione candidata viene ridotto a tali membri della funzione applicabili rispetto all'elenco di argomenti specificato (§12.6.4.2). Se questo set ridotto è vuoto, si verifica un errore in fase di compilazione.
- L'insieme ridotto dei membri candidati viene quindi raggruppato in base al tipo dichiarato. All'interno di ogni gruppo:
- I membri funzione candidati vengono ordinati in base alla priorità di risoluzione del sovraccarico . Se il membro è un override, il overload_resolution_priority proviene dalla dichiarazione meno derivata di tale membro.
- Tutti i membri che hanno un overload_resolution_priority inferiore rispetto al valore più alto trovato all'interno del gruppo di tipi dichiarante vengono rimossi.
- I gruppi ridotti vengono quindi ricombinati nel set finale di membri di funzione candidati applicabili.
- Viene quindi individuato il membro di funzione migliore del set di membri della funzione candidata applicabili. Se il set contiene un solo membro della funzione, tale membro della funzione è il membro della funzione migliore. In caso contrario, il membro della funzione migliore è il membro di una funzione migliore di tutti gli altri membri della funzione rispetto all'elenco di argomenti specificato, purché ogni membro della funzione venga confrontato con tutti gli altri membri della funzione che usano le regole in §12.6.4.3. Se non esiste esattamente un membro di funzione migliore di tutti gli altri membri della funzione, la chiamata del membro della funzione è ambigua e si verifica un errore di binding.
Ad esempio, questa funzionalità causerebbe la stampa del frammento di codice seguente "Span", anziché "Array":
using System.Runtime.CompilerServices;
var d = new C1();
int[] arr = [1, 2, 3];
d.M(arr); // Prints "Span"
class C1
{
[OverloadResolutionPriority(1)]
public void M(ReadOnlySpan<int> s) => Console.WriteLine("Span");
// Default overload resolution priority
public void M(int[] a) => Console.WriteLine("Array");
}
L'effetto di questa modifica è che, come la potatura per i tipi più derivati, viene aggiunta una potatura finale per la priorità nella risoluzione degli overload. Poiché questa eliminazione si verifica alla fine del processo di risoluzione dell'overload, ciò implica che un tipo di base non può dare ai suoi membri una priorità superiore rispetto a un tipo derivato. Questo è intenzionale e impedisce che si verifichi una razza di armi in cui un tipo di base può cercare sempre di essere migliore di un tipo derivato. Per esempio:
using System.Runtime.CompilerServices;
var d = new Derived();
d.M([1, 2, 3]); // Prints "Derived", because members from Base are not considered due to finding an applicable member in Derived
class Base
{
[OverloadResolutionPriority(1)]
public void M(ReadOnlySpan<int> s) => Console.WriteLine("Base");
}
class Derived : Base
{
public void M(int[] a) => Console.WriteLine("Derived");
}
I numeri negativi possono essere usati e possono essere usati per contrassegnare un overload specifico come peggio di tutti gli altri overload predefiniti.
Il overload_resolution_priority di un membro deriva dalla dichiarazione meno derivata di tale membro.
overload_resolution_priority non viene ereditato o dedotto da alcun membro di interfaccia che un membro del tipo può implementare e dato un membro Mx
che implementa un membro dell'interfaccia Mi
, non viene generato alcun avviso se Mx
e Mi
hanno overload_resolution_prioritiesdiversi .
NB: lo scopo di questa regola è replicare il comportamento del modificatore
params
.
System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute
Introduciamo il seguente attributo alla libreria di classi di base (BCL):
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class OverloadResolutionPriorityAttribute(int priority) : Attribute
{
public int Priority => priority;
}
Tutti i metodi in C# hanno un overload_resolution_priority predefinito pari a 0, a meno che non siano attribuiti con OverloadResolutionPriorityAttribute
. Se vengono associati a quell'attributo, allora il loro overload_resolution_priority è il valore intero fornito come primo argomento dell'attributo.
È un errore applicare OverloadResolutionPriorityAttribute
alle posizioni seguenti:
- Proprietà non indicizzatore
- Funzioni di accesso a proprietà, indicizzatori o eventi
- Operatori di conversione
- Lambdas
- Funzioni locali
- Finalizzatori
- Costruttori statici
Gli attributi rilevati in queste posizioni nei metadati vengono ignorati da C#.
È un errore applicare OverloadResolutionPriorityAttribute
in una posizione che verrebbe ignorata, ad esempio in un override di un metodo di base, poiché la priorità viene determinata dalla dichiarazione meno derivata di un membro.
NB: Questo comportamento differisce intenzionalmente dal comportamento del modificatore
params
, che consente di ridefinire o aggiungere quando viene ignorato.
Chiamabilità dei membri
Un'importante avvertenza per OverloadResolutionPriorityAttribute
è che può rendere effettivamente non chiamabili dall'origine determinati membri. Per esempio:
using System.Runtime.CompilerServices;
int i = 1;
var c = new C3();
c.M1(i); // Will call C3.M1(long), even though there's an identity conversion for M1(int)
c.M2(i); // Will call C3.M2(int, string), even though C3.M1(int) has less default parameters
class C3
{
public void M1(int i) {}
[OverloadResolutionPriority(1)]
public void M1(long l) {}
[Conditional("DEBUG")]
public void M2(int i) {}
[OverloadResolutionPriority(1), Conditional("DEBUG")]
public void M2(int i, [CallerArgumentExpression(nameof(i))] string s = "") {}
public void M3(string s) {}
[OverloadResolutionPriority(1)]
public void M3(object o) {}
}
Per questi esempi, gli overload di priorità predefiniti diventano effettivamente vestigiali e possono essere chiamati solo dopo aver effettuato alcuni passaggi che richiedono uno sforzo aggiuntivo.
- Conversione del metodo in un delegato e quindi uso di tale delegato.
- Per alcuni scenari di varianza dei tipi di riferimento, ad esempio
M3(object)
con priorità rispetto aM3(string)
, questa strategia avrà esito negativo. - Anche i metodi condizionali, ad esempio
M2
, non sarebbero chiamabili con questa strategia, perché i metodi condizionali non possono essere convertiti in delegati.
- Per alcuni scenari di varianza dei tipi di riferimento, ad esempio
- Uso della funzionalità di runtime
UnsafeAccessor
per chiamarla tramite firma corrispondente. - Uso manuale della riflessione per ottenere un riferimento al metodo e quindi richiamarlo.
- Il codice non ricompilato continuerà a chiamare i metodi precedenti.
- IL scritto a mano può specificare qualsiasi elemento scelto.
Domande aperte
Raggruppamento dei metodi di estensione (risposta)
Come attualmente indicato, i metodi di estensione vengono ordinati in base alla priorità solo all'interno del proprio tipo. Per esempio:
new C2().M([1, 2, 3]); // Will print Ext2 ReadOnlySpan
static class Ext1
{
[OverloadResolutionPriority(1)]
public static void M(this C2 c, Span<int> s) => Console.WriteLine("Ext1 Span");
[OverloadResolutionPriority(0)]
public static void M(this C2 c, ReadOnlySpan<int> s) => Console.WriteLine("Ext1 ReadOnlySpan");
}
static class Ext2
{
[OverloadResolutionPriority(0)]
public static void M(this C2 c, ReadOnlySpan<int> s) => Console.WriteLine("Ext2 ReadOnlySpan");
}
class C2 {}
Quando si esegue la risoluzione dell'overload per i membri dell'estensione, dovremmo evitare di ordinare per tipo dichiarato e considerare invece tutte le estensioni all'interno dello stesso ambito?
Risposta
Ci raggruppamo sempre. L'esempio precedente stampa Ext2 ReadOnlySpan
Ereditarietà degli attributi sulle sostituzioni (risposte)
L'attributo deve essere ereditato? In caso contrario, qual è la priorità del membro sovrascritto?
Se l'attributo viene specificato in un membro virtuale, è necessario eseguire l'override di tale membro per ripetere l'attributo?
Risposta
L'attributo non verrà contrassegnato come ereditato. Esamineremo la dichiarazione meno derivata di un membro per determinare la priorità di risoluzione del sovraccarico.
Errore o avviso dell'applicazione in caso di override (risposta)
class Base
{
[OverloadResolutionPriority(1)] public virtual void M() {}
}
class Derived
{
[OverloadResolutionPriority(2)] public override void M() {} // Warn or error for the useless and ignored attribute?
}
Cosa dovremmo fare su un'applicazione di un OverloadResolutionPriorityAttribute
in un contesto in cui viene ignorato, come un override:
- Non fare nulla, lasciare che venga ignorato in silenzio.
- Genera un avviso che indica che l'attributo verrà ignorato.
- Genera un errore che indica che l'attributo non è consentito.
3 è l'approccio più prudente, se pensiamo che ci sia uno spazio in futuro in cui potrebbe essere necessario consentire a un override di specificare questo attributo.
Risposta
Si procederà con 3 e si bloccherà l'applicazione nei luoghi in cui verrebbe ignorata.
Implementazione implicita dell'interfaccia (risposta)
Qual è il comportamento di un'implementazione implicita dell'interfaccia? Deve essere necessario specificare OverloadResolutionPriority
? Qual è il comportamento del compilatore quando rileva un'implementazione implicita senza priorità? Ciò avverrà quasi certamente, perché una libreria di interfacce può essere aggiornata, ma non un'implementazione. L'arte precedente qui con params
non deve specificare e non trasportare il valore:
using System;
var c = new C();
c.M(1, 2, 3); // error CS1501: No overload for method 'M' takes 3 arguments
((I)c).M(1, 2, 3);
interface I
{
void M(params int[] ints);
}
class C : I
{
public void M(int[] ints) { Console.WriteLine("params"); }
}
Le opzioni disponibili sono:
- Segui
params
.OverloadResolutionPriorityAttribute
non verrà trasferito in modo implicito né sarà necessario specificarlo. - Portare l'attributo in modo implicito.
- Non eseguire il trasporto implicito dell'attributo, richiederne l'impostazione nel sito di chiamata.
- Ciò comporta una domanda aggiuntiva: quale comportamento deve essere quando il compilatore rileva questo scenario con riferimenti compilati?
Risposta
Andremo con 1.
Altri errori dell'applicazione (risposte)
Ci sono altre località come questo e che devono essere confermate. Includono:
- Operatori di conversione: la specifica non indica mai che gli operatori di conversione passano attraverso la risoluzione dell'overload, quindi di conseguenza, l'implementazione impedisce l'applicazione di questi membri. Dovrebbe essere confermato?
- Espressioni lambda: analogamente, le espressioni lambda non sono mai soggette alla risoluzione dell'overload, quindi l'implementazione li blocca. Dovrebbe essere confermato?
- Distruttori: ancora una volta bloccati.
- Costruttori statici: ancora una volta bloccati.
- Funzioni locali: non sono attualmente bloccate perché vengono sottoposte a risoluzione dell'overload, semplicemente non è possibile eseguirne l'overload. Questo è simile al modo in cui non si verifica alcun errore quando l'attributo viene applicato a un membro di un tipo non sottoposto a overload. Questo comportamento deve essere confermato?
Risposta
Tutte le posizioni elencate sopra sono bloccate.
Comportamento langversion (risposta)
L'implementazione attualmente genera solo errori di langversion quando viene applicata OverloadResolutionPriorityAttribute
, non quando influisce effettivamente su qualsiasi elemento. Questa decisione è stata presa perché sono presenti API che verranno aggiunte dal BCL (ora e nel tempo) che inizieranno a usare questo attributo; se l'utente imposta manualmente la versione del linguaggio su C# 12 o versioni precedenti, possono visualizzare questi membri e, a seconda del comportamento langversion, è possibile:
- Se si ignora l'attributo in C# <13, si verifica un errore di ambiguità perché l'API è veramente ambigua senza l'attributo o ;
- Se si commette un errore quando l'attributo influisce sul risultato, si può incorrere in un problema che rende l'API non consumabile. Ciò sarà particolarmente negativo perché
Debug.Assert(bool)
viene de-priorizzato in .NET 9. - Se modifichiamo silenziosamente la risoluzione, possiamo incontrare comportamenti potenzialmente diversi tra diverse versioni del compilatore se una versione riconosce l'attributo e un'altra no.
L'ultimo comportamento è stato scelto, perché comporta la maggiore compatibilità futura, ma il risultato mutevole potrebbe essere sorprendente per alcuni utenti. È necessario confermarlo o scegliere una delle altre opzioni?
Risposta
Si passerà con l'opzione 1, ignorando automaticamente l'attributo nelle versioni precedenti del linguaggio.
Alternative
Una precedente proposta di ha cercato di specificare un approccio BinaryCompatOnlyAttribute
, che era molto pesante nel rendere le cose meno visibili. Tuttavia, questo presenta molti problemi di implementazione difficili che indicano che la proposta è troppo forte per essere utile (impedendo il test delle API precedenti, ad esempio) o in modo debole che abbia perso alcuni degli obiettivi originali (ad esempio essere in grado di avere un'API che altrimenti sarebbe considerata ambigua chiamare una nuova API). Tale versione viene replicata di seguito.
BinaryCompatOnlyAttribute Proposal (obsoleto)
BinaryCompatOnlyAttribute
Progettazione dettagliata
System.BinaryCompatOnlyAttribute
Viene introdotto un nuovo attributo riservato:
namespace System;
// Excludes Assembly, GenericParameter, Module, Parameter, ReturnValue
[AttributeUsage(AttributeTargets.Class
| AttributeTargets.Constructor
| AttributeTargets.Delegate
| AttributeTargets.Enum
| AttributeTargets.Event
| AttributeTargets.Field
| AttributeTargets.Interface
| AttributeTargets.Method
| AttributeTargets.Property
| AttributeTargets.Struct,
AllowMultiple = false,
Inherited = false)]
public class BinaryCompatOnlyAttribute : Attribute {}
Se applicato a un membro di tipo, tale membro viene considerato inaccessibile in ogni posizione dal compilatore, ovvero non contribuisce alla ricerca dei membri, alla risoluzione dell'overload o a qualsiasi altro processo simile.
Domini di accessibilità
Aggiorniamo il §7.5.3 dei domini di accessibilità come segue:
Il dominio di accessibilità di un membro è costituito dalle sezioni (possibilmente disgiunte) del testo del programma in cui è consentito l'accesso al membro. Ai fini della definizione del dominio di accessibilità di un membro, si dice che un membro sia di primo livello se non viene dichiarato all'interno di un tipo e un membro viene detto annidato se viene dichiarato all'interno di un altro tipo. Inoltre, il testo del programma di un programma è definito come tutto il testo contenuto in tutte le unità di compilazione del programma e il testo del programma di un tipo è definito come tutto il testo contenuto nella type_declarationdi tale tipo (inclusi, possibilmente, i tipi annidati all'interno del tipo).
Il dominio di accessibilità di un tipo predefinito ( ad esempio
object
,int
odouble
) è illimitato.Il dominio di accessibilità di un tipo non associato di primo livello
T
(§8.4.4) dichiarato in un programmaP
è definito come segue:
- Se
T
è contrassegnato conBinaryCompatOnlyAttribute
, il dominio di accessibilità diT
è completamente inaccessibile al testo del programma diP
e a qualsiasi programma che fa riferimento aP
.- Se l'accessibilità dichiarata di
T
è pubblica, il dominio di accessibilità diT
è il testo del programma diP
e qualsiasi programma che fa riferimento aP
.- Se l'accessibilità dichiarata di
T
è interna, il dominio di accessibilità diT
è il testo del programma diP
.Nota: da queste definizioni, si segue che il dominio di accessibilità di un tipo non associato di primo livello è sempre almeno il testo programma del programma in cui è dichiarato tale tipo. nota di chiusura
Il dominio di accessibilità per un tipo costruito
T<A₁, ..., Aₑ>
è l'intersezione del dominio di accessibilità del tipo generico non associatoT
e dei domini di accessibilità degli argomenti di tipoA₁, ..., Aₑ
.Il dominio di accessibilità di un membro annidato
M
dichiarato in un tipoT
all'interno di un programmaP
, è definito come segue (notando cheM
stesso potrebbe essere un tipo):
- Se
M
è contrassegnato conBinaryCompatOnlyAttribute
, il dominio di accessibilità diM
è completamente inaccessibile al testo del programma diP
e a qualsiasi programma che fa riferimento aP
.- Se l'accessibilità dichiarata di
M
èpublic
, il dominio di accessibilità diM
è il dominio di accessibilità diT
.- Se l'accessibilità dichiarata di
M
èprotected internal
, lasciare cheD
sia l'unione del codice del programma diP
e del codice del programma di qualsiasi tipo derivato daT
, dichiarato al di fuori diP
. Il dominio di accessibilità diM
è l'intersezione del dominio di accessibilità diT
conD
.- Se l'accessibilità dichiarata di
M
èprivate protected
, lasciareD
essere l'intersezione del testo del programma diP
e il testo del programma diT
e qualsiasi tipo derivato daT
. Il dominio di accessibilità diM
è l'intersezione del dominio di accessibilità diT
conD
.- Se l'accessibilità dichiarata di
M
èprotected
, lasciareD
essere l'unione del testo del programma diT
e il testo del programma di qualsiasi tipo derivato daT
. Il dominio di accessibilità diM
è l'intersezione del dominio di accessibilità diT
conD
.- Se l'accessibilità dichiarata di
M
èinternal
, il dominio di accessibilità diM
è l'intersezione del dominio di accessibilità diT
con il testo del programma diP
.- Se l'accessibilità dichiarata di
M
èprivate
, il dominio di accessibilità diM
è il testo del programma diT
.
L'obiettivo di queste aggiunte è di renderlo in modo che i membri contrassegnati con BinaryCompatOnlyAttribute
siano completamente inaccessibili a qualsiasi posizione, non partecipano alla ricerca dei membri e non possono influire sul resto del programma. Di conseguenza, ciò significa che non possono implementare membri di interfaccia, non possono chiamarsi tra loro e non possono essere sottoposti a override (metodi virtuali), nascosti o implementati (membri dell'interfaccia). Se questo è troppo rigoroso è l'oggetto di diverse domande aperte di seguito.
Domande non risolte
Metodi virtuali e sovrascrittura
Cosa fare quando un metodo virtuale viene contrassegnato come BinaryCompatOnly
? Le override in una classe derivata potrebbero non trovarsi nemmeno nell'assembly corrente e potrebbe essere il caso in cui l'utente stia cercando di introdurre una nuova versione di un metodo che, ad esempio, differisce solo per il tipo di ritorno, un aspetto su cui C# normalmente non consente l'overload. Cosa accade a qualsiasi override del metodo precedente nella ricompilazione? È consentito eseguire l'override del membro BinaryCompatOnly
se sono contrassegnati anche come BinaryCompatOnly
?
Usare all'interno della stessa DLL
Questa proposta afferma che i membri BinaryCompatOnly
non sono visibili in nessun punto, nemmeno nell'assemblaggio in fase di compilazione. Risulta troppo restrittivo, o i membri di BinaryCompatAttribute
devono forse concatenarsi tra loro?
Implementazione implicita dei membri dell'interfaccia
I membri BinaryCompatOnly
devono essere in grado di implementare i membri dell'interfaccia? Oppure dovrebbe essere loro impedito di farlo. Ciò richiederebbe che, quando un utente vuole trasformare un'implementazione implicita dell'interfaccia in BinaryCompatOnly
, dovrebbero inoltre fornire un'implementazione esplicita dell'interfaccia, probabilmente clonando lo stesso corpo dell'implementazione del membro BinaryCompatOnly
poiché l'implementazione esplicita dell'interfaccia non sarebbe più in grado di accedere al membro originale.
Implementazione di membri dell'interfaccia contrassegnati BinaryCompatOnly
Cosa fare quando un membro dell'interfaccia è stato contrassegnato come BinaryCompatOnly
? Il tipo deve comunque fornire un'implementazione per tale membro; può essere che è necessario semplicemente dire che i membri dell'interfaccia non possono essere contrassegnati come BinaryCompatOnly
.
C# feature specifications