Sdílet prostřednictvím


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řed string.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 na string i string[]. To znamená, že je nejednoznačný při předání metodě s přetížením params string[] i params 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řed M3(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.
  • 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í:

  1. Nedělejte nic, nechte to tiše ignorovat.
  2. Zadejte upozornění, že atribut bude ignorován.
  3. 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:

  1. Sledujte params. OverloadResolutionPriorityAttribute nebude implicitně přeneseno ani nebude nutné jej specifikovat.
  2. Implicitně přeneste atribut.
  3. Nepřenášejte atribut implicitně, vyžadujte, aby byl zadán při volání.
    1. 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, intnebo double) je neomezená.

Doména přístupnosti nevázaného typu nejvyšší úrovně T (§8.4.4), která je deklarována v programu P je definována takto:

  • Pokud je T označena BinaryCompatOnlyAttribute, je doména přístupnosti T zcela nepřístupná pro text programu P a jakýkoli program, který odkazuje na P.
  • Pokud je deklarovaná přístupnost T veřejná, doména přístupnosti T je programový text P a jakýkoli program, který odkazuje na P.
  • Pokud je deklarovaná přístupnost T interní, doména přístupnosti T je text programu P.

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 typu T a domény přístupnosti argumentů typu A₁, ..., Aₑ.

Doména přístupnosti vnořeného člena M deklarovaného v typu T v rámci programu Pje definována následujícím způsobem (přičemž M samotný může být typ):

  • Pokud je M označena BinaryCompatOnlyAttribute, je doména přístupnosti M zcela nepřístupná pro text programu P a jakýkoli program, který odkazuje na P.
  • Pokud je deklarovaná přístupnost Mpublic, doména přístupnosti M je doména přístupnosti T.
  • Pokud je deklarovaná přístupnost Mprotected internal, ať je D sjednocením textu programu P a textu programu jakéhokoli typu odvozeného z T, který je deklarován vně P. Doména přístupnosti M je průnikem domény přístupnosti T a D.
  • Pokud je přístupnost M deklarována jako private protected, pak D je průsečíkem textu programu P, textu programu T a jakéhokoli typu odvozeného z T. Doména přístupnosti M je průnikem domény přístupnosti T a D.
  • Pokud je deklarovaná přístupnost Mprotected, považujme D za sjednocení textu programu zahrnujícího Ta textu programu jakéhokoliv typu odvozeného z T. Doména přístupnosti M je průnikem domény přístupnosti T a D.
  • Pokud je deklarovaná přístupnost Minternal, doména přístupnosti M je průnikem domény přístupnosti T s textem programu P.
  • Pokud je deklarovaná přístupnost Mprivate, doména přístupnosti M je text programu T.

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.