Sdílet prostřednictvím


Statické abstraktní členy v rozhraních

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ůze o 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/4436

Shrnutí

Rozhraní může určit abstraktní statické členy, pro které musí implementující třídy a struktury poskytnout explicitní nebo implicitní implementaci. K členům lze přistupovat prostřednictvím parametrů typu, které jsou omezeny rozhraním.

Motivace

V současné době neexistuje způsob, jak abstraktovat statické členy a psát generalizovaný kód, který se vztahuje na typy definující tyto statické členy. To je obzvláště problematické u druhů komponent, které existují pouze v statické formě, zejména u operátorů.

Tato funkce umožňuje obecné algoritmy nad číselnými typy reprezentované omezeními rozhraní, které určují přítomnost daných operátorů. Algoritmy lze proto vyjádřit z hlediska těchto operátorů:

// Interface specifies static properties and operators
interface IAddable<T> where T : IAddable<T>
{
    static abstract T Zero { get; }
    static abstract T operator +(T t1, T t2);
}

// Classes and structs (including built-ins) can implement interface
struct Int32 : …, IAddable<Int32>
{
    static Int32 IAddable.operator +(Int32 x, Int32 y) => x + y; // Explicit
    public static int Zero => 0;                          // Implicit
}

// Generic algorithms can use static members on T
public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;                   // Call static operator
    foreach (T t in ts) { result += t; } // Use `+`
    return result;
}

// Generic method can be applied to built-in and user-defined types
int sixtyThree = AddAll(new [] { 1, 2, 4, 8, 16, 32 });

Syntax

Členy rozhraní

Tato funkce by umožňovala deklarovat virtuální členy statického rozhraní.

Pravidla před C# 11

Před C# 11 jsou členy instance v rozhraních implicitně abstraktní (nebo virtuální, pokud mají výchozí implementaci), ale mohou volitelně mít modifikátor abstract (nebo virtual). Členy jiných než virtuálních instancí musí být explicitně označeny jako sealed.

Členové statického rozhraní jsou dnes implicitně ne-virtuální a neumožňují abstract, virtual nebo modifikátory sealed.

Návrh

Abstraktní statické členy

Členové statického rozhraní kromě polí mohou mít také modifikátor abstract. Abstraktní statické členy nesmí mít implementaci (a v případě vlastností nejsou povoleny přístupové metody s implementací).

interface I<T> where T : I<T>
{
    static abstract void M();
    static abstract T P { get; set; }
    static abstract event Action E;
    static abstract T operator +(T l, T r);
    static abstract bool operator ==(T l, T r);
    static abstract bool operator !=(T l, T r);
    static abstract implicit operator T(string s);
    static abstract explicit operator string(T t);
}
Virtuální statické členy

Členové statického rozhraní kromě polí mohou mít také modifikátor virtual. Virtuální statické členy musí mít tělo.

interface I<T> where T : I<T>
{
    static virtual void M() {}
    static virtual T P { get; set; }
    static virtual event Action E;
    static virtual T operator +(T l, T r) { throw new NotImplementedException(); }
}
Explicitně nevirtuální statické členy

Pro symetrii s nevirtuálními členy instance by statickým členům (s výjimkou polí) měl být povolen volitelný modifikátor sealed, i když jsou ve výchozím nastavení nevirtuální.

interface I0
{
    static sealed void M() => Console.WriteLine("Default behavior");
    
    static sealed int f = 0;
    
    static sealed int P1 { get; set; }
    static sealed int P2 { get => f; set => f = value; }
    
    static sealed event Action E1;
    static sealed event Action E2 { add => E1 += value; remove => E1 -= value; }
    
    static sealed I0 operator +(I0 l, I0 r) => l;
}

Implementace členů rozhraní

Dnešní pravidla

Třídy a struktury mohou implementovat abstraktní členy instancí rozhraní implicitně nebo explicitně. Implicitně implementovaný člen rozhraní je běžný člen (virtuální nebo nevirtuální) deklarace třídy nebo struktury, která rovněž "plní roli" implementace člena rozhraní. Člen může být dokonce zděděný ze základní třídy, a proto ani není k dispozici v deklaraci třídy.

Explicitně implementovaný člen rozhraní používá kvalifikovaný název k identifikaci příslušného člena rozhraní. Implementace není přímo přístupná jako člen třídy nebo struktury, ale pouze prostřednictvím rozhraní.

