Condividi tramite


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 usa CallerArgumentExpression 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 a string.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 in string che in string[]. Ciò significa che è ambiguo quando viene passato a un metodo con overload sia di params string[] che di params 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 a M3(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.
  • 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:

  1. Non fare nulla, lasciare che venga ignorato in silenzio.
  2. Genera un avviso che indica che l'attributo verrà ignorato.
  3. 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:

  1. Segui params. OverloadResolutionPriorityAttribute non verrà trasferito in modo implicito né sarà necessario specificarlo.
  2. Portare l'attributo in modo implicito.
  3. Non eseguire il trasporto implicito dell'attributo, richiederne l'impostazione nel sito di chiamata.
    1. 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, into double) è illimitato.

Il dominio di accessibilità di un tipo non associato di primo livello T (§8.4.4) dichiarato in un programma P è definito come segue:

  • Se T è contrassegnato con BinaryCompatOnlyAttribute, il dominio di accessibilità di T è completamente inaccessibile al testo del programma di P e a qualsiasi programma che fa riferimento a P.
  • Se l'accessibilità dichiarata di T è pubblica, il dominio di accessibilità di T è il testo del programma di P e qualsiasi programma che fa riferimento a P.
  • Se l'accessibilità dichiarata di T è interna, il dominio di accessibilità di T è il testo del programma di P.

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 associato T e dei domini di accessibilità degli argomenti di tipo A₁, ..., Aₑ.

Il dominio di accessibilità di un membro annidato M dichiarato in un tipo T all'interno di un programma P, è definito come segue (notando che M stesso potrebbe essere un tipo):

  • Se M è contrassegnato con BinaryCompatOnlyAttribute, il dominio di accessibilità di M è completamente inaccessibile al testo del programma di P e a qualsiasi programma che fa riferimento a P.
  • Se l'accessibilità dichiarata di M è public, il dominio di accessibilità di M è il dominio di accessibilità di T.
  • Se l'accessibilità dichiarata di M è protected internal, lasciare che D sia l'unione del codice del programma di P e del codice del programma di qualsiasi tipo derivato da T, dichiarato al di fuori di P. Il dominio di accessibilità di M è l'intersezione del dominio di accessibilità di T con D.
  • Se l'accessibilità dichiarata di M è private protected, lasciare D essere l'intersezione del testo del programma di P e il testo del programma di T e qualsiasi tipo derivato da T. Il dominio di accessibilità di M è l'intersezione del dominio di accessibilità di T con D.
  • Se l'accessibilità dichiarata di M è protected, lasciare D essere l'unione del testo del programma di Te il testo del programma di qualsiasi tipo derivato da T. Il dominio di accessibilità di M è l'intersezione del dominio di accessibilità di T con D.
  • Se l'accessibilità dichiarata di M è internal, il dominio di accessibilità di M è l'intersezione del dominio di accessibilità di T con il testo del programma di P.
  • Se l'accessibilità dichiarata di M è private, il dominio di accessibilità di M è il testo del programma di T.

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.