Priorita rozlišení přetížení
Poznámka
Tento článek je specifikace funkce. Specifikace slouží jako návrhový dokument pro funkci. Zahrnuje navrhované změny specifikace spolu s informacemi potřebnými při návrhu a vývoji funkce. Tyto články se publikují, dokud nebudou navrhované změny specifikace finalizovány a začleněny do aktuální specifikace ECMA.
Mezi specifikací funkce a dokončenou implementací může docházet k nějakým nesrovnalostem. Tyto rozdíly jsou zachyceny v příslušných poznámkách schůzce návrhu jazyka (LDM).
Další informace o procesu přijetí specifikací funkcí do jazyka C# najdete v článku o specifikacích .
Problém šampiona: https://github.com/dotnet/csharplang/issues/7706
Shrnutí
Zavádíme nový atribut, System.Runtime.CompilerServices.OverloadResolutionPriority
, který mohou autoři rozhraní API použít k úpravě relativní priority přetížení v rámci jednoho typu jako prostředek řízení uživatelů rozhraní API pro použití konkrétních rozhraní API, i když by tato rozhraní API byla obvykle považována za nejednoznačné nebo jinak nebyla zvolena pravidly řešení přetížení jazyka C#.
Motivace
Autoři rozhraní API často narazí na problém s tím, co dělat s členem poté, co je zastaralý. Pro účely zpětné kompatibility mnozí správci ponechají existujícího člena s ObsoleteAttribute
nastaveným na chybu na dobu neurčitou, aby se předešlo problémům u uživatelů, kteří upgradují binární soubory za běhu. To se týká zejména systémů plug-in, kde autor modulu plug-in neřídí prostředí, ve kterém modul plug-in běží. Tvůrce prostředí může chtít zachovat starší metodu, ale blokovat přístup k němu pro jakýkoli nově vyvinutý kód. Ale ObsoleteAttribute
sama o sobě nestačí. Typ nebo člen je stále viditelný v rozlišení přetížení a může způsobit nežádoucí chyby rozlišení přetížení, pokud existuje dokonale dobrá alternativa, ale tato alternativa je buď nejednoznačná se zastaralým členem, nebo přítomnost zastaralého členu způsobí, že rozlišení přetížení skončí předčasně, aniž by kdy zvažovalo dobrý člen. Pro tento účel chceme autorům rozhraní API zajistit způsob, jak nasměrovat řešení přetížení při řešení nejednoznačnosti, aby mohli rozvíjet oblasti svých rozhraní API a směrovat uživatele k výkonným API, aniž by museli ohrozit uživatelskou zkušenost.
Tým BCL (Base Class Library) obsahuje několik příkladů, kde to může být užitečné. Mezi (hypotetické) příklady patří:
- Vytvoření přetížení
Debug.Assert
, které používáCallerArgumentExpression
k získání výrazu, který je uplatňován, aby mohl být zahrnut do zprávy, a aby bylo upřednostněno před existujícím přetížením. - Upřednostnění
string.IndexOf(string, StringComparison = Ordinal)
předstring.IndexOf(string)
. To by se muselo probrat jako potenciální zásadní změna, ale existuje myšlenka, že je to lepší výchozí hodnota a s větší pravděpodobností bude to, co uživatel zamýšlel. - Kombinace tohoto návrhu a
CallerAssemblyAttribute
by umožnila metodám s implicitní identitou volajícího vyhnout se nákladným procházkám zásobníku.Assembly.Load(AssemblyName)
to dnes dělá a mohlo by to být mnohem efektivnější. -
Microsoft.Extensions.Primitives.StringValues
zpřístupňuje implicitní převod nastring
istring[]
. To znamená, že je nejednoznačný při předání metodě s přetíženímparams string[]
iparams ReadOnlySpan<string>
. Tento atribut lze použít k určení priority jednoho z přetížení, aby se zabránilo nejednoznačnosti.
Podrobný návrh
Priorita rozlišení přetížení
Definujeme nový koncept, overload_resolution_priority, který se používá během procesu řešení skupiny metod.
overload_resolution_priority je 32bitová celočíselná hodnota. Všechny metody mají ve výchozím nastavení overload_resolution_priority nastaveno na 0, a to lze změnit aplikací OverloadResolutionPriorityAttribute
na metodu. Oddíl §12.6.4.1 specifikace jazyka C# aktualizujeme následujícím způsobem (změna tučným písmem):
Po zjištění členů kandidátské funkce a seznamu argumentů je výběr nejlepšího člena funkce ve všech případech stejný:
- Zaprvé je sada členů kandidátské funkce omezena na členy funkce, které se vztahují k danému seznamu argumentů (§12.6.4.2). Pokud je tato omezená sada prázdná, dojde k chybě v době kompilace.
- Poté je zúžená skupina kandidátských členů seskupena podle typu deklarace. V rámci každé skupiny:
- Kandidátské členy funkcí jsou uspořádány podle priority přetížení. Pokud je člen přepsán, overload_resolution_priority pochází z nejméně odvozené deklarace tohoto člena.
- Odeberou se všichni členové, kteří mají nižší overload_resolution_priority než nejvyšší nalezený v rámci skupiny deklarujícího typu.
- Zmenšené skupiny se pak znovu zkombinují do konečné sady použitelných členů kandidátské funkce.
- Pak se nachází nejlepší člen funkce ze sady použitelných členů kandidátské funkce. Pokud sada obsahuje pouze jeden člen funkce, je tento člen funkce nejlepším členem funkce. V opačném případě je nejlepším členem funkce jeden člen funkce, který je lepší než všechny ostatní členy funkce s ohledem na daný seznam argumentů za předpokladu, že každý člen funkce je porovnán se všemi ostatními členy funkce pomocí pravidel v §12.6.4.3. Pokud neexistuje přesně jedna funkce, která je lepší než všechny ostatní funkce, pak je vyvolání funkce nejednoznačné a dojde k chybě při vazbě.
Tato funkce by například způsobila, že následující fragment kódu vytiskne "Span", a ne "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");
}
Výsledkem této změny je, že podobně jako při prořezávání u většiny odvozených typů přidáváme závěrečné prořezávání pro prioritu řešení přetížení. Jelikož k tomuto ořezání dochází až na samém konci procesu řešení přetížení, znamená to, že základní typ nemůže nastavit své členy na vyšší prioritu než kterýkoli odvozený typ. Jedná se o úmyslné a zabraňuje vzniku závodu zbraní, kdy se základní typ může snažit být vždy lepší než odvozený typ. Například:
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");
}
Je povoleno používat záporná čísla a lze je použít k označení konkrétního přetížení jako horšího než u všech ostatních výchozích přetížení.
overload_resolution_priority člena pochází z nejméně odvozené deklarace tohoto člena.
overload_resolution_priority není zděděno ani odvozeno od žádných členů rozhraní, které může člen typu implementovat, a má na mysli člena Mx
, který implementuje člena rozhraní Mi
, nevydá se žádné upozornění, pokud Mx
a Mi
mají odlišné overload_resolution_priorities.
NB: Záměrem tohoto pravidla je replikovat chování modifikátoru
params
.
System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute
Do seznamu BCL zavádíme následující atribut:
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;
}
Všechny metody v jazyce C# mají výchozí overload_resolution_priority 0, pokud nejsou přiřazeny OverloadResolutionPriorityAttribute
. Pokud jsou atributem přiřazeny, jejich overload_resolution_priority je celočíselná hodnota zadaná prvnímu argumentu atributu.
Použití OverloadResolutionPriorityAttribute
na následujících místech je chybné:
- Neindexační vlastnosti
- Vlastnosti, indexátory nebo přístupové metody událostí
- Operátory převodu
- Lambda
- Místní funkce
- Finalizéry
- Statické konstruktory
C# ignoruje atributy, které jsou na těchto místech v metadatech.
Je chybou aplikovat OverloadResolutionPriorityAttribute
na místě, kde by byl ignorován, například při přetížení základní metody, protože priorita se určuje z nejméně odvozené deklarace člena.
NB: Úmyslně se liší od chování modifikátoru
params
, který umožňuje při ignorování znovu určit nebo přidat.
Volatelnost členů
Důležitým upozorněním pro OverloadResolutionPriorityAttribute
je, že může určité členy efektivně učinit nevolatelné z původního zdroje. Například:
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) {}
}
V těchto příkladech se výchozí přetížení priority efektivně stávají vestigiálními a lze je vyvolat pouze několika kroky, které vyžadují určité dodatečné úsilí:
- Převod metody na delegáta a následné použití daného delegáta.
- V některých scénářích rozptylu typů odkazů, jako je například
M3(object)
, které jsou upřednostněny předM3(string)
, tato strategie selže. - Podmíněné metody, jako je například
M2
, by také nebyly při této strategii volatelné, protože podmíněné metody nelze převést na delegáty.
- V některých scénářích rozptylu typů odkazů, jako je například
- Pomocí funkce modulu runtime
UnsafeAccessor
ji můžete volat prostřednictvím kompatibilní signatury. - Použití reflexe ručně ke získání odkazu na metodu a jejímu následnému vyvolání.
- Kód, který není rekompilován, bude nadále volat staré metody.
- Ručně psaný kód IL může specifikovat, co si vybere.
Otevřené otázky
Seskupování metod rozšíření (zodpovězeno)
Jak je uvedeno v současné době, metody rozšíření jsou seřazeny podle priority pouze v rámci jejich vlastního typu. Například:
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 {}
Při řešení přetížení pro členy rozšíření bychom neměli řadit podle typu deklarace, ale místo toho zohlednit všechna rozšíření ve stejném oboru?
Odpověď
Vždy se budeme seskupovat. Výše uvedený příklad vytiskne Ext2 ReadOnlySpan
Dědičnost atributů při přepsáních (vyřešeno)
Má být atribut zděděný? Pokud ne, jaká je priorita přepsání člena?
Pokud je atribut zadán ve virtuálním členu, musí být přepsání tohoto člena vyžadováno k opakování atributu?
Odpověď
Atribut nebude označen jako zděděný. Podíváme se na nejméně odvozenou deklaraci člena, abychom určili prioritu řešení přetížení.
Chyba nebo upozornění aplikace při přepsání (vyřešeno)
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?
}
Co bychom měli udělat při použití OverloadResolutionPriorityAttribute
v kontextu, kde se ignoruje, například přetížení:
- Nedělejte nic, nechte to tiše ignorovat.
- Zadejte upozornění, že atribut bude ignorován.
- Vydat chybu, že atribut není povolený.
3 je nejopatrnější přístup, pokud si myslíme, že v budoucnu může existovat prostor, kde bychom mohli chtít povolit přepsání pro určení tohoto atributu.
Odpověď
Zvolíme číslo 3 a zablokujeme aplikaci tam, kde by byla ignorována.
Implicitní implementace rozhraní (zodpovězeno)
Co by mělo být chování implicitní implementace rozhraní? Je nutné zadat OverloadResolutionPriority
? Co by mělo být chování kompilátoru, když dojde k implicitní implementaci bez priority? To se téměř jistě stane, protože knihovna rozhraní může být aktualizována, ale ne implementace. Předchozí umění zde s params
není specifikovat a nepřevést hodnotu:
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"); }
}
Naše možnosti jsou:
- Sledujte
params
.OverloadResolutionPriorityAttribute
nebude implicitně přeneseno ani nebude nutné jej specifikovat. - Implicitně přeneste atribut.
- Nepřenášejte atribut implicitně, vyžadujte, aby byl zadán při volání.
- To přináší další otázku: co by mělo být chování, když kompilátor narazí na tento scénář s kompilovanými odkazy?
Odpověď
Půjdeme s 1.
Další chyby aplikace (zodpovězené)
Existuje několik dalších umístění, jako je tento, které je potřeba potvrdit. Patří mezi ně:
- Specifikace nikdy neříká, že převodní operátory procházejí přetíženým rozlišením, takže implementace blokuje aplikaci na tyto členy. Mělo by to být potvrzeno?
- Lambda – podobně lambda nejsou nikdy předmětem řešení přetížení, takže implementace je blokuje. Mělo by to být potvrzeno?
- Destruktory – opět blokované.
- Statické konstruktory – opět blokované.
- Místní funkce – tyto funkce nejsou momentálně blokované, protože přetížit, nemůžete je přetížit. To je podobné tomu, jak nenarazíme na chybu, když je atribut aplikován na člena typu, který není přetížený. Má být toto chování potvrzeno?
Odpověď
Všechna výše uvedená umístění jsou blokovaná.
Chování langversion (zodpovězeno)
Implementace v současné době vydává pouze chyby langversion při použití OverloadResolutionPriorityAttribute
, ne, když skutečně něco ovlivňuje. Toto rozhodnutí bylo provedeno, protože existují rozhraní API, která BCL přidá (nyní i v průběhu času), která začnou používat tento atribut; Pokud uživatel nastaví jazykovou verzi zpět na verzi C# 12 nebo starší, mohou se tyto členy zobrazit a v závislosti na chování naší funkce langversion buď:
- Pokud atribut v jazyce C# <13 ignorujeme, dojde k chybě nejednoznačnosti, protože rozhraní API je bez atributu skutečně nejednoznačné, nebo;
- Pokud dojde k chybě, když atribut ovlivnil výsledek, mohl by se výskyt chyby projevit jako nepoužitelnost rozhraní API. To bude zvlášť špatné, protože
Debug.Assert(bool)
je v .NET 9 de-prioritizováno, neboť - Pokud tiše změníme rozlišení, může dojít k potenciálně odlišnému chování mezi různými verzemi kompilátoru, pokud jeden kompilátor rozumí atributu a jiný ne.
Poslední chování bylo zvoleno, protože výsledkem je největší kompatibilita vpřed, ale měnící se výsledek může být pro některé uživatele překvapivý. Měli bychom to potvrdit, nebo bychom měli zvolit jednu z dalších možností?
Odpověď
Budeme pokračovat s možností 1 a tiše ignorovat atribut v předchozích jazykových verzích.
Alternativy
Předchozí návrh se pokusil určit BinaryCompatOnlyAttribute
přístup, který byl velmi náročný při odebírání věcí z viditelnosti. To ale má spoustu náročných problémů s implementací, které buď znamenají, že návrh je příliš silný, aby byl užitečný (brání testování starých rozhraní API, například) nebo tak slabý, že zmeškala některé původní cíle (například schopnost mít rozhraní API, které by jinak bylo považováno za nejednoznačné volání nového rozhraní API). Tato verze se replikuje níže.
BinaryCompatOnlyAttribute Návrh pouze pro binární kompatibilitu (zastaralé)
BinaryCompatOnlyAttribute
Podrobný návrh
System.BinaryCompatOnlyAttribute
Zavádíme nový rezervovaný atribut:
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 {}
Při použití u člena typu se tento člen považuje za nepřístupný pro kompilátor na jakémkoli místě, což znamená, že nepřispívá k vyhledávání členů, řešení přetížení ani žádnému jinému podobnému procesu.
Domény přístupnosti
Aktualizujeme §7.5.3 Domény přístupnostinásledujícím:
Doména přístupnosti člena se skládá z (pravděpodobně nesouvislého) oddílu textu programu, ve kterém je povolený přístup k členu. Pro účely definování domény přístupnosti člena se říká, že je na úrovni nejvyšší, pokud není deklarován v rámci typu, a o členu se říká, že je vnořený, pokud je deklarován v rámci jiného typu. Kromě toho je text programu definován jako veškerý text obsažený ve všech kompilačních jednotkách tohoto programu a text určitého typu programu je definován jako veškerý text obsažený v type_declarationdaného typu (včetně případně vnořených typů).
Doména přístupnosti předdefinovaného typu (například
object
,int
nebodouble
) je neomezená.Doména přístupnosti nevázaného typu nejvyšší úrovně
T
(§8.4.4), která je deklarována v programuP
je definována takto:
- Pokud je
T
označenaBinaryCompatOnlyAttribute
, je doména přístupnostiT
zcela nepřístupná pro text programuP
a jakýkoli program, který odkazuje naP
.- Pokud je deklarovaná přístupnost
T
veřejná, doména přístupnostiT
je programový textP
a jakýkoli program, který odkazuje naP
.- Pokud je deklarovaná přístupnost
T
interní, doména přístupnostiT
je text programuP
.Poznámka: Z těchto definic vyplývá, že doména přístupnosti nevázaného typu nejvyšší úrovně je vždy alespoň text programu programu, ve kterém je tento typ deklarován. koncová poznámka
Doména přístupnosti pro konstruovaný typ
T<A₁, ..., Aₑ>
je průnikem domény přístupnosti nevázaného obecného typuT
a domény přístupnosti argumentů typuA₁, ..., Aₑ
.Doména přístupnosti vnořeného člena
M
deklarovaného v typuT
v rámci programuP
je definována následujícím způsobem (přičemžM
samotný může být typ):
- Pokud je
M
označenaBinaryCompatOnlyAttribute
, je doména přístupnostiM
zcela nepřístupná pro text programuP
a jakýkoli program, který odkazuje naP
.- Pokud je deklarovaná přístupnost
M
public
, doména přístupnostiM
je doména přístupnostiT
.- Pokud je deklarovaná přístupnost
M
protected internal
, ať jeD
sjednocením textu programuP
a textu programu jakéhokoli typu odvozeného zT
, který je deklarován vněP
. Doména přístupnostiM
je průnikem domény přístupnostiT
aD
.- Pokud je přístupnost
M
deklarována jakoprivate protected
, pakD
je průsečíkem textu programuP
, textu programuT
a jakéhokoli typu odvozeného zT
. Doména přístupnostiM
je průnikem domény přístupnostiT
aD
.- Pokud je deklarovaná přístupnost
M
protected
, považujmeD
za sjednocení textu programu zahrnujícíhoT
a textu programu jakéhokoliv typu odvozeného zT
. Doména přístupnostiM
je průnikem domény přístupnostiT
aD
.- Pokud je deklarovaná přístupnost
M
internal
, doména přístupnostiM
je průnikem domény přístupnostiT
s textem programuP
.- Pokud je deklarovaná přístupnost
M
private
, doména přístupnostiM
je text programuT
.
Cílem těchto dodatků je zajistit, aby členové, kteří jsou označeni BinaryCompatOnlyAttribute
, byli zcela nedostupní z kteréhokoli místa, nebudou se účastnit vyhledávání členů a nemohou ovlivnit zbytek programu. To znamená, že nemůžou implementovat členy rozhraní, nemůžou se navzájem volat a nemohou být přepsány (virtuální metody), skryté nebo implementované (členy rozhraní). Zda je to příliš přísné, je předmětem několika otevřených otázek níže.
Nevyřešené otázky
Virtuální metody a překrývání
Co děláme, když je virtuální metoda označená jako BinaryCompatOnly
? Přepsání v odvozené třídě nemusí být ani v aktuálním sestavení, a může se stát, že uživatel chce zavést novou verzi metody, která se například liší pouze návratovým typem, což je něco, co C# obvykle neumožňuje při přetěžování. Co se stane s přepsáními předchozí metody při opětovném kompilaci? Mají povoleno přepsat člena BinaryCompatOnly
, pokud jsou také označeny jako BinaryCompatOnly
?
Používání v rámci stejné knihovny DLL
Tento návrh uvádí, že členové BinaryCompatOnly
nejsou nikde neviditelné, ani v sestavení, které se právě kompiluje. Je to příliš striktní, nebo by členové BinaryCompatAttribute
měli možná navazovat na sebe?
Implicitní implementace členů rozhraní
Měli by mít členové BinaryCompatOnly
možnost implementovat členy rozhraní? Nebo by jim v tom mělo být bráněno. To by vyžadovalo, že když chce uživatel převést implicitní implementaci rozhraní na BinaryCompatOnly
, bude muset navíc poskytnout explicitní implementaci rozhraní, pravděpodobně zkopírováním stejného těla jako člena BinaryCompatOnly
, protože explicitní implementace rozhraní už nebude mít přístup k původnímu členu.
Implementace členů rozhraní označených jako BinaryCompatOnly
Co děláme, když je člen rozhraní označený jako BinaryCompatOnly
? Typ stále musí poskytnout implementaci daného člena; je možné, že musíme jednoduše říci, že členy rozhraní nelze označit jako BinaryCompatOnly
.
C# feature specifications