Statiska abstrakta medlemmar i gränssnitt
Not
Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.
Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. De där skillnaderna dokumenteras i de relevanta -anteckningarna från språkutformningsmöten (LDM).
Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.
Champion-problem: https://github.com/dotnet/csharplang/issues/4436
Sammanfattning
Ett gränssnitt kan ange abstrakta statiska medlemmar som de implementerande klasserna och strukturerna sedan måste tillhandahålla en explicit eller implicit implementering av. Medlemmarna kan nås via typparametrar som är begränsade av gränssnittet.
Motivation
Det finns för närvarande inget sätt att abstrahera över statiska medlemmar och skriva generaliserad kod som gäller för olika typer som definierar dessa statiska medlemmar. Detta är särskilt problematiskt för medlemstyper som endast finns i statisk form, särskilt operatorer.
Den här funktionen tillåter generiska algoritmer över numeriska typer, som representeras av gränssnittsbegränsningar som anger förekomsten av givna operatorer. Algoritmerna kan därför uttryckas i termer av sådana operatorer:
// 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
Gränssnittsmedlemmar
Funktionen skulle göra det möjligt för statiska gränssnittsmedlemmar att deklareras som virtuella.
Regler före C# 11
Före C# 11 var instansmedlemmar i gränssnitt implicit abstrakta (eller virtuella om de har en standardimplementering), men kan valfritt ha en abstract
(eller virtual
) modifierare. Icke-virtuella instansmedlemmar måste uttryckligen markeras som sealed
.
Medlemmar i det statiska gränssnittet är i dag implicit icke-virtuella och tillåter inte abstract
, virtual
eller sealed
modifierare.
Förslag
Abstrakta statiska medlemmar
Andra medlemmar i det statiska gränssnittet än fält tillåts också ha abstract
-modifieraren. Abstrakta statiska medlemmar får inte ha en brödtext (eller om det gäller egenskaper får inte åtkomsttagarna ha en brödtext).
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);
}
Virtuella statiska medlemmar
Andra medlemmar i det statiska gränssnittet än fält tillåts också ha virtual
-modifieraren. Virtuella statiska medlemmar måste ha en kropp.
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(); }
}
Explicit icke-virtuella statiska medlemmar
För symmetri med icke-virtuella instansmedlemmar bör statiska medlemmar (utom fält) tillåtas en valfri sealed
modifierare, även om de inte är virtuella som standard:
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;
}
Implementering av gränssnittsmedlemmar
Dagens regler
Klasser och structs kan implementera abstrakta instansmedlemmar i gränssnitt antingen implicit eller explicit. En implicit implementerad gränssnittsmedlem är en normal (virtuell eller icke-virtuell) medlemsdeklaration för klassen eller strukturen som råkar även implementera gränssnittsmedlemmen. Medlemmen kan till och med ärvas från en basklass och därför inte ens finnas i klassdeklarationen.
En explicit implementerad gränssnittsmedlem använder ett kvalificerat namn för att identifiera den aktuella gränssnittsmedlemmen. Implementeringen är inte direkt tillgänglig som medlem i klassen eller structen, utan endast via gränssnittet.
Förslag
Ingen ny syntax krävs i klasser och structs för att underlätta implicit implementering av statiska abstrakta gränssnittsmedlemmar. Befintliga statiska medlemsdeklarationer tjänar detta syfte.
Explicita implementeringar av statiska abstrakta gränssnittsmedlemmar använder ett kvalificerat namn tillsammans med static
modifieraren.
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;
}
Semantik
Operatorbegränsningar
I dag har alla unary- och binära operatordeklarationer vissa krav som innebär att minst en av deras operander ska vara av typen T
eller T?
, där T
är instanstypen för den omslutande typen.
Dessa krav måste lättas så att en begränsad operand tillåts vara av en typparameter som betraktas som "instanstypen för den omslutande typen".
För att en typparameter T
räknas som "instanstypen för den omslutande typen" måste den uppfylla följande krav:
-
T
är en parameter av direkttyp i gränssnittet där operatordeklarationen inträffar, och -
T
är direkt styrd av vad specifikationen kallar "instanstyp" – dvs. det omgivande gränssnittet med sina egna typparametrar som används som typargument.
Likhetsoperatorer och konverteringar
Abstrakta/virtuella deklarationer av ==
- och !=
-operatorer samt abstrakta/virtuella deklarationer av implicita och explicita konverteringsoperatorer tillåts i gränssnitt. Härledda gränssnitt kommer också att kunna implementera dem.
För operatorerna ==
och !=
måste minst en parametertyp vara en typparameter som räknas som "instanstypen för den omslutande typen", enligt definitionen i föregående avsnitt.
Implementera statiska abstrakta medlemmar
Reglerna för när en statisk medlemsdeklaration i en klass eller struct anses implementera en statisk abstrakt gränssnittsmedlem och för vilka krav som gäller när den gör det, är desamma som för instansmedlemmar.
TBD: Det kan finnas ytterligare eller andra regler som vi ännu inte har tänkt på.
Gränssnitt som typargument
Vi diskuterade problemet som togs upp av https://github.com/dotnet/csharplang/issues/5955 och bestämde oss för att lägga till en begränsning kring användningen av ett gränssnitt som ett typargument (https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts). Här är begränsningen som den föreslogs av https://github.com/dotnet/csharplang/issues/5955 och godkändes av LDM.
Ett gränssnitt som innehåller eller ärver en statisk abstrakt/virtuell medlem som inte har den mest specifika implementeringen i gränssnittet kan inte användas som ett typargument. Om alla statiska abstrakta/virtuella medlemmar har den mest specifika implementeringen kan gränssnittet användas som ett typargument.
Åtkomst till statiska abstrakta gränssnittsmedlemmar
En statisk abstrakt gränssnittsmedlem M
kan nås på en typparameter T
med uttrycket T.M
när T
begränsas av ett gränssnitt I
och M
är en tillgänglig statisk abstrakt medlem i I
.
T M<T>() where T : I<T>
{
T.M();
T t = T.P;
T.E += () => { };
return t + T.P;
}
Vid körning används den medlemsimplementering som finns på den typ som anges som typargument.
C c = M<C>(); // The static members of C get called
Eftersom frågeuttryck anges som en syntaktisk omskrivning låter C# dig faktiskt använda en typ som frågekälla, så länge den har statiska medlemmar för de frågeoperatorer som du använder! Med andra ord, om syntaxen passar, godkänner vi det! Vi tror inte att det här beteendet var avsiktligt eller viktigt i den ursprungliga LINQ och vi vill inte utföra arbetet för att stödja det på typparametrar. Om det finns scenarier där ute kommer vi att höra om dem och kan välja att omfamna detta senare.
Avvikelsesäkerhet §18.2.3.2
Avvikelsesäkerhetsregler bör gälla för signaturer för statiska abstrakta medlemmar. Det tillägg som föreslås i https://github.com/dotnet/csharplang/blob/main/proposals/variance-safety-for-static-interface-members.md#variance-safety bör justeras från
Dessa begränsningar gäller inte för förekomster av typer i deklarationer av statiska medlemmar.
till
Dessa begränsningar gäller inte för förekomster av typer i deklarationer av icke-virtuella, icke-abstrakta statiska medlemmar.
§10.5.4 Användardefinierade implicita konverteringar
Följande punkter
- Fastställ typerna
S
,S₀
ochT₀
.- Om
E
har en typ ska du låtaS
vara den typen. - Om
S
ellerT
är nullbara värdetyper kan du låtaSᵢ
ochTᵢ
vara deras underliggande typer, annars låter duSᵢ
ochTᵢ
varaS
respektiveT
. - Om
Sᵢ
ellerTᵢ
är typparametrar ska du låtaS₀
ochT₀
vara deras effektiva basklasser, annars låter duS₀
ochT₀
varaSₓ
respektiveTᵢ
.
- Om
- Leta reda på vilken uppsättning typer,
D
, som användardefinierade konverteringsoperatorer kommer att övervägas från. Den här uppsättningen består avS0
(omS0
är en klass eller struct), basklasserna förS0
(omS0
är en klass) ochT0
(omT0
är en klass eller struct). - Hitta uppsättningen med tillämpliga användardefinierade och lyfta konverteringsoperatorer,
U
. Denna uppsättning består av användardefinierade och upphöjda implicita konverteringsoperatorer som deklarerats av klasserna eller strukturerna iD
som konverterar från en typ som omfattarS
till en typ som omfattas avT
. OmU
är tom är konverteringen odefinierad och ett kompileringsfel inträffar.
justeras enligt följande:
- Fastställ typerna
S
,S₀
ochT₀
.- Om
E
har en typ ska du låtaS
vara den typen. - Om
S
ellerT
är nullbara värdetyper kan du låtaSᵢ
ochTᵢ
vara deras underliggande typer, annars låter duSᵢ
ochTᵢ
varaS
respektiveT
. - Om
Sᵢ
ellerTᵢ
är typparametrar ska du låtaS₀
ochT₀
vara deras effektiva basklasser, annars låter duS₀
ochT₀
varaSₓ
respektiveTᵢ
.
- Om
- Hitta uppsättningen med tillämpliga användardefinierade och lyfta konverteringsoperatorer,
U
.- Leta reda på vilken uppsättning typer,
D1
, som användardefinierade konverteringsoperatorer kommer att övervägas från. Den här uppsättningen består avS0
(omS0
är en klass eller struct), basklasserna förS0
(omS0
är en klass) ochT0
(omT0
är en klass eller struct). - Hitta uppsättningen med tillämpliga användardefinierade och lyfta konverteringsoperatorer,
U1
. Denna uppsättning består av användardefinierade och upphöjda implicita konverteringsoperatorer som deklarerats av klasserna eller strukturerna iD1
som konverterar från en typ som omfattarS
till en typ som omfattas avT
. - Om
U1
inte är tom ärU
U1
. Annars- Leta reda på vilken uppsättning typer,
D2
, som användardefinierade konverteringsoperatorer kommer att övervägas från. Den här uppsättningen består avSᵢ
effektiv gränssnittsuppsättning och deras basgränssnitt (omSᵢ
är en typparameter) ochTᵢ
effektiv gränssnittsuppsättning (omTᵢ
är en typparameter). - Hitta uppsättningen med tillämpliga användardefinierade och lyfta konverteringsoperatorer,
U2
. Den här uppsättningen består av de användardefinierade och lyfte implicita konverteringsoperatorerna som deklareras av gränssnitten iD2
som konverterar från en typ som omfattarS
till en typ som omfattas avT
. - Om
U2
inte är tom ärU
U2
- Leta reda på vilken uppsättning typer,
- Leta reda på vilken uppsättning typer,
- Om
U
är tom är konverteringen odefinierad och ett kompileringsfel inträffar.
§10.3.9 Användardefinierade explicita konverteringar
Följande punkter
- Fastställ typerna
S
,S₀
ochT₀
.- Om
E
har en typ ska du låtaS
vara den typen. - Om
S
ellerT
är nullbara värdetyper kan du låtaSᵢ
ochTᵢ
vara deras underliggande typer, annars låter duSᵢ
ochTᵢ
varaS
respektiveT
. - Om
Sᵢ
ellerTᵢ
är typparametrar ska du låtaS₀
ochT₀
vara deras effektiva basklasser, annars låter duS₀
ochT₀
varaSᵢ
respektiveTᵢ
.
- Om
- Leta reda på vilken uppsättning typer,
D
, som användardefinierade konverteringsoperatorer kommer att övervägas från. Den här uppsättningen består avS0
(omS0
är en klass eller struct), basklasserna förS0
(omS0
är en klass),T0
(omT0
är en klass eller struct) och basklasserna förT0
(omT0
är en klass). - Hitta uppsättningen med tillämpliga användardefinierade och lyfta konverteringsoperatorer,
U
. Den här uppsättningen består av användardefinierade och lyfta implicita eller explicita konverteringsoperatorer som deklarerats av klasserna eller strukturerna iD
som konverterar från en typ som omfattar eller omfattas avS
till en typ som omfattar eller omfattas avT
. OmU
är tom är konverteringen odefinierad och ett kompileringsfel inträffar.
justeras enligt följande:
- Fastställ typerna
S
,S₀
ochT₀
.- Om
E
har en typ ska du låtaS
vara den typen. - Om
S
ellerT
är nullbara värdetyper kan du låtaSᵢ
ochTᵢ
vara deras underliggande typer, annars låter duSᵢ
ochTᵢ
varaS
respektiveT
. - Om
Sᵢ
ellerTᵢ
är typparametrar ska du låtaS₀
ochT₀
vara deras effektiva basklasser, annars låter duS₀
ochT₀
varaSᵢ
respektiveTᵢ
.
- Om
- Hitta uppsättningen med tillämpliga användardefinierade och lyfta konverteringsoperatorer,
U
.- Leta reda på vilken uppsättning typer,
D1
, som användardefinierade konverteringsoperatorer kommer att övervägas från. Den här uppsättningen består avS0
(omS0
är en klass eller struct), basklasserna förS0
(omS0
är en klass),T0
(omT0
är en klass eller struct) och basklasserna förT0
(omT0
är en klass). - Hitta uppsättningen med tillämpliga användardefinierade och lyfta konverteringsoperatorer,
U1
. Den här uppsättningen består av användardefinierade och lyfta implicita eller explicita konverteringsoperatorer som deklarerats av klasserna eller strukturerna iD1
som konverterar från en typ som omfattar eller omfattas avS
till en typ som omfattar eller omfattas avT
. - Om
U1
inte är tom ärU
U1
. Annars- Leta reda på vilken uppsättning typer,
D2
, som användardefinierade konverteringsoperatorer kommer att övervägas från. Den här uppsättningen består avSᵢ
effektiva gränssnittsuppsättningen och deras basgränssnitt (omSᵢ
är en typparameter) ochTᵢ
effektiv gränssnittsuppsättning och deras basgränssnitt (omTᵢ
är en typparameter). - Hitta uppsättningen med tillämpliga användardefinierade och lyfta konverteringsoperatorer,
U2
. Den här uppsättningen består av användardefinierade och utökade implicita eller explicita konverteringsoperatorer, vilka deklareras av gränssnitten iD2
, som konverterar från en typ som antingen omfattar eller omfattas avS
till en typ som antingen omfattar eller omfattas avT
. - Om
U2
inte är tom ärU
U2
- Leta reda på vilken uppsättning typer,
- Leta reda på vilken uppsättning typer,
- Om
U
är tom är konverteringen odefinierad och ett kompileringsfel inträffar.
Standardimplementeringar
En ytterligare funktion i det här förslaget är att tillåta statiska virtuella medlemmar i gränssnitt att ha standardimplementeringar, precis som virtuella/abstrakta instansmedlemmar gör.
En komplikation här är att standardimplementeringar skulle vilja anropa andra statiska virtuella medlemmar virtuellt. Att tillåta att statiska virtuella medlemmar anropas direkt i gränssnittet skulle kräva att en dold typparameter flödar som representerar den "självtyp" som den aktuella statiska metoden verkligen anropades på. Detta verkar komplicerat, dyrt och potentiellt förvirrande.
Vi diskuterade en enklare version som upprätthåller begränsningarna i det aktuella förslaget att statiska virtuella medlemmar bara kan anropas på typparametrar. Eftersom gränssnitt med statiska virtuella medlemmar ofta har en explicit typparameter som representerar en "själv"-typ, skulle detta inte vara en stor förlust: andra statiska virtuella medlemmar kan bara anropas på den självtypen. Den här versionen är mycket enklare och verkar ganska genomförbar.
På https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md#default-implementations-of-abstract-statics bestämde vi oss för att stödja standardimplementeringar av statiska medlemmar som följer/utökar de regler som fastställs i https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/default-interface-methods.md i enlighet med detta.
Mönsterjämförelse
Med följande kod kan en användare rimligen förvänta sig att koden skriver ut "True" (som den skulle göra om det konstanta mönstret var skrivet 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));
}
Men eftersom indatatypen för mönstret inte är double
kommer det konstanta 1
-mönstret först att kontrollera inkommande T
mot int
. Detta är ointuitivt, så det blockeras tills en framtida C#-version lägger till bättre hantering för numerisk matchning mot typer som härleds från INumberBase<T>
. För att göra det säger vi att vi uttryckligen identifierar INumberBase<T>
som den typ som alla "tal" kommer att härledas från och blockerar mönstret om vi försöker matcha ett numeriskt konstant mönster mot en nummertyp som vi inte kan representera mönstret i (dvs. en typparameter som är begränsad till INumberBase<T>
eller en användardefinierad nummertyp som ärver från INumberBase<T>
).
Formellt lägger vi till ett undantag till definitionen av mönsterkompatibla för konstanta mönster:
Ett konstant mönster testar värdet för ett uttryck mot ett konstant värde. Konstanten kan vara ett konstant uttryck, till exempel en literal, namnet på en deklarerad
const
variabel eller en uppräkningskonstant. När indatavärdet inte är en öppen typ konverteras konstantuttrycket implicit till typen för det matchade uttrycket. Om typen av indatavärde inte är mönsterkompatibel med typen av konstant uttryck är mönstermatchningsåtgärden ett fel. Om det konstanta uttrycket som matchas mot är ett numeriskt värde är indatavärdet en typ som ärver frånSystem.Numerics.INumberBase<T>
, och det inte finns någon konstant konvertering från det konstanta uttrycket till typen av indatavärde, är mönstermatchningsåtgärden ett fel.
Vi lägger också till ett liknande undantag för relationsmönster:
När indata är en typ för vilken en lämplig inbyggd binär relationsoperator definieras som är tillämplig med indata som dess vänstra operande och den angivna konstanten som dess högra operande, tas utvärderingen av operatorn som innebörden av relationsmönstret. Annars konverterar vi indata till typen av uttryck med hjälp av en explicit nullbar konvertering eller avboxningskonvertering. Det är ett kompileringsfel om det inte finns någon sådan konvertering. Det är ett kompileringsfel om indatatypen är en typparameter som är begränsad till eller en typ som ärver från
System.Numerics.INumberBase<T>
och indatatypen inte har någon lämplig inbyggd binär relationsoperator definierad. Mönstret anses inte matcha om konverteringen misslyckas. Om konverteringen lyckas är resultatet av mönstermatchningsåtgärden resultatet av utvärderingen av uttrycket e OP v där e är de konverterade indata, OP är relationsoperatorn och v är det konstanta uttrycket.
Nackdelar
- "statisk abstrakt" är ett nytt koncept och kommer att på ett meningsfullt sätt öka den konceptuella komplexiteten i C#.
- Det är inte en billig funktion att bygga. Vi borde se till att det är värt det.
Alternativ
Strukturella begränsningar
En annan metod skulle vara att ha "strukturella begränsningar" direkt och uttryckligen kräva förekomsten av specifika operatorer för en typparameter. Nackdelarna med det är: - Detta måste skrivas ut varje gång. Att ha en namngiven begränsning verkar bättre. - Detta är en helt ny typ av begränsning, medan den föreslagna funktionen använder det befintliga begreppet gränssnittsbegränsningar. - Det skulle bara fungera för operatörer, men inte så lätt för andra typer av statiska medlemmar.
Olösta frågor
Statiska abstrakta gränssnitt och statiska klasser
Mer information finns i https://github.com/dotnet/csharplang/issues/5783 och https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes.
Designa möten
- 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