Dela via


Prioritet för överbelastningslösning

Not

Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.

Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader återfinns i de relevanta anteckningarna från Language Design Meeting (LDM) .

Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.

Champion-problem: https://github.com/dotnet/csharplang/issues/7706

Sammanfattning

Vi introducerar ett nytt attribut, System.Runtime.CompilerServices.OverloadResolutionPriority, som kan användas av API-författare för att justera den relativa prioriteten för överbelastningar inom en enda typ som ett sätt att styra API-konsumenter att använda specifika API:er, även om dessa API:er normalt betraktas som tvetydiga eller annars inte väljs av C#:s regler för överbelastningsmatchning.

Motivation

API-författare stöter ofta på ett problem med vad de ska göra med en medlem när den har föråldrats. För bakåtkompatibilitetsändamål kommer många att behålla den befintliga komponenten med ObsoleteAttribute inställd att visa ett fel i all evighet, för att undvika att störa användare som uppgraderar binärer under körning. Detta drabbar särskilt plugin-system, där författaren till ett plugin-program inte styr miljön där plugin-programmet körs. Skaparen av miljön kanske vill behålla en äldre metod närvarande, men blockera åtkomst till den för nyutvecklad kod. Men ObsoleteAttribute i sig räcker inte. Typen eller medlemmen är fortfarande synlig i överbelastningsmatchning och kan orsaka oönskade överbelastningsmatchningsfel när det finns ett helt bra alternativ, men det alternativet är antingen tvetydigt med den föråldrade medlemmen eller förekomsten av den föråldrade medlemmen gör att överbelastningslösningen slutar tidigt utan att någonsin överväga den bra medlemmen. För det här ändamålet vill vi ha ett sätt för API-författare att vägleda överbelastningsmatchning för att lösa tvetydigheten, så att de kan utveckla sina API-ytområden och styra användare mot högpresterande API:er utan att behöva äventyra användarupplevelsen.

BCL-teamet (Base Class Libraries) har flera exempel på var detta kan vara användbart. Några (hypotetiska) exempel är:

  • Skapa en överbelastning av Debug.Assert som använder CallerArgumentExpression för att få uttrycket som hävdas, så att det kan inkluderas i meddelandet och göra det mer föredraget framför den befintliga överbelastningen.
  • Gör string.IndexOf(string, StringComparison = Ordinal) att föredra framför string.IndexOf(string). Detta skulle behöva diskuteras som en potentiell icke-bakåtkompatibel ändring, men det finns de som anser att det är det bättre standardvalet och mer sannolikt att vara vad användaren menade.
  • En kombination av det här förslaget och CallerAssemblyAttribute skulle göra det möjligt för metoder som har en implicit anroparidentitet att undvika dyra stackgenomgångar. Assembly.Load(AssemblyName) gör detta i dag, och det kan vara mycket effektivare.
  • Microsoft.Extensions.Primitives.StringValues exponerar en implicit konvertering till både string och string[]. Det innebär att den är tvetydig när den skickas till en metod med både params string[] och params ReadOnlySpan<string> överlagringar. Det här attributet kan användas för att prioritera en av överlagringarna för att förhindra tvetydigheten.

Detaljerad design

Prioritet för överladdningslösning

Vi definierar ett nytt begrepp, overload_resolution_priority, som används under processen för att lösa en metodgrupp. overload_resolution_priority är ett 32-bitars heltalsvärde. Alla metoder har en overload_resolution_priority på 0 som standard, och detta kan ändras genom att tillämpa OverloadResolutionPriorityAttribute på en metod. Vi uppdaterar avsnitt §12.6.4.1 av C#-specifikationen enligt följande (ändring i fetstil):