Návrh

Ve třídách a strukturách není nutná žádná nová syntaxe, která by usnadnila implicitní implementaci statických abstraktních členů rozhraní. Stávající deklarace statického členu slouží k danému účelu.

Explicitní implementace statických abstraktních členů rozhraní používají kvalifikovaný název spolu s modifikátorem static.

class C : I<C>
{
    string _s;
    public C(string s) => _s = s;
    static void I<C>.M() => Console.WriteLine("Implementation");
    static C I<C>.P { get; set; }
    static event Action I<C>.E // event declaration must use field accessor syntax
    {
        add { ... }
        remove { ... }
    }
    static C I<C>.operator +(C l, C r) => new C($"{l._s} {r._s}");
    static bool I<C>.operator ==(C l, C r) => l._s == r._s;
    static bool I<C>.operator !=(C l, C r) => l._s != r._s;
    static implicit I<C>.operator C(string s) => new C(s);
    static explicit I<C>.operator string(C c) => c._s;
}

Sémantika

Omezení operátorů

V současné době mají všechny deklarace unárního a binárního operátoru určitý požadavek zahrnující alespoň jeden z jejich operandů být typu T nebo T?, kde T je typ instance ohraničujícího typu.

Tyto požadavky musí být uvolněny, aby omezený operand mohl být parametrem typu, který je považován za "typ instance obklopujícího typu".

Aby se parametr typu T počítal jako "typ instance obalujícího typu", musí splňovat následující požadavky:

  • T je parametr přímého typu v rozhraní, ve kterém se deklarace operátoru vyskytuje, a
  • T je přímo omezen tím, co specifikace nazývá "typ instance" – tj. okolním rozhraním s vlastními parametry typu, které se používají jako argumenty typu.

Operátory rovnosti a převody

V rozhraních budou povoleny abstraktní/virtuální deklarace operátorů == a != a také abstraktní/virtuální deklarace implicitních a explicitních převodních operátorů. Odvozená rozhraní budou také povolena k jejich implementaci.

Pro operátory == a != musí být alespoň jeden parametr typu, který je považován za "typ instance ohraničujícího typu", jak bylo definováno v předchozí části.

Implementace statických abstraktních členů

Pravidla pro případy, kdy je deklarace statického členu ve třídě nebo struktuře považována za implementaci statického abstraktního členu rozhraní a pro jaké požadavky platí, pokud ano, jsou stejné jako pro členy instance.

TBD: Zde mohou být další nebo jiná pravidla nezbytná, o které jsme si zatím nepřemýšleli.

Rozhraní jako argumenty typu

Probrali jsme problém vyvolaný https://github.com/dotnet/csharplang/issues/5955 a rozhodli jsme se přidat omezení týkající se použití rozhraní jako argumentu typu (https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts). Toto je omezení, které navrhl https://github.com/dotnet/csharplang/issues/5955 a schválilo LDM.

Rozhraní obsahující nebo dědící statického abstraktního či virtuálního člena, který nemá v tomto rozhraní nejvíce konkrétní implementaci, nelze použít jako argument typu. Pokud mají všechny statické abstraktní nebo virtuální členy nejvýraznější implementaci, lze rozhraní použít jako argument typu.

Přístup ke statickým abstraktním členům rozhraní

Ke statickému abstraktnímu členu rozhraní M lze přistupovat u parametru typu T pomocí výrazu T.M, pokud je T omezen rozhraním I a M je přístupný statický abstraktní člen I.

T M<T>() where T : I<T>
{
    T.M();
    T t = T.P;
    T.E += () => { };
    return t + T.P;
}

Za běhu se použije implementace konkrétního člena, která existuje u konkrétního typu, který je zadán jako typový argument.

C c = M<C>(); // The static members of C get called

Vzhledem k tomu, že výrazy dotazu jsou specifikované jako syntaktické přepsání, jazyk C# ve skutečnosti umožňuje použít typ jako zdroj dotazu, pokud má statické členy pro operátory dotazu, které používáte. Jinými slovy, pokud se syntax hodí, povolíme to! Myslíme si, že toto chování nebylo záměrné ani důležité v původním LINQ a nechceme, aby se podporovalo u parametrů typu. Pokud existují scénáře, o kterých uslyšíme, můžeme se rozhodnout je později přijmout.

