Covariant återvändanden
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. Dessa skillnader samlas in i de relevanta anteckningarna från Language Design Meeting (LDM) .
Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.
Champion-fråga: https://github.com/dotnet/csharplang/issues/49
Sammanfattning
Stöd för kovarianska returtyper. Tillåt specifikt åsidosättning av en metod för att deklarera en mer härledd returtyp än den metod den åsidosätter, och på liknande sätt tillåt åsidosättning av en skrivskyddad egenskap för att deklarera en mer härledd typ. Åsidosättningsdeklarationer som visas i fler härledda typer skulle krävas för att tillhandahålla en returtyp som är minst lika specifik som den som visas i åsidosättningar i dess bastyper. Metodens eller egenskapens anropare skulle i en statisk kontext ta emot den mer förfinade returtypen från ett anrop.
Motivation
Det är ett vanligt mönster i kod att olika metodnamn måste uppfinnas för att kringgå språkbegränsningen där åsidosättningar måste returnera samma typ som den metod som åsidosätts.
Detta skulle vara användbart i fabriksmönstret. I Roslyn-kodbasen skulle vi till exempel ha
class Compilation ...
{
public virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
public override CSharpCompilation WithOptions(Options options)...
}
Detaljerad design
Det här är en specifikation för kovarianska returtyper i C#. Vår avsikt är att tillåta att en metods åsidosättning returnerar en mer härledd returtyp än den metod den ersätter och att på samma sätt tillåta åsidosättning av en skrivskyddad egenskap för att returnera en mer härledd returtyp. Anropare av metoden eller egenskapen skulle få den mer förfinade returtypen statiskt från ett anrop, och åsidosättningar som förekommer i mer härledda typer måste ange en returtyp som är minst lika specifik som den som förekommer i åsidosättningar i dess bastyper.
Åsidosättning av klassmetod
Den befintliga begränsningen för metoder för klassers åsidosättning (§15.6.5)
- Åsidosättningsmetoden och den åsidosatta basmetoden har samma returtyp.
ändras till
- Åsidosättningsmetoden måste ha en returtyp som kan konverteras genom en identitetskonvertering eller (om metoden har en värderetur – inte en referensretur se §13.1.0.5 implicit referenskonvertering till returtypen för den åsidosatta basmetoden.
Och följande ytterligare krav läggs till i listan:
- Åsidosättningsmetoden måste ha en returtyp som kan konverteras av en identitetskonvertering eller (om metoden har en värderetur – inte en referensretur, §13.1.0.5) implicit referenskonvertering till returtypen för varje åsidosättning av den åsidosatta basmetoden som deklareras i en (direkt eller indirekt) bastyp för åsidosättningsmetoden.
- Åsidosättningsmetodens returtyp måste vara minst lika åtkomlig som åsidosättningsmetoden (Åtkomstområden – §7.5.3).
Den här begränsningen tillåter att en åsidosättningsmetod i en private
-klass har en private
returtyp. Det krävs dock en public
åsidosättningsmetod i en public
typ för att ha en public
returtyp.
Åsidosättning av klassegenskap och indexerare
De befintliga begränsningarna på klassers åsidosättningsegenskaper (§15.7.6)
En åsidosättande egenskapsdeklaration ska ange exakt samma åtkomstmodifierare och namn som den ärvda egenskapen, och det ska finnas en identitetskonvertering
mellan typen av den åsidosättande och den ärvda egenskapen. Om den ärvda egenskapen bara har en enda accessor (dvs. om den ärvda egenskapen är skrivskyddad eller skrivbaserad) ska den åsidosättande egenskapen endast innehålla den accessorn. Om den ärvda egenskapen innehåller båda accessorerna (dvs. om den ärvda egenskapen är läs- och skrivbar) kan den överordnade egenskapen innehålla antingen en enskild accessor eller båda accessorerna.
ändras till
En åsidosättande egenskapsdeklaration ska ange exakt samma åtkomsmodifierare och namn som den ärvda egenskapen, och det ska finnas en identitetsomvandling eller (om den ärvda egenskapen är skrivskyddad och har en värdetillbakagivning - inte en ref return§13.1.0.5) implicit referensomvandling från typen av den åsidosättande egenskapen till typen av den ärvda egenskapen. Om den ärvda egenskapen bara har en enda accessor (dvs. om den ärvda egenskapen är skrivskyddad eller skrivbaserad) ska den åsidosättande egenskapen endast innehålla den accessorn. Om den ärvda egenskapen innehåller båda accessorerna (dvs. om den ärvda egenskapen är läs- och skrivbar) kan den överordnade egenskapen innehålla antingen en enskild accessor eller båda accessorerna. Den åsidosättande egenskapens typ måste vara minst lika tillgänglig som den åsidosättande egenskapen (Tillgänglighetsdomäner - §7.5.3).
I resten av specifikationsförslaget nedan föreslås en ytterligare utvidgning av covarianta returtyper för gränssnittsmetoder som ska övervägas vid ett senare tillfälle.
Gränssnittsmetod, egenskap och åsidosättning av indexerare
Genom att lägga till de typer av medlemmar som tillåts i ett gränssnitt med tillägg av DIM-funktionen i C# 8.0 lägger vi ytterligare till stöd för override
medlemmar tillsammans med covariant-returer. Dessa följer reglerna för medlemmar i override
som anges för klasser, med följande skillnader:
Följande text finns i klasser:
Metoden som åsidosätts av en åsidosättningsdeklaration kallas åsidosättad basmetod. För en åsidosättningsmetod som
M
deklareras i en klassC
bestäms den åsidosatta basmetoden genom att undersöka varje basklass iC
, från och med den direkta basklassen förC
och fortsätta med varje efterföljande direktbasklass tills minst en tillgänglig metod finns i en viss basklasstyp som har samma signatur somM
efter ersättning av typargument.
får den motsvarande specifikationen för gränssnitt:
Metoden som åsidosätts av en åsidosättningsdeklaration kallas åsidosättad basmetod. För en åsidosättningsmetod
M
som deklareras i ett gränssnittI
bestäms den åsidosatta basmetoden genom att undersöka samtliga direkta och indirekta basgränssnitt tillI
och samla uppsättningen av gränssnitt som deklarerar en åtkomlig metod med samma signatur somM
, med undantag av att typargument ersätts. Om den här uppsättningen gränssnitt har en mest härledda typen, till vilken det finns en identitet eller implicit referenskonvertering från varje typ i den här uppsättningen, och den typen innehåller en unik sådan metoddeklaration, är det den åsidosättande basmetoden.
Vi tillåter på liknande sätt override
egenskaper och indexerare i gränssnitt som anges för klasser i §15.7.6 Virtuella, förseglade, åsidosättnings- och abstrakta accessorer.
Namnsökning
Namnsökning i närvaro av klass-override
-deklarationer ändrar för närvarande resultatet av namnsökningen genom att införa information om den hittade medlemmen från den mest härledda override
-deklarationen i klasshierarkin med början från typen av identifierarens kvalificerare (eller this
när det inte finns någon kvalificerare). Till exempel har vi i §12.6.2.2 Motsvarande parametrar.
För virtuella metoder och indexerare som definierats i klasser väljs parameterlistan från den första deklarationen eller åsidosättningen av funktionsmedlemmen som hittades när den började med mottagarens statiska typ och söker igenom dess basklasser.
till detta lägger vi till
För virtuella metoder och indexerare som definierats i gränssnitt väljs parameterlistan från deklarationen eller åsidosättningen av funktionsmedlemmen som finns i den mest härledda typen bland de typer som innehåller deklarationen av åsidosättning av funktionsmedlemmen. Det är ett kompileringsfel om det inte finns någon unik sådan typ.
Den nuvarande texten för en egenskaps eller indexerares resultattyp
- Om
I
identifierar en instansegenskap blir resultatet en egenskapsåtkomst med ett associerat instansuttryck avE
och en associerad typ som är egenskapens typ. OmT
är en klasstyp väljs den associerade typen från den första deklarationen eller åsidosättningen av egenskapen som hittades när du började medT
och söker igenom dess basklasser.
utökas med
Om
T
är en gränssnittstyp, väljs den associerade typen baserat på deklarationen eller åsidosättningen av egenskapen som finns i det mest specialiserade avT
eller dess direkta eller indirekta basgränssnitt. Det är ett kompileringsfel om det inte finns någon unik sådan typ.
En liknande ändring bör göras i §12.8.12.3 Indexer-åtkomst
I §12.8.10 Anropsuttryck utökar vi den befintliga texten
- Annars är resultatet ett värde med en associerad typ av returtypen för metoden eller det delegerade. Om anropet är av en instansmetod och mottagaren är av en klasstyp
T
, väljs den associerade typen från den första deklarationen eller åsidosättningen av metoden som hittades när du började medT
och söker igenom dess basklasser.
med
Om anropet är av en instansmetod och mottagaren är av en gränssnittstyp
T
, väljs den associerade typen från deklarationen eller åsidosättningen av metoden som finns i det mest härledda gränssnittet blandT
och dess direkta och indirekta basgränssnitt. Det är ett kompileringsfel om det inte finns någon unik sådan typ.
Implementeringar av implicit gränssnitt
Det här avsnittet av specifikationen
För gränssnittsmappning matchar en klassmedlem
A
en gränssnittsmedlemB
när:
A
ochB
är metoder och namn, typ och formella parameterlistor förA
ochB
är identiska.A
ochB
är egenskaper och namnet och typen avA
ochB
är identiska, ochA
har samma accessorer somB
(A
tillåts ha ytterligare accessorer om det inte är en explicit medlemimplementering av gränssnitt).A
ochB
är händelser, och namnet och typen avA
ochB
är identiska.A
ochB
är indexerare är typ- och formella parameterlistorna förA
ochB
identiska ochA
har samma åtkomst somB
(A
tillåts ha ytterligare åtkomst om det inte är en explicit implementering av gränssnittsmedlemmar).
ändras på följande sätt:
För gränssnittsmappning matchar en klassmedlem
A
en gränssnittsmedlemB
när:
A
ochB
är metoder, och namn- och formella parameterlistorna förA
ochB
är identiska, och returtypen förA
kan konverteras till returtypen förB
via en identitet för implicit referenskonvertering till returtypen förB
.A
ochB
är egenskaper, namnen påA
ochB
är identiska,A
har samma åtkomstmetoder somB
(A
tillåts ha ytterligare åtkomstmetoder om det inte är en explicit implementering av en gränssnittsmedlem), och typen avA
kan konverteras till returtypen förB
via en identitetskonvertering eller, omA
är en skrivskyddad egenskap, en implicit referenskonvertering.A
ochB
är händelser, och namnet och typen avA
ochB
är identiska.A
ochB
är indexerare är de formella parameterlistorna förA
ochB
identiska,A
har samma åtkomst somB
(A
tillåts ha ytterligare åtkomst om det inte är en explicit implementering av gränssnittsmedlemmar) och typen avA
kan konverteras till returtypen förB
via en identitetskonvertering eller, omA
är en skrivskyddad indexerare, en implicit referenskonvertering.
Detta är tekniskt sett en brytande ändring eftersom programmet nedan skriver ut "C1.M" idag, men skulle ge "C2.M" enligt den föreslagna revisionen.
using System;
interface I1 { object M(); }
class C1 : I1 { public object M() { return "C1.M"; } }
class C2 : C1, I1 { public new string M() { return "C2.M"; } }
class Program
{
static void Main()
{
I1 i = new C2();
Console.WriteLine(i.M());
}
}
På grund av denna störande ändring kan vi överväga att inte stödja kovarianta returtyper för implicita implementeringar.
Begränsningar för gränssnittsimplementering
Vi behöver en regel om att en explicit gränssnittsimplementering måste deklarera en returtyp som inte är mindre härledd än returtypen som deklareras i någon åsidosättning i dess basgränssnitt.
API-kompatibilitetskonsekvenser
TBD-
Öppna problem
Specifikationen säger inte hur anroparen får den mer raffinerade returtypen. Förmodligen skulle det göras på ett sätt som liknar det sätt som anropare får den mest härledda åsidosättningens parameterspecifikationer.
Om vi har följande gränssnitt:
interface I1 { I1 M(); }
interface I2 { I2 M(); }
interface I3: I1, I2 { override I3 M(); }
Observera att i I3
har metoderna I1.M()
och I2.M()
"sammanfogats". När du implementerar I3
är det nödvändigt att implementera dem båda tillsammans.
I allmänhet kräver vi en explicit implementering för att referera till den ursprungliga metoden. Frågan är i en klass
class C : I1, I2, I3
{
C IN.M();
}
Vad betyder det här? Vad ska N vara?
Jag föreslår att vi tillåter genomförande av antingen I1.M
eller I2.M
(men inte båda) och behandlar det som ett genomförande av båda.
Nackdelar
- Varje språkändring måste betala sig själv.
- [ ] Vi bör se till att prestandan är rimlig, även när det gäller djupa arvshierarkier
- [ ] Vi bör se till att artefakter i översättningsstrategin inte påverkar språksemantik, även när nya IL används från gamla kompilatorer.
Alternativ
Vi skulle kunna lätta på språkreglerna något för att tillåta i originaltexten,
// Possible alternative. This was not implemented.
abstract class Cloneable
{
public abstract Cloneable Clone();
}
class Digit : Cloneable
{
public override Cloneable Clone()
{
return this.Clone();
}
public new Digit Clone() // Error: 'Digit' already defines a member called 'Clone' with the same parameter types
{
return this;
}
}
Olösta frågor
- [ ] Hur fungerar API:er som har kompilerats för att använda den här funktionen i äldre versioner av språket?
Designa möten
- viss diskussion på https://github.com/dotnet/roslyn/issues/357.
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-08.md
- Offlinediskussion mot ett beslut om att endast stödja åsidosättande av klassmetoder i C# 9.0.
C# feature specifications