När kandidatfunktionens medlemmar och argumentlistan har identifierats är valet av den bästa funktionsmedlemmen detsamma i alla fall:

  • För det första reduceras uppsättningen kandidatfunktionsmedlemmar till de funktionsmedlemmar som gäller för den angivna argumentlistan (§12.6.4.2). Om den här reducerade uppsättningen är tom uppstår ett kompileringsfel.
  • Sedan grupperas den minskade uppsättningen kandidatmedlemmar genom att deklarera typ. Inom varje grupp:
    • Kandidatfunktionsmedlemmar sorteras efter overload_resolution_priority. Om medlemmen är ett överskridande kommer overload_resolution_priority från den minst härledda deklarationen av den medlemmen.
    • Alla medlemmar som har en lägre overload_resolution_priority än den högsta som finns inom deklarationstypgruppen avlägsnas.
  • De reducerade grupperna kombineras sedan till den sista uppsättningen av tillämpliga kandidatfunktionsmedlemmar.
  • Sedan identifieras den bästa funktionsmedlemmen från de tillämpliga kandidatfunktionsmedlemmarnas mängd. Om uppsättningen endast innehåller en funktionsmedlem är funktionsmedlemmen den bästa funktionsmedlemmen. Annars är den bästa funktionsmedlemmen den funktionsmedlem som är bättre än alla andra funktionsmedlemmar med avseende på den angivna argumentlistan, förutsatt att varje funktionsmedlem jämförs med alla andra funktionsmedlemmar som använder reglerna i §12.6.4.3. Om det inte finns exakt en funktionsmedlem som är bättre än alla andra funktionsmedlemmar är funktionsmedlemsanropet tvetydigt och ett bindningstidsfel inträffar.

Denna funktion skulle till exempel göra att följande kodfragment skriver ut "Span" i stället för "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");
}

Effekten av den här ändringen är att vi, precis som beskärning för de mest härledda typerna, lägger till en slutlig beskärning för prioritering av överbelastningslösning. Eftersom den här beskärningen sker i slutet av processen för överbelastningslösning innebär det att en bastyp inte kan göra sina medlemmar högre prioritet än någon härledd typ. Detta är avsiktligt och förhindrar att en kapprustning inträffar där en bastyp alltid kan försöka vara bättre än en härledd typ. Till exempel:

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

Negativa tal tillåts användas och kan användas för att markera en specifik överlagring som sämre än alla andra standardöverbelastningar.

Den overload_resolution_priority för en medlem kommer från den minst härledda deklarationen av medlemmen. overload_resolution_priority är inte ärvd eller härledd från några gränssnittsmedlemmar som en typmedlem kan implementera, och givet en medlem Mx som implementerar en gränssnittsmedlem Miutfärdas ingen varning om Mx och Mi har olika overload_resolution_priorities.

Obs! Avsikten med den här regeln är att replikera beteendet för den params modifieraren.

System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute

Vi introducerar följande attribut för 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;
}

Alla metoder i C# har en standard överbelastningsupplösningsprioritet på 0, såvida de inte tilldelas OverloadResolutionPriorityAttribute. Om de tilldelas det attributet är deras overload_resolution_priority det heltalsvärde som anges till det första argumentet i attributet.

Det är ett fel att tillämpa OverloadResolutionPriorityAttribute på följande platser:

  • Egenskaper som inte är indexerare
  • Egenskaps-, indexerare- eller händelseåtkomster
  • Konverteringsoperatorer
  • Lambda-funktioner
  • Lokala funktioner
  • Finalisatorer
  • Statiska konstruktorer

Attribut som påträffas på dessa platser i metadata ignoreras av C#.

Det är ett fel att tillämpa OverloadResolutionPriorityAttribute på en plats som ignoreras, till exempel vid en åsidosättning av en basmetod, eftersom prioriteten läses från den minst härledda deklarationen för en medlem.

Obs! Detta skiljer sig avsiktligt från beteendet hos params-modifieraren, vilket gör det möjligt att specifera om eller lägga till när det ignoreras.

Kontaktbarhet för medlemmar

En viktig varning för OverloadResolutionPriorityAttribute är att det kan göra vissa medlemmar effektivt okallbara från källan. Till exempel:

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