Bezpečnost rozptylu §18.2.3.2

Pravidla zabezpečení odchylek by se měla vztahovat na podpisy statických abstraktních členů. Návrh na doplnění v https://github.com/dotnet/csharplang/blob/main/proposals/csharp-9.0/variance-safety-for-static-interface-members.md#variance-safety by měl být upraven z

Tato omezení se nevztahují na výskyty typů v rámci deklarací statických členů.

k

Tato omezení se nevztahují na výskyty typů v rámci deklarací nevirtuálních ani neabstraktních statických členů.

§10.5.4 uživatelem definované implicitní převody

Následující odrážky

  • Určete typy S, S₀ a T₀.
    • Pokud má E typ, ať je S tímto typem.
    • Pokud jsou S nebo T nulovatelné hodnotové typy, pak jsou Sᵢ a Tᵢ jejich podkladovými typy, jinak jsou Sᵢ a Tᵢ odpovídajícími S a T.
    • Pokud jsou parametry typu Sᵢ nebo Tᵢ, nechte S₀ a T₀ být jejich efektivními základními třídami, jinak nechte S₀ a T₀ být Sₓ a Tᵢ.
  • Najděte sadu typů, D, ze kterých se budou považovat uživatelem definované operátory převodu. Tato sada se skládá z S0 (pokud S0 je třída nebo struktura), základní třídy S0 (pokud S0 je třída) a T0 (pokud T0 je třída nebo struktura).
  • Vyhledejte množinu použitelných uživatelsky definovaných a povýšených operátorů převodu U. Tato sada se skládá z uživatelem definovaných a implicitně zvednutých konverzních operátorů deklarovaných třídami nebo strukturami v D, které převádějí z typu, který pokrývá S, na typ, který je pokrývaný T. Pokud je U prázdný, převod není definován a dojde k chybě v době kompilace.

jsou upraveny takto:

  • Určete typy S, S₀ a T₀.
    • Pokud má E typ, ať je S tímto typem.
    • Pokud jsou S nebo T nulovatelné hodnotové typy, pak jsou Sᵢ a Tᵢ jejich podkladovými typy, jinak jsou Sᵢ a Tᵢ odpovídajícími S a T.
    • Pokud jsou parametry typu Sᵢ nebo Tᵢ, nechte S₀ a T₀ být jejich efektivními základními třídami, jinak nechte S₀ a T₀ být Sₓ a Tᵢ.
  • Vyhledejte množinu použitelných uživatelsky definovaných a povýšených operátorů převodu U.
    • Najděte sadu typů, D1, ze kterých se budou považovat uživatelem definované operátory převodu. Tato sada se skládá z S0 (pokud S0 je třída nebo struktura), základní třídy S0 (pokud S0 je třída) a T0 (pokud T0 je třída nebo struktura).
    • Vyhledejte množinu použitelných uživatelsky definovaných a povýšených operátorů převodu U1. Tato sada se skládá z uživatelem definovaných a implicitně zvednutých konverzních operátorů deklarovaných třídami nebo strukturami v D1, které převádějí z typu, který pokrývá S, na typ, který je pokrývaný T.
    • Pokud U1 není prázdný, U je U1. Jinak
      • Najděte sadu typů, D2, ze kterých se budou považovat uživatelem definované operátory převodu. Tato sada se skládá z Sᵢefektivní sady rozhraní a jejich základních rozhraní (pokud Sᵢ je parametr typu) a Tᵢefektivní sady rozhraní (pokud Tᵢ je parametr typu).
      • Vyhledejte množinu použitelných uživatelsky definovaných a povýšených operátorů převodu U2. Tato sada se skládá z uživatelem definovaných a zvednutých implicitních konverzních operátorů deklarovaných rozhraními v D2, které převádějí z typu, který zahrnuje S, na typ, který zahrnuje T.
      • Pokud U2 není prázdný, U je U2
  • Pokud je U prázdný, převod není definován a dojde k chybě v době kompilace.

§10.3.9 explicitní konverze definované uživatelem

