Megosztás a következőn keresztül:


Statikus absztrakt tagok a felületeken

Jegyzet

Ez a cikk egy funkcióspecifikáció. A specifikáció a funkció tervezési dokumentumaként szolgál. Tartalmazza a specifikáció javasolt módosításait, valamint a funkció tervezése és fejlesztése során szükséges információkat. Ezeket a cikkeket mindaddig közzéteszik, amíg a javasolt specifikációmódosításokat nem véglegesítik, és be nem építik a jelenlegi ECMA-specifikációba.

A szolgáltatás specifikációja és a befejezett implementáció között eltérések lehetnek. Ezeket a különbségeket a vonatkozó nyelvi tervezési értekezlet (LDM) megjegyzései rögzítik.

A funkcióspektusok C# nyelvi szabványba való bevezetésének folyamatáról a specifikációkcímű cikkben olvashat bővebben.

Bajnoki probléma: https://github.com/dotnet/csharplang/issues/4436

Összefoglalás

Az interfész megengedheti, hogy absztrakt statikus tagokat határozzon meg, amelyekhez az implementáló osztályoknak és struktúráknak explicit vagy implicit implementációt kell biztosítaniuk. A tagok az interfész által korlátozott típusparaméterek közül érhetők el.

Motiváció

Jelenleg nincs mód arra, hogy absztrakciót alkalmazzon a statikus tagok felett, és általánosított kódot írjon, amely a statikus tagokat meghatározó típusokra vonatkozik. Ez különösen problémás az olyan tagtípusok esetében, amelyek csak léteznek statikus formában, különösen az operátorok esetében.

Ez a funkció általános algoritmusokat tesz lehetővé numerikus típusok felett, amelyeket az adott operátorok jelenlétét meghatározó felületi megkötések jelölnek. Az algoritmusok tehát az ilyen operátorok tekintetében kifejezhetők:

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

Szintaxis

Interfésztagok

A funkció lehetővé tenné a statikus interfész tagok virtuálisként történő deklarálását.

Szabályok a C# 11 előtt

A C# 11 előtt a felületi példányok tagjai implicit módon absztraktak (vagy virtuálisak, ha alapértelmezett implementációval rendelkeznek), de szükség esetén abstract (vagy virtual) módosítókkal is rendelkezhetnek. A nem virtuális példánytagokat explicit módon kell sealedmegjelölni.

A statikus felület tagjai ma implicit módon nem virtuálisak, és nem engedélyezik abstract, virtual vagy sealed módosítókat.

Javaslat

Absztrakt statikus tagok

A mezőkön kívüli statikus illesztőtagok is használhatják a abstract módosító elemet. Az absztrakt statikus tagok nem rendelkezhetnek törzsekkel (vagy tulajdonságok esetén a tartozékokkal nem rendelkezhet törzs).

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ális statikus tagok

A mezőkön kívüli statikus illesztőtagok is használhatják a virtual módosító elemet. A virtuális statikus tagoknak rendelkezniük kell egy törzsvel.

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(); }
}
Kifejezetten nem virtuális statikus tagok

A nem virtuális példányok tagjaival való szimmetria esetén a statikus tagoknak (kivéve a mezőket) engedélyezni kell egy választható sealed módosító használatát, annak ellenére, hogy alapértelmezés szerint nem virtuálisak:

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

Az interfésztagok implementálása

A mai szabályok

Az osztályok és a szerkezetek implicit vagy explicit módon implementálhatják az interfészek absztrakt példányainak tagjait. Az implicit módon implementált interfésztag az osztály vagy a struktúra egy normál (virtuális vagy nem virtuális) tagdeklarációja, amely egyszerűen csak "véletlenül" az interfész tagját is megvalósítja. A tag akár egy alaposztályból is örökölhető, így még az osztálydeklarációban sem szerepelhet.

Egy explicit módon implementált felülettag minősített névvel azonosítja a szóban forgó felülettagot. A megvalósítás közvetlenül nem érhető el tagként az osztályon vagy a szerkezeten, hanem csak a felületen keresztül.

Javaslat

Az osztályokban és a szerkezetekben nincs szükség új szintaxisra a statikus absztrakt interfésztagok implicit megvalósításának megkönnyítéséhez. A meglévő statikus tag deklarációk ezt a célt szolgálják.

A statikus absztrakt felülettagok explicit implementációi a static módosítóval együtt minősített nevet használnak.

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