I de här exemplen blir standardprioritetsöverbelastningarna i praktiken vestigala och kan bara anropas genom några få steg som kräver lite extra arbete:

  • Konvertera metoden till ett ombud och sedan använda det ombudet.
    • För vissa referenstypavvikelsescenarier, till exempel M3(object) som prioriteras framför M3(string), misslyckas den här strategin.
    • Villkorsstyrda metoder, som M2, kan inte heller anropas med den här strategin, eftersom villkorsstyrda metoder inte kan konverteras till delegater.
  • Använder körningsfunktionen UnsafeAccessor för att anropa den med hjälp av en matchande signatur.
  • Använd reflektion manuellt för att hämta en referens till metoden och sedan anropa den.
  • Kod som inte är omkompilerad fortsätter att anropa gamla metoder.
  • Handskriven IL kan ange vad den vill.

Öppna frågor

Gruppering av tilläggsmetod (besvarad)

Som det för närvarande är formulerat sorteras tilläggsmetoder i prioritesordning endast inom sin egen typ. Till exempel:

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

Ska vi inte sortera efter den deklarerade typen när vi bedömer överbelastning för utökningar, utan istället överväga alla medlemmar inom samma omfång?

Svar

Vi grupperar alltid. Exemplet ovan skriver ut Ext2 ReadOnlySpan

Attributarv vid åsidosättning (besvarad)

Ska attributet ärvas? Om inte, vad är prioriteten för den överordnade medlemmen?
Om attributet anges för en virtuell medlem, bör en åsidosättning av den medlemmen krävas för att upprepa attributet?

Svar

Attributet markeras inte som ärvt. Vi kommer att titta på den minst härledda deklarationen av en medlem för att avgöra dess prioritet vid överbelastningslösning.

Programfel eller varning vid åsidosättning (besvaras)

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

Vilket bör vi göra när det gäller tillämpningen av en OverloadResolutionPriorityAttribute i en kontext där den ignoreras, till exempel en åsidosättning:

  1. Gör ingenting, låt det tyst ignoreras.
  2. Utfärda en varning om att attributet ignoreras.
  3. Utfärda ett fel om att attributet inte är tillåtet.

3 är den mest försiktiga metoden, om vi tror att det kan finnas ett utrymme i framtiden där vi kanske vill tillåta en åsidosättning för att ange det här attributet.

Svar

Vi går med 3 och blockerar program på platser där det ignoreras.

Implicit gränssnittsimplementering (besvarat)

Vad ska beteendet för en implicit gränssnittsimplementering vara? Måste du ange OverloadResolutionPriority? Vad ska kompilatorns beteende vara när den stöter på en implicit implementering utan prioritet? Detta kommer nästan säkert att inträffa, eftersom ett gränssnittsbibliotek kan uppdateras, men inte en implementering. Tidigare teknik här med params är att inte specificera och inte bära över värdet:

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

Våra alternativ är:

  1. Följ params. OverloadResolutionPriorityAttribute överförs inte implicit eller måste anges.
  2. Överför attributet implicit.
  3. Överför inte attributet implicit, kräv att det anges på anropsplatsen.
    1. Detta ger en extra fråga: vad ska beteendet vara när kompilatorn stöter på det här scenariot med kompilerade referenser?

Svar

Vi går med 1.

Ytterligare programfel (besvarade)

Det finns några fler platser som måste bekräftas, som den här. De omfattar:

  • Konverteringsoperatorer – specifikationen säger aldrig att konverteringsoperatorer går igenom överbelastningslösning, så implementeringen blockerar användningen av dessa medlemmar. Ska det bekräftas?
  • Lambdas - På samma sätt är lambdas aldrig föremål för överbelastningsupplösning, så implementeringen blockerar dem. Ska det bekräftas?
  • Destructors - igen, för närvarande blockerad.
  • Statiska konstruktorer – återigen blockerade.
  • Lokala funktioner – Dessa blockeras inte för närvarande eftersom de genomgår överbelastningsupplösning kan du bara inte överbelasta dem. Detta liknar hur vi inte får ett fel när attributet tillämpas på en medlem av en typ som inte är överlagrad. Bör det här beteendet bekräftas?

Svar

Alla platser som anges ovan är blockerade.