Následující odrážky

  • Určete typy S, S₀ a T₀.
    • Pokud má E typ, ať je S tímto typem.
    • Pokud jsou S nebo T nulovatelné hodnotové typy, pak jsou Sᵢ a Tᵢ jejich podkladovými typy, jinak jsou Sᵢ a Tᵢ odpovídajícími S a T.
    • Pokud jsou parametry typu Sᵢ nebo Tᵢ, nechte S₀ a T₀ být jejich efektivními základními třídami, jinak nechte S₀ a T₀ být Sᵢ a Tᵢ.
  • Najděte sadu typů, D, ze kterých se budou považovat uživatelem definované operátory převodu. Tato sada se skládá z S0 (pokud S0 je třída nebo struktura), základní třídy S0 (pokud S0 je třída), T0 (pokud T0 je třída nebo struktura) a základní třídy T0 (pokud T0 je třída).
  • Vyhledejte množinu použitelných uživatelsky definovaných a povýšených operátorů převodu U. Tato sada se skládá z uživatelem definovaných implicitních nebo explicitních konverzních operátorů deklarovaných třídami nebo strukturami ve D, které převádějí z typu zahrnujícího nebo zahrnutého v S na typ zahrnující nebo zahrnutý v T. Pokud je U prázdný, převod není definován a dojde k chybě v době kompilace.

jsou upraveny takto:

  • Určete typy S, S₀ a T₀.
    • Pokud má E typ, ať je S tímto typem.
    • Pokud jsou S nebo T nulovatelné hodnotové typy, pak jsou Sᵢ a Tᵢ jejich podkladovými typy, jinak jsou Sᵢ a Tᵢ odpovídajícími S a T.
    • Pokud jsou parametry typu Sᵢ nebo Tᵢ, nechte S₀ a T₀ být jejich efektivními základními třídami, jinak nechte S₀ a T₀ být Sᵢ a Tᵢ.
  • Vyhledejte množinu použitelných uživatelsky definovaných a povýšených operátorů převodu U.
    • Najděte sadu typů, D1, ze kterých se budou považovat uživatelem definované operátory převodu. Tato sada se skládá z S0 (pokud S0 je třída nebo struktura), základní třídy S0 (pokud S0 je třída), T0 (pokud T0 je třída nebo struktura) a základní třídy T0 (pokud T0 je třída).
    • Vyhledejte množinu použitelných uživatelsky definovaných a povýšených operátorů převodu U1. Tato sada se skládá z uživatelem definovaných implicitních nebo explicitních konverzních operátorů deklarovaných třídami nebo strukturami ve D1, které převádějí z typu zahrnujícího nebo zahrnutého v S na typ zahrnující nebo zahrnutý v T.
    • Pokud U1 není prázdný, U je U1. Jinak
      • Najděte sadu typů, D2, ze kterých se budou považovat uživatelem definované operátory převodu. Tato sada se skládá z Sᵢefektivní sady rozhraní a jejich základních rozhraní (pokud Sᵢ je parametr typu) a Tᵢefektivní sady rozhraní a jejich základní rozhraní (pokud Tᵢ je parametr typu).
      • Vyhledejte množinu použitelných uživatelsky definovaných a povýšených operátorů převodu U2. Tato sada se skládá z uživatelem definovaných a povolených implicitních nebo explicitních konverzních operátorů deklarovaných rozhraními v D2, které převádějí z typu obsahujícího nebo obsaženého v S na typ obsahující nebo obsažený v T.
      • Pokud U2 není prázdný, U je U2
  • Pokud je U prázdný, převod není definován a dojde k chybě v době kompilace.

Výchozí implementace

další funkcí tohoto návrhu je umožnit statickým virtuálním členům v rozhraních mít výchozí implementace, stejně jako to dělají virtuální/abstraktní členové instance.

Jednou z komplikací je, že výchozí implementace by chtěly volat ostatní statické virtuální členy virtuálně. Povolení zavolání statických virtuálních členů přímo v rozhraní by vyžadovalo tok skrytého parametru typu představujícího typ "self", na kterém se skutečně vyvolala aktuální statická metoda. Zdá se to složité, nákladné a potenciálně matoucí.

Probrali jsme jednodušší verzi, která zachovává omezení aktuálního návrhu, že statické virtuální členy mohou pouze být vyvolány u parametrů typu. Vzhledem k tomu, že rozhraní se statickými virtuálními členy často mají explicitní parametr typu představující typ "self", nejedná se o velkou ztrátu: ostatní statické virtuální členy lze volat na tomto typu "self". Tato verze je mnohem jednodušší a zdá se docela proveditelná.