Szemantika

Operátorkorlátozások

Ma minden nem naplós és bináris operátor-deklarációnak van valamilyen követelménye, amely szerint az operandusok legalább egyikének T vagy T?típusúnak kell lennie, ahol a T a beágyazási típus példánytípusa.

Ezeket a követelményeket enyhíteni kell, hogy a korlátozott operandus olyan típusparaméter lehessen, amely a "befoglaló típus példánytípusának" minősül.

Ahhoz, hogy egy típusparaméter T "a beágyazási típus példánytípusának" számíthasson, meg kell felelnie a következő követelményeknek:

  • T egy közvetlen típusparaméter azon a felületen, amelyen az operátor deklarációja történik, és
  • T közvetlenül korlátozza, amit a specifikáció "példánytípusnak" nevez – azaz a környező felületnek a típusargumentumként használt saját típusparamétereivel.

Egyenlőségi operátorok és konverziók

A == és != operátorok absztrakt/virtuális deklarációi, valamint az implicit és explicit konverziós operátorok absztrakt/virtuális deklarációi engedélyezve lesznek a felületeken. A származtatott interfészeknek is megengedik, hogy implementálhassák őket.

Az == és != operátorok esetében legalább egy paramétertípusnak olyan típusparaméternek kell lennie, amely az előző szakaszban meghatározott "a beágyazási típus példánytípusának" számít.

Statikus absztrakt tagok implementálása

Az osztályban vagy struktúrában lévő statikus tagdeklaráció akkor minősül statikus absztrakt interfész tag implementálásának, és akkor vonatkoznak rá a követelmények, ha megegyeznek a példánytagokkal.

TBD: Előfordulhat, hogy további vagy eltérő szabályokra van szükség, amelyekre még nem gondoltunk.

Illesztők típusargumentumként

Megvitattuk a https://github.com/dotnet/csharplang/issues/5955 által felvetett problémát, és úgy döntöttünk, hogy típusargumentumként (https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts) korlátozzuk a felület használatát. Az alábbiakban a https://github.com/dotnet/csharplang/issues/5955 által javasolt és az LDM által jóváhagyott korlátozást találjuk.

Nem használható típusargumentumként olyan statikus absztrakt/virtuális tagot tartalmazó vagy öröklő felület, amely nem rendelkezik a legspecifikusabb implementációval az illesztőben. Ha az összes statikus absztrakt/virtuális tag rendelkezik a legspecifikusabb implementációval, a felület típusargumentumként használható.

Statikus absztrakt felülettagok elérése

A statikus absztrakt felület tagja M a T típusparaméteren érhető el a T.M kifejezés használatával, amikor T egy I interfésszel van korlátozva, és M egy hozzáférhető statikus absztrakt tagja I.

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

Futásidőben az a tényleges tag-implementáció kerül felhasználásra, amely a tényleges, típusargumentumként megadott típuson létezik.

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

Mivel a lekérdezési kifejezések szintaktikai újraírásként vannak definiálva, a C# lehetővé teszi, hogy típusú-et használjon lekérdezési forrásként, amennyiben vannak statikus tagjai az alkalmazott lekérdezési operátorokhoz! Más szóval, ha a szintaxis illik, akkor megengedjük! Úgy gondoljuk, hogy ez a viselkedés nem volt szándékos vagy fontos az eredeti LINQ-ban, és nem szeretnénk támogatást biztosítani a típusparamétereknél. Ha vannak olyan forgatókönyvek, amelyekről hallani fogunk, és később dönthetünk úgy, hogy ezt elfogadjuk.

Variancia biztonsági §18.2.3.2

A varianciára vonatkozó biztonsági szabályokat a statikus absztrakt tagok aláírására kell alkalmazni. A https://github.com/dotnet/csharplang/blob/main/proposals/csharp-9.0/variance-safety-for-static-interface-members.md#variance-safety javasolt kiegészítést módosítani kell.

Ezek a korlátozások nem vonatkoznak a statikus tagok deklarációiban lévő típusok előfordulására.

hoz

Ezek a korlátozások nem vonatkoznak a nem virtuális, nem absztrakt statikus tagok deklarációiban szereplő típusok előfordulására.

§10.5.4 Felhasználó által definiált implicit konverziók