Langversionsbeteende (besvarat)

Implementeringen utfärdar för närvarande endast langversionsfel när OverloadResolutionPriorityAttribute tillämpas, inte när det faktiskt påverkar något. Det här beslutet fattades eftersom det finns API:er som BCL lägger till (både nu och över tid) som börjar använda det här attributet. Om användaren manuellt anger sin språkversion tillbaka till C# 12 eller tidigare kan de se dessa medlemmar och, beroende på vårt språkversionsbeteende, antingen:

  • Om vi ignorerar attributet i C# <13 kan du stöta på ett tvetydighetsfel eftersom API:et verkligen är tvetydigt utan attributet, eller;
  • Om vi gör ett fel när attributet påverkar resultatet, kommer vi att stöta på ett fel som gör att API:et inte kan användas. Detta kommer att vara särskilt dåligt eftersom Debug.Assert(bool) avprioriteras i .NET 9, eller;
  • Om vi tyst ändrar upplösningen kan det uppstå ett potentiellt annorlunda beteende mellan olika kompilatorversioner om man förstår attributet och en annan inte gör det.

Det senaste beteendet valdes eftersom det resulterar i den mest framåtriktade kompatibiliteten, men det ändrade resultatet kan vara överraskande för vissa användare. Ska vi bekräfta detta, eller ska vi välja något av de andra alternativen?

Svar

Vi går med alternativ 1 och ignorerar attributet tyst i tidigare språkversioner.

Alternativ

Ett tidigare förslag försökte ange en BinaryCompatOnlyAttribute strategi, som var överdrivet kraftfull när det gällde att göra saker mindre synliga. Det har dock många problem med hård implementering som antingen innebär att förslaget är för starkt för att vara användbart (till exempel att förhindra testning av gamla API:er) eller så svagt att det missade några av de ursprungliga målen (till exempel att kunna ha ett API som annars skulle betraktas som tvetydigt anropa ett nytt API). Den versionen replikeras nedan.

BinaryCompatOnlyAttribute-förslag (föråldrad)

BinaryCompatOnlyAttribute

Detaljerad design

System.BinaryCompatOnlyAttribute

Vi introducerar ett nytt reserverat attribut:

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

När den tillämpas på en typmedlem behandlas den medlemmen som otillgänglig på alla platser av kompilatorn, vilket innebär att den inte bidrar till medlemssökning, överbelastningsmatchning eller någon annan liknande process.

Hjälpmedelsdomäner

Vi uppdaterar §7.5.3 Tillgänglighetsdomäner enligt följande :

Den tillgänglighetsdomänen av en medlem består av de (eventuellt åtskilda) avsnitten i programtexten där åtkomst till medlemmen tillåts. För att definiera tillgänglighetsdomänen för en medlem, anses en medlem vara toppnivå om den inte deklareras inom en typ, och en medlem anses vara kapslad om den deklareras inom en annan typ. Dessutom definieras den programtexten av ett program som all text som finns i alla kompileringsenheter i programmet, och programtexten för en typ definieras som all text som finns i type_declarationav den typen (inklusive eventuellt typer som är kapslade inom typen).

Tillgänglighetsdomänen för en fördefinierad typ (till exempel object, inteller double) är obegränsad.

Tillgänglighetsdomänen för en obundna toppnivåtyp T (§8.4.4) som deklareras i ett program P definieras på följande sätt:

  • Om T har markerats med BinaryCompatOnlyAttributeär tillgänglighetsdomänen för T helt otillgänglig för programtexten i P och alla program som refererar till P.
  • Om den deklarerade tillgängligheten för T är offentlig är tillgänglighetsdomänen för T programtexten för P och alla program som refererar till P.
  • Om den deklarerade tillgängligheten för T är intern är tillgänglighetsdomänen för T programtexten i P.

Obs: Av dessa definitioner följer att tillgänglighetsdomänen för en obunden toppnivå alltid är minst programtexten i det program där den typen deklareras. slutkommentar

