Covariant resultaat
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.
Kampioenprobleem: https://github.com/dotnet/csharplang/issues/49
Samenvatting
Ondersteuning voor covariant retourtypen. Het is met name mogelijk om het overschrijven van een methode toe te staan om een meer afgeleid retourtype te declareren dan de methode die wordt overschreven, en om het overschrijven van een alleen-lezen eigenschap toe te staan om een meer afgeleid type te declareren. Overschrijvingsdeclaraties die in meer afgeleide typen optreden, moeten een retourtype opgeven dat ten minste zo specifiek is als dat in overschrijvingen in de basistypen voorkomt. Aanroepers van de methode of eigenschap ontvangen statisch het meer verfijnde retourtype van een oproep.
Motivatie
Het is een veelvoorkomend patroon in code dat verschillende methodes moeten worden bedacht om de beperking in de programmeertaal te omzeilen die vereist dat overrides hetzelfde type retourneren als de overschreven methode.
Dit is handig in het fabriekspatroon. In de Roslyn-codebasis hebben we bijvoorbeeld
class Compilation ...
{
public virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
public override CSharpCompilation WithOptions(Options options)...
}
Gedetailleerd ontwerp
Dit is een specificatie voor covariant retourtypen in C#. Onze bedoeling is om het overschrijven van een methode toe te staan zodat deze een meer afgeleid retourtype retourneert dan de methode die het overschrijft, en om het overschrijven van een alleen-lezen eigenschap toe te staan zodat deze een meer afgeleid retourtype retourneert. Aanroepers van de methode of eigenschap ontvangen statisch het meer verfijnde retourtype van een aanroep, en overrides die voorkomen in meer afgeleide typen moeten een retourtype bieden dat ten minste zo specifiek is als dat in de overrides van hun basistypen.
Klassemethode overschrijven
De bestaande beperking op het overschrijven van klasses (§15.6.5 methoden)
- De override-methode en de overschreven basismethode hebben hetzelfde retourtype.
is gewijzigd in
- De override-methode moet een retourtype hebben dat converteerbaar is door een identiteitsconversie of (als de methode een waarde retourneert - niet een ref-return geeft zie §13.1.0.5) impliciete verwijzingsconversie naar het retourtype van de overschreven basismethode.
En de volgende aanvullende vereisten worden toegevoegd aan die lijst:
- De overschrijvingsmethode moet een retourtype hebben dat converteerbaar is door een identiteitsconversie, of (als de methode een waarde retourneert - geen ref return, §13.1.0.5) impliciete referentieconversie naar het retourtype van elke overschrijving van de overschreden basismethode, die is gedeclareerd in een (direct of indirect) basistype van de overschrijvingsmethode.
- Het retourtype van de overridemethode moet minstens zo toegankelijk zijn als de overridemethode (Toegankelijkheidsdomeinen - §7.5.3).
Met deze beperking wordt toegestaan dat een overridemethode in een private
-klasse het retourtype private
heeft. Er is echter een public
overschrijvingsmethode vereist in een public
type om een public
retourtype te hebben.
Klasse-eigenschap en indexeerfunctie overschrijven
De bestaande beperking voor het overschrijven van klasse-eigenschappen (§15.7.6)
Een overschrijvingsverklaring van eigenschappen moet exact dezelfde toegankelijkheidsmodifiers en naam aangeven als de geërfde eigenschap, en er moet een identiteitsconversie
zijn tussen het type van de overschrijvings- en de geërfde eigenschap. Als de overgenomen eigenschap slechts één toegangsfunctie heeft (bijvoorbeeld als de overgenomen eigenschap alleen-lezen of alleen-schrijven is), bevat de overschrijvende eigenschap alleen die toegangsfunctie. Als de overgenomen eigenschap beide accessors bevat (bijvoorbeeld als de overgenomen eigenschap lezen/schrijven is), kan de overschrijvende eigenschap één accessor of beide accessors bevatten.
is gewijzigd in
Een eigenschapsoverschrijvingsverklaring moet exact dezelfde toegankelijkheidsmodifiers en naam specificeren als de overgenomen eigenschap, en er moet een identiteitsconversie of (als de overgenomen eigenschap alleen-lezen is en een waarde retourneert - geen ref return§13.1.0.5) impliciete verwijzingsconversie zijn van het type van de overschrijvende eigenschap naar het type van de overgenomen eigenschap. Als de overgenomen eigenschap slechts één toegangsfunctie heeft (bijvoorbeeld als de overgenomen eigenschap alleen-lezen of alleen-schrijven is), bevat de overschrijvende eigenschap alleen die toegangsfunctie. Als de overgenomen eigenschap beide accessors bevat (bijvoorbeeld als de overgenomen eigenschap lezen/schrijven is), kan de overschrijvende eigenschap één accessor of beide accessors bevatten. Het type van de overschrijvende eigenschap moet minstens zo toegankelijk zijn als de overschrijvende eigenschap (Toegankelijkheidsdomeinen - §7.5.3).
De rest van de onderstaande ontwerpspecificatie stelt een verdere uitbreiding voor van covariant-retourtypen van interfacemethoden die op een later moment moeten worden overwogen.
Interfacemethode, eigenschap en indexeerfunctie overschrijven
Door de soorten leden uit te breiden die in een interface zijn toegestaan met de toevoeging van de DIM-functie in C# 8.0, voegen we ook ondersteuning toe voor override
-leden, samen met covarante resultaten. Deze regels volgen de regels van override
leden zoals gespecificeerd voor klassen, met de volgende verschillen:
De volgende tekst in klassen:
De methode die door een overschrijvingsdeclaratie wordt overschreven, wordt de overschreven basismethodegenoemd. Voor een override-methode
M
, gedeclareerd in een klasseC
, wordt de overriden basismethode bepaald door elke basisklasse vanC
te onderzoeken, te beginnen met de directe basisklasse vanC
en door te gaan met elke opeenvolgende directe basisklasse, totdat in een bepaald basisklassetype ten minste één toegankelijke methode wordt gevonden die dezelfde handtekening heeft alsM
na substitutie van typeargumenten.
wordt de bijbehorende specificatie voor interfaces gegeven:
De methode die door een overschrijvingsdeclaratie wordt overschreven, wordt de overschreven basismethodegenoemd. Voor een override-methode
M
die is gedeclareerd in een interfaceI
, wordt de overschreven basismethode bepaald door de directe of indirecte basisinterfaces vanI
te onderzoeken en de set interfaces te verzamelen die een toegankelijke methode declareren met dezelfde methodehandtekening alsM
na vervanging van typeargumenten. Als deze set interfaces een meest afgeleide typeheeft, waaruit een identiteit of impliciete verwijzingsconversie voor elk type in deze set mogelijk is en dat type een unieke methodeverklaring van dit soort bevat, dan is dat de overschreven basismethode.
We geven ook override
eigenschappen en indexeerfuncties toe in interfaces zoals opgegeven voor klassen in §15.7.6 Virtual, verzegeld, overschrijven en abstracte accessors.
Naam opzoeken
Naamopzoeking in de aanwezigheid van klasse override
-declaraties verandert op dit moment het resultaat van de naamopzoeking door details van het gevonden lid op te leggen vanuit de meest specifieke override
-declaratie in de klassehiërarchie, beginnend vanaf het type van de kwalificatie van de identificator (of this
wanneer er geen kwalificatie is). Bijvoorbeeld, in §12.6.2.2 Overeenkomende parameters we hebben
Voor virtuele methoden en indexeerfuncties die zijn gedefinieerd in klassen, wordt de parameterlijst gekozen uit de eerste declaratie of overschrijving van het functielid dat is gevonden bij het starten met het statische type van de ontvanger en het doorzoeken van de basisklassen.
we voegen aan deze toe
Voor virtuele methoden en indexeerfuncties die zijn gedefinieerd in interfaces, wordt de parameterlijst gekozen uit de declaratie of overschrijving van het functielid, gevonden in het meest afgeleide type onder de typen die de declaratie of overschrijving van het functielid bevatten. Het is een compilatiefout als er geen uniek type bestaat.
De bestaande tekst voor het resultaattype van een eigenschap of indexeerfunctie
- Als
I
een exemplaareigenschap identificeert, is het resultaat een eigenschapstoegang met een bijbehorende exemplaarexpressie vanE
en een gekoppeld type dat het type van de eigenschap is. AlsT
een klassetype is, wordt het bijbehorende type gekozen uit de eerste declaratie of overschrijving van de eigenschap die is gevonden wanneer begonnen wordt metT
en door de basisklassen te zoeken.
is uitgebreid met
Als
T
een interfacetype is, wordt het bijbehorende type gekozen uit de declaratie of overschrijving van de eigenschap die is gevonden in de meest afgeleide versie vanT
of de directe of indirecte basisinterfaces. Het is een compilatiefout als er geen uniek type bestaat.
Er moet een soortgelijke wijziging worden aangebracht in §12.8.12.3 Indexertoegang
In §12.8.10 Aanroepexpressies breiden wij de bestaande tekst uit
- Anders is het resultaat een waarde, met een type dat geassocieerd is met het retourtype van de methode of delegate. Als de aanroep van een instantiemethode is en de ontvanger van een klassetype
T
is, wordt het bijbehorende type gekozen uit de eerste declaratie of onderdrukking van de methode die is gevonden bij het starten metT
en het doorzoeken van de basisklassen.
met
Als de aanroep van een instantiemethode is en de ontvanger van een interfacetype
T
is, wordt het bijbehorende type gekozen uit de declaratie of overschrijving van de methode die is gevonden in de meest afgeleide interface vanT
of een van zijn directe en indirecte basisinterfaces. Het is een compilatiefout als er geen uniek type bestaat.
Impliciete interface-implementaties
Deze sectie van de specificatie
Voor interfacetoewijzing komt een klasselid
A
overeen met een interfacelidB
wanneer:
A
enB
zijn methoden, en de naam, het type en de formele parameterlijsten vanA
enB
zijn identiek.A
enB
eigenschappen zijn, de naam en het typeA
enB
identiek zijn, enA
heeft dezelfde toegangsrechten alsB
(A
is toegestaan extra toegangsrechten te hebben als het geen expliciete implementatie van interfacelid is).A
enB
zijn gebeurtenissen en de naam en het typeA
enB
zijn identiek.A
enB
indexeerfuncties zijn, het type en de formele parameterlijsten vanA
enB
identiek zijn enA
dezelfde toegangsrechten heeft alsB
(A
is toegestaan extra toegangsrechten te hebben als het geen expliciete implementatie van interfacelid is).
wordt als volgt gewijzigd:
Voor interfacetoewijzing komt een klasselid
A
overeen met een interfacelidB
wanneer:
A
enB
zijn methoden, en de naam- en formele parameterlijsten vanA
enB
identiek zijn, en het retourtype vanA
is converteerbaar naar het retourtype vanB
via een identiteit van impliciete verwijzingsomzetting naar het retourtype vanB
.A
enB
eigenschappen zijn, zijn de naam vanA
enB
identiek,A
heeft dezelfde toegangsrechten alsB
(A
is toegestaan extra toegangsrechten te hebben als het geen expliciete interfacelid-implementatie is) en het typeA
converteerbaar is naar het retourtypeB
via een identiteitsconversie of, alsA
een leeseigenschap is, een impliciete verwijzingsconversie.A
enB
zijn gebeurtenissen en de naam en het typeA
enB
zijn identiek.A
enB
indexeerfuncties zijn, zijn de formele parameterlijsten vanA
enB
identiek,A
heeft dezelfde toegangsrechten alsB
(A
is toegestaan extra toegangsrechten te hebben als het geen expliciete implementatie van interfacelid is) en het typeA
is converteerbaar naar het retourtypeB
via een identiteitsconversie of, alsA
een alleen-lezen indexeerfunctie is, een impliciete verwijzingsconversie.
Dit is technisch gezien een brekende verandering, omdat in het onderstaande programma vandaag "C1.M" wordt afgedrukt, maar onder de voorgestelde herziening zou "C2.M" worden afgedrukt.
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());
}
}
Vanwege deze onderbrekende wijziging kunnen we overwegen om geen ondersteuning te bieden aan covariante retourtypen voor impliciete implementaties.
Beperkingen voor interface-implementatie
We hebben een regel nodig dat een expliciete interface-implementatie een retourtype moet declareren dat niet minder afgeleid mag zijn dan het retourtype dat is gedeclareerd in elke overschrijving in de basisinterfaces.
Gevolgen voor API-compatibiliteit
Nader te bepalen
Openstaande Problemen
De specificatie zegt niet hoe de beller het meer verfijnde retourtype krijgt. Waarschijnlijk zou dat op vergelijkbare wijze worden gedaan als de manier waarop aanroepers de parameterspecificaties van de meest afgeleide overschrijving verkrijgen.
Als we de volgende interfaces hebben:
interface I1 { I1 M(); }
interface I2 { I2 M(); }
interface I3: I1, I2 { override I3 M(); }
Houd er rekening mee dat in I3
de methoden I1.M()
en I2.M()
zijn samengevoegd. Bij het implementeren van I3
is het noodzakelijk om ze beide samen te implementeren.
Over het algemeen is een expliciete implementatie vereist om te verwijzen naar de oorspronkelijke methode. De vraag is, in een klas
class C : I1, I2, I3
{
C IN.M();
}
Wat betekent dat hier? Wat moet N zijn?
Ik stel voor dat we de uitvoering van I1.M
of I2.M
(maar niet beide) toestaan en dat behandelen als een implementatie van beide.
Nadelen
- Elke taalwijziging moet zichzelf terugverdienen.
- [ ] We moeten ervoor zorgen dat de prestaties redelijk zijn, zelfs in het geval van uitgebreide overnamehiërarchieën
- [] We moeten ervoor zorgen dat artefacten van de vertaalstrategie geen invloed hebben op taalsemantiek, zelfs wanneer nieuwe IL van oude compilers wordt gebruikt.
Alternatieven
We kunnen de taalregels enigszins versoepelen om, in bron, toe te staan,
// 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;
}
}
Niet-opgeloste vragen
- [ ] Hoe werken API's die zijn gecompileerd om deze functie te gebruiken in oudere versies van de taal?
Designvergaderingen
- enige discussie bij https://github.com/dotnet/roslyn/issues/357.
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-08.md
- Offlinediscussie voor een beslissing om het overschrijven van klassemethoden alleen in C# 9.0 te ondersteunen.
C# feature specifications