V https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md#default-implementations-of-abstract-statics jsme se rozhodli podporovat výchozí implementace statických členů, které následují/rozšiřují pravidla stanovená v https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/default-interface-methods.md odpovídajícím způsobem.

Porovnávání vzorů

Při daném kódu může uživatel rozumně očekávat, že vypíše "True" (stejně jako kdyby byl konstantní vzor psán inline):

M(1.0);

static void M<T>(T t) where T : INumberBase<T>
{
    Console.WriteLine(t is 1); // Error. Cannot use a numeric constant
    Console.WriteLine((t is int i) && (i is 1)); 
}

Vzhledem k tomu, že vstupní typ vzoru není double, konstantní 1 vzor nejprve zkontroluje příchozí T proti int. To je neintuitivní, takže se zablokuje, dokud budoucí verze jazyka C# nepřidá lepší zpracování pro číselné porovnávání s typy odvozenými z INumberBase<T>. K tomu řekneme, že explicitně rozpoznáme INumberBase<T> jako typ, ze kterého budou odvozena všechna čísla, a zablokujeme vzor, pokud se snažíme shodovat číselný konstantní vzor s číselným typem, ve kterém nemůžeme znázorňovat vzor (tj. parametr typu omezený na INumberBase<T>nebo uživatelem definovaný typ čísla, který dědí z INumberBase<T>).

Formálně přidáme výjimku k definici vzorové kompatibility pro konstantní vzory:

Konstantní vzor testuje hodnotu výrazu proti konstantní hodnotě. Konstantou může být libovolný konstantní výraz, například literál, název deklarované proměnné const nebo výčtová konstanta. Pokud vstupní hodnota není otevřeným typem, konstantní výraz se implicitně převede na typ odpovídajícího výrazu; Pokud typ vstupní hodnoty není vzorově kompatibilní s typem konstantního výrazu, je operace porovnávání vzorů chybou. Pokud je konstantní výraz, který má být porovnán, číselnou hodnotou, a vstupní hodnota je typ, který dědí z System.Numerics.INumberBase<T>, a neexistuje žádný konstantní převod z konstantního výrazu na typ vstupní hodnoty, pak je operace porovnávání vzorů chyba.

Přidáme také podobnou výjimku pro relační vzory:

Pokud je vstup typem, pro který je definován vhodný integrovaný binární relační operátor, je použitý se vstupem jako levý operand a s danou konstantou jako pravý operand, je vyhodnocení tohoto operátoru považováno za význam relačního vzoru. V opačném případě převedeme vstup na typ výrazu pomocí explicitního převodu s možnou hodnotou null nebo rozbalení. Jedná se o chybu v době kompilace, pokud neexistuje žádný takový převod. Jedná se o chybu v době kompilace, pokud je vstupním typem parametr omezený na typ nebo typ dědící z System.Numerics.INumberBase<T> a vstupní typ nemá definovaný žádný vhodný integrovaný binární relační operátor. Vzor se považuje za neodpovídající, pokud převod selže. Pokud převod proběhne úspěšně, výsledkem operace porovnávání vzorů je výsledek vyhodnocení výrazu e OP v, kde e je převedený vstup, OP je relační operátor a v je konstantní výraz.

Nevýhody

  • "statická abstrakce" je nový koncept, který bude smysluplně přidávat do koncepčního zatížení jazyka C#.
  • Není to levná funkce k sestavení. Měli bychom se ujistit, že to stojí za to.

Alternativy

Strukturální omezení

Alternativním přístupem by bylo mít přímo "strukturální omezení" a explicitně vyžadovat přítomnost konkrétních operátorů u parametru typu. Nevýhody toho jsou: - To by se muselo napsat pokaždé. Pojmenované omezení se zdá být lepší. - Jedná se o zcela nový druh omezení, zatímco navrhovaná funkce využívá stávající koncept omezení rozhraní. - Fungovalo by to jenom pro operátory, ne (snadno) pro jiné druhy statických členů.

Nevyřešené otázky

Statická abstraktní rozhraní a statické třídy

Další informace najdete v tématu https://github.com/dotnet/csharplang/issues/5783 a https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes.

Schůzky o navrhování