Tillgänglighetsdomänen för en konstruerad typ T<A₁, ..., Aₑ> är skärningspunkten för tillgänglighetsdomänen för den obundna allmänna typen T och tillgänglighetsdomänerna för typargumenten A₁, ..., Aₑ.

Tillgänglighetsdomänen för en kapslad medlem M deklareras i en typ T i ett program P, definieras enligt följande (notera att M själv kan vara en typ):

  • Om M har markerats med BinaryCompatOnlyAttributeär tillgänglighetsdomänen för M helt otillgänglig för programtexten i P och alla program som refererar till P.
  • Om den deklarerade tillgängligheten för M är publicär tillgänglighetsdomänen för M tillgänglighetsdomänen för T.
  • Om den deklarerade tillgängligheten för M är protected internalska D vara en union av programtexten för P och programtexten av alla typer som härletts från T, som deklareras utanför P. Tillgänglighetsdomänen för M är skärningspunkten för tillgänglighetsdomänen för T med D.
  • Om den deklarerade tillgängligheten för M är private protectedska D vara skärningspunkten för programtexten i P och programtexten för T och alla typer som härletts från T. Tillgänglighetsdomänen för M är skärningspunkten för tillgänglighetsdomänen för T med D.
  • Om den deklarerade tillgängligheten för M är protectedska D vara en union av programtexten för Toch programtexten av alla typer som härletts från T. Tillgänglighetsdomänen för M är skärningspunkten för tillgänglighetsdomänen för T med D.
  • Om den deklarerade tillgängligheten för M är internalär tillgänglighetsdomänen för M skärningspunkten för tillgänglighetsdomänen för T med programtexten i P.
  • Om den deklarerade tillgängligheten för M är privateär tillgänglighetsdomänen för M programtexten i T.

Målet med dessa tillägg är att göra det så att medlemmar som markerats med BinaryCompatOnlyAttribute är helt otillgängliga för någon plats, de kommer inte att delta i medlemssökning och kan inte påverka resten av programmet. Detta innebär att de inte kan implementera gränssnittsmedlemmar, de kan inte anropa varandra och de kan inte åsidosättas (virtuella metoder), döljas eller implementeras (gränssnittsmedlemmar). Om detta är för strikt är föremål för flera öppna frågor nedan.

Olösta frågor

Virtuella metoder och överskrivning

Vad gör vi när en virtuell metod markeras som BinaryCompatOnly? Åsidosättningar i en härledd klass kanske inte ens finns i den aktuella sammansättningen, och det kan vara så att användaren vill introducera en ny version av en metod som till exempel bara skiljer sig efter returtyp, något som C# normalt inte tillåter överlagring på. Vad händer med eventuella åsidosättningar av den tidigare metoden vid omkompilering? Har de tillåtelse att åsidosätta BinaryCompatOnly-medlemmen om de också är markerade som BinaryCompatOnly?

Använd inom samma DLL

I det här förslaget anges att BinaryCompatOnly medlemmar inte syns någonstans, inte ens i den sammansättning som för närvarande sammanställs. Är det för strikt, eller behöver BinaryCompatAttribute medlemmar kanske länka samman med varandra?

Implementera gränssnittsmedlemmar implicit

Ska BinaryCompatOnly medlemmar kunna implementera gränssnittsmedlemmar? Eller bör de hindras från att göra det. Detta skulle kräva att, när en användare vill omvandla en implicit gränssnittsimplementation till BinaryCompatOnly, måste de dessutom tillhandahålla en explicit gränssnittsimplementation, vilket sannolikt innebär att klona samma implementering som BinaryCompatOnly-medlemmen eftersom den explicita gränssnittsimplementeringen inte längre skulle kunna se den ursprungliga medlemmen.

Implementera gränssnittsmedlemmar som är markerade med BinaryCompatOnly

Vad gör vi när en gränssnittsmedlem har markerats som BinaryCompatOnly? Typen måste fortfarande tillhandahålla en implementering för medlemmen. Det kan vara så att vi helt enkelt måste säga att gränssnittsmedlemmar inte kan markeras som BinaryCompatOnly.