Dela via


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₀ och T₀.
    • Om E har en typ ska du låta S vara den typen.
    • Om S eller T är nullbara värdetyper kan du låta Sᵢ och Tᵢ vara deras underliggande typer, annars låter du Sᵢ och Tᵢ vara S respektive T.
    • Om Sᵢ eller Tᵢ är typparametrar ska du låta S₀ och T₀ vara deras effektiva basklasser, annars låter du S₀ och T₀ vara Sₓ respektive Tᵢ.
  • 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 av S0 (om S0 är en klass eller struct), basklasserna för S0 (om S0 är en klass) och T0 (om T0 ä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 i D som konverterar från en typ som omfattar S till en typ som omfattas av T. Om U är tom är konverteringen odefinierad och ett kompileringsfel inträffar.

justeras enligt följande:

  • Fastställ typerna S, S₀ och T₀.
    • Om E har en typ ska du låta S vara den typen.
    • Om S eller T är nullbara värdetyper kan du låta Sᵢ och Tᵢ vara deras underliggande typer, annars låter du Sᵢ och Tᵢ vara S respektive T.
    • Om Sᵢ eller Tᵢ är typparametrar ska du låta S₀ och T₀ vara deras effektiva basklasser, annars låter du S₀ och T₀ vara Sₓ respektive Tᵢ.
  • 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 av S0 (om S0 är en klass eller struct), basklasserna för S0 (om S0 är en klass) och T0 (om T0 ä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 i D1 som konverterar från en typ som omfattar S till en typ som omfattas av T.
    • Om U1 inte är tom är UU1. 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 av Sᵢeffektiv gränssnittsuppsättning och deras basgränssnitt (om Sᵢ är en typparameter) och Tᵢeffektiv gränssnittsuppsättning (om Tᵢ ä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 i D2 som konverterar från en typ som omfattar S till en typ som omfattas av T.
      • Om U2 inte är tom är UU2
  • 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₀ och T₀.
    • Om E har en typ ska du låta S vara den typen.
    • Om S eller T är nullbara värdetyper kan du låta Sᵢ och Tᵢ vara deras underliggande typer, annars låter du Sᵢ och Tᵢ vara S respektive T.
    • Om Sᵢ eller Tᵢ är typparametrar ska du låta S₀ och T₀ vara deras effektiva basklasser, annars låter du S₀ och T₀ vara Sᵢ respektive Tᵢ.
  • 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 av S0 (om S0 är en klass eller struct), basklasserna för S0 (om S0 är en klass), T0 (om T0 är en klass eller struct) och basklasserna för T0 (om T0 ä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 i D som konverterar från en typ som omfattar eller omfattas av S till en typ som omfattar eller omfattas av T. Om U är tom är konverteringen odefinierad och ett kompileringsfel inträffar.

justeras enligt följande:

  • Fastställ typerna S, S₀ och T₀.
    • Om E har en typ ska du låta S vara den typen.
    • Om S eller T är nullbara värdetyper kan du låta Sᵢ och Tᵢ vara deras underliggande typer, annars låter du Sᵢ och Tᵢ vara S respektive T.
    • Om Sᵢ eller Tᵢ är typparametrar ska du låta S₀ och T₀ vara deras effektiva basklasser, annars låter du S₀ och T₀ vara Sᵢ respektive Tᵢ.
  • 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 av S0 (om S0 är en klass eller struct), basklasserna för S0 (om S0 är en klass), T0 (om T0 är en klass eller struct) och basklasserna för T0 (om T0 ä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 i D1 som konverterar från en typ som omfattar eller omfattas av S till en typ som omfattar eller omfattas av T.
    • Om U1 inte är tom är UU1. 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 av Sᵢeffektiva gränssnittsuppsättningen och deras basgränssnitt (om Sᵢ är en typparameter) och Tᵢeffektiv gränssnittsuppsättning och deras basgränssnitt (om Tᵢ ä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 i D2, som konverterar från en typ som antingen omfattar eller omfattas av S till en typ som antingen omfattar eller omfattas av T.
      • Om U2 inte är tom är UU2
  • 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.

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 doublekommer 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ån System.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