Statische abstracte leden in interfaces
Notitie
Dit artikel is een functiespecificatie. De specificatie fungeert als het ontwerpdocument voor de functie. Het bevat voorgestelde specificatiewijzigingen, samen met informatie die nodig is tijdens het ontwerp en de ontwikkeling van de functie. Deze artikelen worden gepubliceerd totdat de voorgestelde specificaties zijn voltooid en opgenomen in de huidige ECMA-specificatie.
Er kunnen enkele verschillen zijn tussen de functiespecificatie en de voltooide implementatie. Deze verschillen worden vastgelegd in de relevante notities van de taalontwerpvergadering (LDM).
Meer informatie over het proces voor het aannemen van functiespeclets in de C#-taalstandaard vindt u in het artikel over de specificaties.
Kampioensprobleem: https://github.com/dotnet/csharplang/issues/4436
Samenvatting
Een interface kan abstracte statische leden specificeren, waarvoor de implementerende klassen en structs een expliciete of impliciete implementatie moeten bieden. De leden zijn toegankelijk via typeparameters die worden beperkt door de interface.
Motivatie
Er is momenteel geen manier om statische leden te abstraheren en gegeneraliseerde code te schrijven die van toepassing is op verschillende typen die deze statische leden definiëren. Dit is met name problematisch voor lidtypen die alleen bestaan in een statische vorm, met name operatoren.
Met deze functie kunnen algemene algoritmen worden gebruikt voor numerieke typen, vertegenwoordigd door interfacebeperkingen waarmee de aanwezigheid van bepaalde operators wordt opgegeven. De algoritmen kunnen daarom worden uitgedrukt in termen van dergelijke operators:
// 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 });
Syntaxis
Interfaceleden
Met de functie kunnen statische interfaceleden worden gedeclareerd als virtueel.
Regels vóór C# 11
Vóór C# 11 zijn exemplaarleden in interfaces impliciet abstract (of virtueel als ze een standaard implementatie hebben), maar kunnen ze desgewenst een abstract
(of virtual
) modifier hebben. Niet-virtuele instantieleden moeten expliciet worden gemarkeerd als sealed
.
Statische interfaceleden zijn tegenwoordig impliciet niet-virtueel en staan abstract
, virtual
of sealed
modifiers niet toe.
Voorstel
Abstracte statische leden
Statische interfaceleden behalve velden mogen ook de abstract
modifier hebben. Abstracte statische leden mogen geen inhoud hebben (of in het geval van eigenschappen mogen de accessors geen inhoud hebben).
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);
}
Virtuele statische leden
Statische interfaceleden behalve velden mogen ook de virtual
modifier hebben. Virtuele statische leden moeten een hoofdtekst hebben.
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(); }
}
Expliciet niet virtuele statische leden
Voor symmetrie met niet-virtuele instantieleden moeten statische leden (behalve velden) een optionele sealed
-modifier toegestaan krijgen, ook al zijn ze standaard niet-virtueel.
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;
}
Implementatie van interfaceleden
De regels van vandaag
Klassen en structs kunnen abstracte instantieleden van interfaces impliciet of expliciet implementeren. Een impliciet geïmplementeerd interfacelid is een gewone (virtuele of niet-virtuele) liddeclaratie van de klasse of structuur die toevallig ook het interfacelid implementeert. Het lid kan zelfs worden overgenomen van een basisklasse en dus niet eens aanwezig zijn in de klassedeclaratie.
Een expliciet geïmplementeerd interfacelid gebruikt een gekwalificeerde naam om het betreffende interfacelid te identificeren. De implementatie is niet rechtstreeks toegankelijk als lid van de klasse of struct, maar alleen via de interface.
Voorstel
Er is geen nieuwe syntaxis nodig in klassen en structs om impliciete implementatie van leden van statische abstracte interface te vergemakkelijken. Bestaande statische liddeclaraties dienen hiervoor.
Expliciete implementaties van leden van statische abstracte interface gebruiken een gekwalificeerde naam samen met de static
modifier.
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;
}
Semantiek
Operatorbeperkingen
Tegenwoordig hebben alle declaraties van unaire en binaire operatoren een vereiste waarbij ten minste één van hun operanden van het type T
of T?
is, waarbij T
het instantietype van het insluittype is.
Deze vereisten moeten worden versoepeld, zodat een beperkte operand van een typeparameter mag zijn die als 'het exemplaartype van het insluittype' wordt geteld.
Als u wilt dat een typeparameter T
als 'het exemplaartype van het insluittype' wordt geteld, moet deze voldoen aan de volgende vereisten:
-
T
is een directe typeparameter van de interface waarin de operatordeclaratie plaatsvindt, en -
T
wordt rechtstreeks beperkt door wat de specificatie het 'exemplaartype' noemt, dat wil zeggen de omringende interface met zijn eigen typeparameters die worden gebruikt als typeargumenten.
Gelijkheidsoperators en conversies
Abstracte/virtuele declaraties van ==
- en !=
-operators, evenals abstracte/virtuele declaraties van impliciete en expliciete conversieoperators worden toegestaan in interfaces. Afgeleide interfaces kunnen ze ook implementeren.
Voor ==
- en !=
-operators moet ten minste één parametertype een typeparameter zijn die telt als 'het exemplaartype van het insluittype', zoals gedefinieerd in de vorige sectie.
Statische abstracte leden implementeren
De regels voor wanneer een statische liddeclaratie in een klasse of struct wordt beschouwd als het implementeren van een statisch abstract interfacelid en voor welke vereisten van toepassing zijn wanneer dit gebeurt, zijn hetzelfde als bijvoorbeeld leden.
TBD: Mogelijk zijn er hier aanvullende of andere regels nodig die we nog niet hebben bedacht.
Interfaces als typeargumenten
We hebben het probleem besproken door https://github.com/dotnet/csharplang/issues/5955 en besloten om een beperking toe te voegen rond het gebruik van een interface als een typeargument (https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts). Hier volgt de beperking zoals deze is voorgesteld door https://github.com/dotnet/csharplang/issues/5955 en goedgekeurd door de LDM.
Een interface met een statisch abstract/virtueel lid dat niet de meest specifieke implementatie in de interface heeft, kan niet worden gebruikt als een typeargument. Als alle statische abstracte/virtuele leden de meest specifieke implementatie hebben, kan de interface worden gebruikt als een typeargument.
Toegang tot leden van statische abstracte interface
Een lid van een statische abstracte interface M
kan worden geopend op een typeparameter T
met behulp van de expressie T.M
wanneer T
wordt beperkt door een interface I
en M
een toegankelijk statisch abstract lid van I
is.
T M<T>() where T : I<T>
{
T.M();
T t = T.P;
T.E += () => { };
return t + T.P;
}
Tijdens de uitvoering is de werkelijke implementatie van het lid die wordt gebruikt degene die bestaat op het daadwerkelijke type dat is opgegeven als een typeargument.
C c = M<C>(); // The static members of C get called
Omdat query-expressies worden gespecificeerd als een syntactische herformulering, kunt u met C# een type gebruiken als querybron, zolang deze statische leden bevat voor de queryoperators die u wilt gebruiken. Met andere woorden, als de syntaxis past, laten we het toe! We denken dat dit gedrag niet opzettelijk of belangrijk is in de oorspronkelijke LINQ en we willen het werk niet doen om dit te ondersteunen bij typeparameters. Als er scenario's zijn, horen we hierover en kunnen we ervoor kiezen om dit later te omarmen.
Afwijkingsveiligheid §18.2.3.2
De veiligheidsregels voor afwijkingen moeten van toepassing zijn op handtekeningen van statische abstracte leden. De voorgestelde toevoeging in https://github.com/dotnet/csharplang/blob/main/proposals/variance-safety-for-static-interface-members.md#variance-safety moet worden aangepast van
Deze beperkingen gelden niet voor voorkomens van typen binnen declaraties van statische leden.
Aan
Deze beperkingen zijn niet van toepassing op voorkomens van typen binnen declaraties van niet-virtuele, niet-abstracte statische leden.
§10.5.4 Door de gebruiker gedefinieerde impliciete conversies
De volgende punten
- Bepaal de typen
S
,S₀
enT₀
.- Als
E
een type heeft, laatS
dat type zijn. - Als
S
ofT
null-waardetypen zijn, laatSᵢ
enTᵢ
de onderliggende typen zijn, andersSᵢ
enTᵢ
respectievelijk wordenS
enT
. - Als
Sᵢ
ofTᵢ
typeparameters zijn, laatS₀
enT₀
hun effectieve basisklassen zijn, andersS₀
enT₀
respectievelijkSₓ
enTᵢ
worden.
- Als
- Zoek de set typen,
D
, van waaruit door de gebruiker gedefinieerde conversieoperators worden overwogen. Deze set bestaat uitS0
(alsS0
een klasse of struct is), de basisklassen vanS0
(alsS0
een klasse is) enT0
(alsT0
een klasse of struct is). - Zoek de set relevante gebruikersgedefinieerde en verheven conversieoperators,
U
. Deze set bestaat uit de door de gebruiker gedefinieerde en opgeheven impliciete conversieoperators die zijn gedeclareerd door de klassen of structs inD
die worden omgezet van een type datS
omvat naar een type dat wordt omvat doorT
. AlsU
leeg is, is de conversie niet gedefinieerd en treedt er een compilatiefout op.
worden als volgt aangepast:
- Bepaal de typen
S
,S₀
enT₀
.- Als
E
een type heeft, laatS
dat type zijn. - Als
S
ofT
null-waardetypen zijn, laatSᵢ
enTᵢ
de onderliggende typen zijn, andersSᵢ
enTᵢ
respectievelijk wordenS
enT
. - Als
Sᵢ
ofTᵢ
typeparameters zijn, laatS₀
enT₀
hun effectieve basisklassen zijn, andersS₀
enT₀
respectievelijkSₓ
enTᵢ
worden.
- Als
- Zoek de set relevante gebruikersgedefinieerde en verheven conversieoperators,
U
.- Zoek de set typen,
D1
, van waaruit door de gebruiker gedefinieerde conversieoperators worden overwogen. Deze set bestaat uitS0
(alsS0
een klasse of struct is), de basisklassen vanS0
(alsS0
een klasse is) enT0
(alsT0
een klasse of struct is). - Zoek de set relevante gebruikersgedefinieerde en verheven conversieoperators,
U1
. Deze set bestaat uit de door de gebruiker gedefinieerde en opgeheven impliciete conversieoperators die zijn gedeclareerd door de klassen of structs inD1
die worden omgezet van een type datS
omvat naar een type dat wordt omvat doorT
. - Als
U1
niet leeg is, wordtU
U1
. Anders- Zoek de set typen,
D2
, van waaruit door de gebruiker gedefinieerde conversieoperators worden overwogen. Deze set bestaat uitSᵢ
effectieve interfaceset en de bijbehorende basisinterfaces (alsSᵢ
een typeparameter is) enTᵢ
effectieve interfaceset (alsTᵢ
een typeparameter is). - Zoek de set relevante gebruikersgedefinieerde en verheven conversieoperators,
U2
. Deze set bestaat uit de door de gebruiker gedefinieerde en opgeheven impliciete conversieoperators die zijn gedeclareerd door de interfaces inD2
die worden omgezet van een type datS
omvat naar een type dat wordt omvat doorT
. - Als
U2
niet leeg is, wordtU
U2
- Zoek de set typen,
- Zoek de set typen,
- Als
U
leeg is, is de conversie niet gedefinieerd en treedt er een compilatiefout op.
§10.3.9 door de gebruiker gedefinieerde expliciete conversies
De volgende punten
- Bepaal de typen
S
,S₀
enT₀
.- Als
E
een type heeft, laatS
dat type zijn. - Als
S
ofT
null-waardetypen zijn, laatSᵢ
enTᵢ
de onderliggende typen zijn, andersSᵢ
enTᵢ
respectievelijk wordenS
enT
. - Als
Sᵢ
ofTᵢ
typeparameters zijn, laatS₀
enT₀
hun effectieve basisklassen zijn, andersS₀
enT₀
respectievelijkSᵢ
enTᵢ
worden.
- Als
- Zoek de set typen,
D
, van waaruit door de gebruiker gedefinieerde conversieoperators worden overwogen. Deze set bestaat uitS0
(alsS0
een klasse of struct is), de basisklassen vanS0
(alsS0
een klasse is),T0
(alsT0
een klasse of struct is) en de basisklassen vanT0
(alsT0
een klasse is). - Zoek de set relevante gebruikersgedefinieerde en verheven conversieoperators,
U
. Deze set bestaat uit de door de gebruiker gedefinieerde en opgeheven impliciete of expliciete conversieoperators die zijn gedeclareerd door de klassen of structs inD
die worden omgezet van een type dat doorS
wordt omsloten of omvat door een type dat doorT
wordt opgenomen. AlsU
leeg is, is de conversie niet gedefinieerd en treedt er een compilatiefout op.
worden als volgt aangepast:
- Bepaal de typen
S
,S₀
enT₀
.- Als
E
een type heeft, laatS
dat type zijn. - Als
S
ofT
null-waardetypen zijn, laatSᵢ
enTᵢ
de onderliggende typen zijn, andersSᵢ
enTᵢ
respectievelijk wordenS
enT
. - Als
Sᵢ
ofTᵢ
typeparameters zijn, laatS₀
enT₀
hun effectieve basisklassen zijn, andersS₀
enT₀
respectievelijkSᵢ
enTᵢ
worden.
- Als
- Zoek de set relevante gebruikersgedefinieerde en verheven conversieoperators,
U
.- Zoek de set typen,
D1
, van waaruit door de gebruiker gedefinieerde conversieoperators worden overwogen. Deze set bestaat uitS0
(alsS0
een klasse of struct is), de basisklassen vanS0
(alsS0
een klasse is),T0
(alsT0
een klasse of struct is) en de basisklassen vanT0
(alsT0
een klasse is). - Zoek de set relevante gebruikersgedefinieerde en verheven conversieoperators,
U1
. Deze set bestaat uit de door de gebruiker gedefinieerde en opgeheven impliciete of expliciete conversieoperators die zijn gedeclareerd door de klassen of structs inD1
die worden omgezet van een type dat doorS
wordt omsloten of omvat door een type dat doorT
wordt opgenomen. - Als
U1
niet leeg is, wordtU
U1
. Anders- Zoek de set typen,
D2
, van waaruit door de gebruiker gedefinieerde conversieoperators worden overwogen. Deze set bestaat uitSᵢ
effectieve interfaceset en de bijbehorende basisinterfaces (alsSᵢ
een typeparameter is) enTᵢ
effectieve interfaceset en hun basisinterfaces (alsTᵢ
een typeparameter is). - Zoek de set relevante gebruikersgedefinieerde en verheven conversieoperators,
U2
. Deze set bestaat uit de door de gebruiker gedefinieerde en opgeheven impliciete of expliciete conversieoperators die zijn gedeclareerd door de interfaces inD2
die worden omgezet van een type dat doorS
wordt omvat of omvat naar een type dat doorT
wordt opgenomen. - Als
U2
niet leeg is, wordtU
U2
- Zoek de set typen,
- Zoek de set typen,
- Als
U
leeg is, is de conversie niet gedefinieerd en treedt er een compilatiefout op.
Standaard implementaties
Een extra functie van dit voorstel is om het toe te staan dat statische virtuele leden in interfaces standaardimplementaties hebben, net zoals virtuele/abstracte leden dat doen.
Een complicatie hier is dat standaard implementaties andere statische virtuele leden 'virtueel' willen aanroepen. Het toestaan dat statische virtuele leden rechtstreeks op de interface worden aangeroepen, zou het doorsturen van een verborgen typeparameter vereisen die het 'self'-type vertegenwoordigt waarop de huidige statische methode werkelijk is aangeroepen. Dit lijkt ingewikkeld, duur en mogelijk verwarrend.
We hebben een eenvoudigere versie besproken die de beperkingen van het huidige voorstel handhaaft dat statische virtuele leden alleen kunnen alleen worden aangeroepen voor typeparameters. Omdat interfaces met statische virtuele leden vaak een expliciete typeparameter hebben die een zelftype vertegenwoordigt, zou dit geen groot verlies zijn: andere statische virtuele leden kunnen gewoon op dat zelftype worden aangeroepen. Deze versie is veel eenvoudiger en lijkt best te doen.
Op https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md#default-implementations-of-abstract-statics hebben we besloten om standaard implementaties van statische leden te ondersteunen die de regels volgen/uitbreiden die in https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/default-interface-methods.md dienovereenkomstig zijn ingesteld.
Patroonvergelijking
Gezien de volgende code kan een gebruiker redelijkerwijs verwachten dat deze 'True' afdrukt (net als als het constante patroon inline is geschreven):
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));
}
Omdat het invoertype van het patroon echter niet is double
, controleert het constante 1
patroon eerst de binnenkomende T
tegen int
. Dit is niet intuïtief, dus het wordt geblokkeerd totdat een toekomstige C#-versie betere verwerking voor numerieke overeenkomsten toevoegt aan typen die zijn afgeleid van INumberBase<T>
. Om dit te doen, zullen we zeggen dat we expliciet INumberBase<T>
herkennen als het type waaruit alle 'getallen' worden afgeleid en het patroon blokkeren als we een numeriek constant patroon proberen te vergelijken met een getaltype dat we niet kunnen vertegenwoordigen het patroon in (dat wil zeggen, een typeparameter beperkt tot INumberBase<T>
of een door de gebruiker gedefinieerd getaltype dat overneemt van INumberBase<T>
).
Formeel voegen we een uitzondering toe aan de definitie van patroon compatibele voor constante patronen:
Met een constant patroon wordt de waarde van een expressie getest op basis van een constante waarde. De constante kan elke constante expressie zijn, zoals een letterlijke expressie, de naam van een gedeclareerde
const
variabele of een opsommingsconstante. Wanneer de invoerwaarde geen open type is, wordt de constante expressie impliciet geconverteerd naar het type overeenkomende expressie; als het type invoerwaarde niet patroon-compatibele met het type constante expressie, is de patroonkoppelingsbewerking een fout. Als de constante expressie waarmee wordt vergeleken, een numerieke waarde is, is de invoerwaarde een type dat wordt overgenomen vanSystem.Numerics.INumberBase<T>
en er geen constante conversie van de constante expressie naar het type invoerwaarde is, is de patroonkoppelingsbewerking een fout.
We voegen ook een vergelijkbare uitzondering toe voor relationele patronen:
Wanneer de invoer een type is waarvoor een geschikte ingebouwde binaire relationele operator is gedefinieerd die van toepassing is op de invoer als linkeroperand en de opgegeven constante als de rechteroperand, wordt de evaluatie van die operator beschouwd als de betekenis van het relationele patroon. Anders converteren we de invoer naar het type expressie met behulp van een expliciete nullable of unboxing conversie. Het is een compilatiefout als er geen conversie bestaat. Het is een compilatietijdfout als het invoertype een typeparameter is die is beperkt tot of een type dat wordt overgenomen van
System.Numerics.INumberBase<T>
en het invoertype geen geschikte ingebouwde binaire relationele operator heeft gedefinieerd. Het patroon wordt beschouwd als niet overeenkomend als de conversie mislukt. Als de conversie slaagt, is het resultaat van de patroonkoppelingsbewerking het resultaat van het evalueren van de expressie e OP v waarbij e de geconverteerde invoer is, is OP de relationele operator en v de constante expressie.
Nadelen
- 'statisch abstract' is een nieuw concept en zal zinvol worden toegevoegd aan de conceptuele belasting van C#.
- Het is geen goedkope functie om te bouwen. We moeten ervoor zorgen dat het de moeite waard is.
Alternatieven
Structurele beperkingen
Een alternatieve benadering is om "structurele beperkingen" rechtstreeks te hebben en expliciet de aanwezigheid van specifieke operators op een typeparameter te vereisen. De nadelen hiervan zijn: - Dit zou elke keer moeten worden weggeschreven. Het hebben van een benoemde beperking lijkt beter. - Dit is een geheel nieuw soort beperking, terwijl de voorgestelde functie gebruikmaakt van het bestaande concept van interfacebeperkingen. - Het zou alleen werken voor operators, maar niet (eenvoudig) voor andere soorten statische leden.
Niet-opgeloste vragen
Statische abstracte interfaces en statische klassen
Zie https://github.com/dotnet/csharplang/issues/5783 en https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes voor meer informatie.
Ontwerpvergaderingen
- https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-02-08.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-04-05.md
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-06-29.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-04-06.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-06-06.md
C# feature specifications