Az alábbi felsoroláspontok

  • Határozza meg a S, S₀ és T₀típusokat.
    • Ha E rendelkezik típussal, S legyen ilyen típus.
    • Ha S vagy T null értékűek, hagyja, hogy Sᵢ és Tᵢ legyen a mögöttes típusuk, különben Sᵢ és Tᵢ legyen S és T.
    • Ha Sᵢ vagy Tᵢ típusparaméterek, hagyja, hogy S₀ és T₀ legyen a tényleges alaposztályuk, különben S₀ és T₀ legyen Sₓ és Tᵢ.
  • Keresse meg a Dtípuskészletet, amelyből a felhasználó által definiált konverziós operátorok lesznek figyelembe véve. Ez a készlet S0 (ha S0 osztály vagy szerkezet), a S0 alaposztályai (ha S0 osztály) és T0 (ha T0 osztály vagy szerkezet).
  • Keresse meg a felhasználó által definiált és átvitt konverziós operátorok készletét, U. Ez a készlet a D osztályai vagy struktúrái által deklarált, felhasználó által definiált és emelt implicit konverziós operátorokból áll, amelyek egy S-et magában foglaló típusból egy Táltal átfoglalt típussá alakítanak. Ha U üres, a konvertálás nincs meghatározva, és fordítási időhiba lép fel.

az alábbiak szerint vannak módosítva:

  • Határozza meg a S, S₀ és T₀típusokat.
    • Ha E rendelkezik típussal, S legyen ilyen típus.
    • Ha S vagy T null értékűek, hagyja, hogy Sᵢ és Tᵢ legyen a mögöttes típusuk, különben Sᵢ és Tᵢ legyen S és T.
    • Ha Sᵢ vagy Tᵢ típusparaméterek, hagyja, hogy S₀ és T₀ legyen a tényleges alaposztályuk, különben S₀ és T₀ legyen Sₓ és Tᵢ.
  • Keresse meg a felhasználó által definiált és átvitt konverziós operátorok készletét, U.
    • Keresse meg a D1típuskészletet, amelyből a felhasználó által definiált konverziós operátorok lesznek figyelembe véve. Ez a készlet S0 (ha S0 osztály vagy szerkezet), a S0 alaposztályai (ha S0 osztály) és T0 (ha T0 osztály vagy szerkezet).
    • Keresse meg a felhasználó által definiált és átvitt konverziós operátorok készletét, U1. Ez a készlet a D1 osztályai vagy struktúrái által deklarált, felhasználó által definiált és emelt implicit konverziós operátorokból áll, amelyek egy S-et magában foglaló típusból egy Táltal átfoglalt típussá alakítanak.
    • Ha U1 nem üres, akkor UU1. Egyébként
      • Keresse meg a D2típuskészletet, amelyből a felhasználó által definiált konverziós operátorok lesznek figyelembe véve. Ez a készlet Sᵢhatékony illesztőkészletből és azok alapillesztőiből áll (ha Sᵢ típusparaméter), valamint Tᵢhatékony illesztőkészletből (ha Tᵢ típusparaméter).
      • Keresse meg a felhasználó által definiált és átvitt konverziós operátorok készletét, U2. A készlet a D2 interfészei által deklarált, felhasználó által definiált és emelt implicit konverziós operátorokat foglalja magában, amelyek olyan típusból konvertálnak, ami magában foglalja S-et, egy olyan típusra, amelyet Tfoglal magában.
      • Ha U2 nem üres, akkor UU2 van.
  • Ha U üres, a konvertálás nincs meghatározva, és fordítási időhiba lép fel.

§10.3.9 felhasználó által meghatározott explicit konverziók

Az alábbi felsoroláspontok

  • Határozza meg a S, S₀ és T₀típusokat.
    • Ha E rendelkezik típussal, S legyen ilyen típus.
    • Ha S vagy T null értékűek, hagyja, hogy Sᵢ és Tᵢ legyen a mögöttes típusuk, különben Sᵢ és Tᵢ legyen S és T.
    • Ha Sᵢ vagy Tᵢ típusparaméterek, hagyja, hogy S₀ és T₀ legyen a tényleges alaposztályuk, különben S₀ és T₀ legyen Sᵢ és Tᵢ.
  • Keresse meg a Dtípuskészletet, amelyből a felhasználó által definiált konverziós operátorok lesznek figyelembe véve. Ez a készlet S0 (ha S0 osztály vagy struktúra), a S0 alaposztályai (ha S0 osztály), T0 (ha T0 osztály vagy struktúra), valamint a T0 alaposztályai (ha T0 osztály).
  • Keresse meg a felhasználó által definiált és átvitt konverziós operátorok készletét, U. Ez a készlet az osztályok vagy struktúrák által a D-ben deklarált, felhasználó által definiált és emelt implicit vagy explicit konverziós operátorokból áll, amelyek egy S által magában foglalt vagy körülhatárolt típusból egy Táltal magában foglalt vagy körülhatárolt típussá alakítanak át. Ha U üres, a konvertálás nincs meghatározva, és fordítási időhiba lép fel.

az alábbiak szerint vannak módosítva:

  • Határozza meg a S, S₀ és T₀típusokat.
    • Ha E rendelkezik típussal, S legyen ilyen típus.
    • Ha S vagy T null értékűek, hagyja, hogy Sᵢ és Tᵢ legyen a mögöttes típusuk, különben Sᵢ és Tᵢ legyen S és T.
    • Ha Sᵢ vagy Tᵢ típusparaméterek, hagyja, hogy S₀ és T₀ legyen a tényleges alaposztályuk, különben S₀ és T₀ legyen Sᵢ és Tᵢ.
  • Keresse meg a felhasználó által definiált és átvitt konverziós operátorok készletét, U.
    • Keresse meg a D1típuskészletet, amelyből a felhasználó által definiált konverziós operátorok lesznek figyelembe véve. Ez a készlet S0 (ha S0 osztály vagy struktúra), a S0 alaposztályai (ha S0 osztály), T0 (ha T0 osztály vagy struktúra), valamint a T0 alaposztályai (ha T0 osztály).
    • Keresse meg a felhasználó által definiált és átvitt konverziós operátorok készletét, U1. Ez a készlet az osztályok vagy struktúrák által a D1-ben deklarált, felhasználó által definiált és emelt implicit vagy explicit konverziós operátorokból áll, amelyek egy S által magában foglalt vagy körülhatárolt típusból egy Táltal magában foglalt vagy körülhatárolt típussá alakítanak át.
    • Ha U1 nem üres, akkor UU1. Egyébként
      • Keresse meg a D2típuskészletet, amelyből a felhasználó által definiált konverziós operátorok lesznek figyelembe véve. Ez a készlet Sᵢhatékony illesztőkészletből és azok alapillesztőiből (ha Sᵢ típusparaméter), valamint Tᵢhatékony illesztőkészletből és azok alapillesztőiből (ha Tᵢ típusparaméter).
      • Keresse meg a felhasználó által definiált és átvitt konverziós operátorok készletét, U2. Ez a készlet az D2 interfészei által deklarált, felhasználó által definiált, valamint emelt szintű implicit vagy explicit konverziós operátorokból áll, amelyek olyan típusból konvertálnak, amelyet a S magában foglal vagy rá kiterjed, olyan típusra, amelyet a Tmagában foglal vagy rá kiterjed.
      • Ha U2 nem üres, akkor UU2 van.
  • Ha U üres, a konvertálás nincs meghatározva, és fordítási időhiba lép fel.

Alapértelmezett implementációk

A javaslat további funkciója, hogy lehetővé tegye a statikus virtuális tagok számára a felületeken az alapértelmezett implementációkat, ahogyan a virtuális/absztrakt tagok is teszik.

Az egyik bonyodalom az, hogy az alapértelmezett implementációk más statikus virtuális tagokat "virtuálisan" szeretnének meghívni. Ahhoz, hogy a statikus virtuális tagokat közvetlenül a felületen lehessen meghívni, egy rejtett típusparamétert kell futtatni, amely az aktuális statikus metódus által ténylegesen meghívott "ön" típust jelöli. Ez bonyolultnak, költségesnek és potenciálisan zavarónak tűnik.

Megvitattunk egy egyszerűbb verziót, amely fenntartja a jelenlegi javaslat korlátait: a statikus virtuális tagok csak típusparamétereken hívhatók meg . Mivel a statikus virtuális tagokkal rendelkező interfészek gyakran rendelkeznek egy "ön" típust képviselő explicit típusparaméterrel, ez nem lenne nagy veszteség: más statikus virtuális tagokat csak az adott öntípusra lehetne meghívni. Ez a verzió sokkal egyszerűbb, és meglehetősen végrehajthatónak tűnik.

Úgy döntöttünk https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md#default-implementations-of-abstract-statics, hogy támogatjuk a statikus tagok alapértelmezett implementációit, követve és kibővítve a https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/default-interface-methods.md-ben meghatározott szabályokat.

Mintaegyezés

A következő kóddal a felhasználó jogosan várhatja el, hogy a kód 'True'-t nyomtasson (mint ahogy azt tenné, ha az állandó mintát közvetlenül írtuk volna meg).

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

Mivel azonban a minta bemeneti típusa nem double, az állandó 1 minta először ellenőrzi a bejövő T-t int-mal szemben. Ez nem intuitív, ezért blokkolva van, amíg egy jövőbeli C#-verzió nem biztosít jobb kezelést a INumberBase<T>-ból származó típusokkal való numerikus egyeztetéshez. Ehhez azt fogjuk mondani, hogy explicit módon felismerjük INumberBase<T>, mint az a típus, amelyből az összes "szám" származik, és letiltjuk a mintát, ha olyan számtípushoz próbálunk egy numerikus állandó mintát illeszteni, amely nem felel meg a mintának (azaz egy INumberBase<T>-re korlátozott típusparaméter vagy egy INumberBase<T>öröklő felhasználó által definiált számtípus).

Formálisan kivételt adunk az állandó minták esetében a mintával kompatibilis definíciójához.

Az állandó minta egy kifejezés értékét egy állandó értéken teszteli. Az állandó lehet bármilyen állandó kifejezés, például egy konstans, egy deklarált const változó neve vagy enumerálási állandó. Ha a bemeneti érték nem nyílt típus, az állandó kifejezés implicit módon konvertálódik a egyeztetett kifejezés típusára; ha a bemeneti érték típusa nem mintakompatibilis az állandó kifejezés típusával, a mintaegyeztetési művelet hiba. Ha az egyeztetett állandó kifejezés numerikus érték, a bemeneti érték egy olyan típus, amely System.Numerics.INumberBase<T>öröklődik, és az állandó kifejezésből a bemeneti érték típusára nincs konstans átalakítás, a mintaegyeztetési művelet hiba.

A relációs mintákhoz hasonló kivételt is hozzáadunk:

Amikor a bemenet egy olyan típus, amelyhez létezik egy megfelelő beépített bináris relációs operátor, és a bemenet a bal oldali operandusként, az adott állandó pedig a jobb oldali operandusként alkalmazandó, akkor az operátor kiértékelését értelmezzük a relációs minta jelentéseként. Ellenkező esetben a bemenetet explicit null értékű vagy nemboxolásos átalakítással alakítjuk át a kifejezés típusára. Fordítási idejű hiba, ha nincs ilyen átalakítás. Fordítási idejű hiba, ha a bemeneti típus egy olyan típusparaméter, amely System.Numerics.INumberBase<T>-ra van korlátozva, vagy egy System.Numerics.INumberBase<T>-típusból örököl, és a bemeneti típusnál nincsen definiálva megfelelő beépített bináris relációs operátor. A mintázat nem tekintendő egyezőnek, ha az átalakítás sikertelen. Ha az átalakítás sikeres, akkor a mintaegyeztetési művelet eredménye az e OP v kifejezés kiértékelésének eredménye, ahol az e az átalakított bemenet, az OP a relációs operátor, a v pedig az állandó kifejezés.

Hátránya

  • A "statikus absztrakció" egy új fogalom, amely értelmesen növeli a C#elméleti terhelését.
  • Nem olcsó funkció megépíteni. Meg kell győződnünk róla, hogy megéri.

Alternatívák

Szerkezeti korlátozások

Egy másik megközelítés az lenne, ha "strukturális korlátozások" lennének közvetlenül, és explicit módon megkövetelnék adott operátorok jelenlétét egy típusparaméteren. Ennek hátrányai: - Ezt minden alkalommal ki kell írni. A névvel ellátott kényszer jobb választásnak tűnik. - Ez egy teljesen új típusú kényszer, míg a javasolt funkció a felületi korlátozások meglévő fogalmát használja. - Ez csak az operátoroknál működik, más típusú statikus tagoknál viszont nem (könnyen).

Megoldatlan kérdések

Statikus absztrakciós felületek és statikus osztályok

További információért tekintse meg a https://github.com/dotnet/csharplang/issues/5783 és a https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes.

Tervezési értekezletek