Kowariantne typy zwracane
Notatka
Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.
Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są przechwytywane w odpowiednich spotkania projektowego języka (LDM).
Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .
Streszczenie
Obsługa kowariantnych typów zwracanych . W szczególności zezwól na zastąpienie metody w celu zadeklarowania typu zwracanego, który jest bardziej pochodny niż metoda, którą zastępuje, i podobnie na zastąpienie właściwości tylko do odczytu, aby zadeklarować typ, który jest bardziej pochodny. Deklaracje przesłonięcia pojawiające się w bardziej pochodnych typach musiałyby zapewniać typ zwracany co najmniej tak szczegółowy, jak te pojawiające się w przesłonięciach w typach bazowych. Wywołujący metodę lub właściwość będą statycznie otrzymywać bardziej precyzyjny typ zwracany z wywołania.
Motywacja
Jest to powszechny wzorzec w kodzie, w którym trzeba wymyślić różne nazwy metod, aby obejść ograniczenie językowe, że nadpisania muszą zwracać ten sam typ co metoda nadpisywana.
Byłoby to przydatne we wzorcu projektowym fabryki. Na przykład w bazie kodu Roslyn mielibyśmy
class Compilation ...
{
public virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
public override CSharpCompilation WithOptions(Options options)...
}
Szczegółowy projekt
Jest to specyfikacja kowariantnych typów zwracania w C#. Naszym celem jest zezwolenie na zastąpienie metody, aby zwracała bardziej pochodny typ niż metoda, którą zastępuje, i podobnie, aby zezwolić na zastąpienie właściwości tylko do odczytu, aby zwracała bardziej pochodny typ zwracany. Wywołujący metodę lub właściwość statycznie będą otrzymywać bardziej precyzyjny typ zwracany z wywołania, a przesłonięcia pojawiające się w bardziej pochodnych typach muszą zapewnić typ zwracany co najmniej tak szczegółowy jak ten, który pojawia się w przesłonięciach jego typów bazowych.
Przesłonięcie metody klasy
Istniejące ograniczenie na metody przesłania klasy (§15.6.5)
- Przesłaniająca metoda i przesłonięta metoda bazowa mają taki sam typ zwracany.
jest modyfikowany na
- Metoda zastąpienia musi mieć typ zwracany, który jest konwertowany przez konwersję tożsamości lub (jeśli metoda ma zwracaną wartość — a nie zwracać ref zobacz §13.1.1.0.5 niejawnej konwersji odwołania do zwracanego typu metody bazowej zastąpienia.
Następujące dodatkowe wymagania są dołączane do tej listy:
- Metoda zastąpienia musi mieć typ zwracany przez konwersję tożsamości lub (jeśli metoda ma zwracaną wartość — a nie zwracać ref, §13.1.0.5) niejawnej konwersji odwołania do zwracanego typu każdej przesłonięć metody bazowej, która jest zadeklarowana w metodzie bazowej (bezpośredniej lub pośredniej) przesłonięcia.
- Typ zwracany metody zastąpienia musi być co najmniej tak dostępny, jak metoda zastąpienia (domeny ułatwień dostępu — §7.5.3).
To ograniczenie zezwala na zastąpienie metody w klasie private
, aby mieć typ zwracany private
. Jednak wymaga nadpisania metody public
w typie public
, aby zastosować typ zwracany public
.
Właściwość klasy i przeciążenie indeksatora
Istniejące ograniczenie przesłonięcia klasy ( właściwości§15.7.6)
Deklaracja właściwości zastępowania określa dokładnie te same modyfikatory ułatwień dostępu i nazwę co dziedziczona właściwość, a między typem zastępowania a dziedziczonej właściwościmusi istnieć konwersja tożsamości
. Jeśli właściwość dziedziczona ma tylko jedną metodę dostępu (tj. jeśli dziedziczona właściwość jest tylko do odczytu lub tylko do zapisu), właściwość zastępowania obejmuje tylko to akcesorium. Jeśli dziedziczona właściwość zawiera obie metody dostępu (tj. jeśli dziedziczona właściwość jest odczyt-zapis), właściwość zastępowania może zawierać jedno akcesorium lub oba metody dostępu.
jest modyfikowany do
Deklaracja zastępująca właściwość powinna określać dokładnie te same modyfikatory dostępu i nazwę co dziedziczona właściwość, i powinna istnieć konwersja tożsamości lub (jeśli dziedziczona właściwość jest tylko do odczytu i zwraca wartość, a nie odwołanie ref§13.1.0.5) niejawna konwersja odwołania z typu właściwości zastępowanej do typu dziedziczonej właściwości. Jeśli właściwość dziedziczona ma tylko jedną metodę dostępu (tj. jeśli dziedziczona właściwość jest tylko do odczytu lub tylko do zapisu), właściwość zastępowania obejmuje tylko to akcesorium. Jeśli dziedziczona właściwość zawiera obie metody dostępu (tj. jeśli dziedziczona właściwość jest odczyt-zapis), właściwość zastępowania może zawierać jedno akcesorium lub oba metody dostępu. Typ właściwości zastępowania musi być co najmniej tak dostępny, jak właściwość zastępowania (domeny ułatwień dostępu — §7.5.3).
Pozostała część poniższej specyfikacji roboczej proponuje dalsze rozszerzenie kowariantnych zwrotów metod interfejsu, które mają być rozważane później.
Nadpisywanie metody, właściwości i indeksatora interfejsu
Dodając do rodzajów elementów, które mogą występować w interfejsie dzięki dodaniu funkcji DIM w języku C# 8.0, dodatkowo wprowadzamy obsługę elementów override
wraz z kowariantnymi zwrotami. Są zgodne z regułami członków override
określonymi dla klas, z następującymi różnicami:
Następujący tekst w klasach:
Metoda zastępowana przez deklarację przesłonięcia jest znana jako zastąpiona metoda bazowa . W przypadku metody zastąpienia
M
zadeklarowanej w klasieC
metoda bazowa, która została zastąpiona, jest określana przez sprawdzenie każdej klasy bazowejC
, zaczynając od bezpośredniej klasy bazowejC
i kontynuując przez każdą kolejną bezpośrednią klasę bazową, aż zostanie znaleziona co najmniej jedna dostępna metoda o tej samej sygnaturze jakM
po podstawieniu argumentów typów.
jest podana odpowiednia specyfikacja interfejsów:
Metoda zastępowana przez deklarację przesłonięcia jest znana jako przesłonięta metoda bazowa. W przypadku metody zastępującej
M
zadeklarowanej w interfejsieI
, metoda przesłonięta jest ustalana poprzez sprawdzenie każdego bezpośredniego lub pośredniego interfejsu podstawowegoI
, zbierając zestaw interfejsów deklarujących dostępną metodę, która ma ten sam podpis coM
po zastąpieniu argumentów typu. Jeśli ten zestaw interfejsów ma najbardziej pochodny typ, do którego można dokonać konwersji tożsamości lub niejawnej konwersji referencyjnej z każdego typu w tym zestawie, a ten typ zawiera unikatową deklarację takiej metody, to jest zastąpiona metoda bazowa .
Podobnie zezwalamy na override
właściwości i indeksatory w interfejsach określonych dla klas w §15.7.6 Virtual, zapieczętowane, zastępowane i abstrakcyjne metody dostępu.
Wyszukiwanie nazw
Wyszukiwanie nazw w obecności deklaracji klasy override
obecnie modyfikuje wynik, narzucając informacje o znalezionych członkach z najbardziej pochodnej deklaracji override
w hierarchii klas, zaczynając od typu kwalifikatora identyfikatora (lub this
, gdy brak kwalifikatora). Na przykład w §12.6.2.2 mamy Odpowiednie parametry
W przypadku metod wirtualnych i indeksatorów zdefiniowanych w klasach lista parametrów jest wybierana z pierwszej deklaracji lub zastępowania elementu członkowskiego funkcji znalezionego podczas rozpoczynania od statycznego typu odbiornika i przeszukiwania jego klas bazowych.
do tego dodajemy
W przypadku metod wirtualnych i indeksatorów zdefiniowanych w interfejsach lista parametrów jest wybierana z deklaracji lub zastąpienia elementu członkowskiego funkcji znalezionego w najbardziej pochodnym typie wśród tych typów zawierających deklarację zastąpienia elementu członkowskiego funkcji. Jest to błąd czasu kompilacji, jeśli nie istnieje taki unikatowy typ.
W przypadku typu wyniku w dostępie do właściwości lub indeksatora, istniejący tekst jest używany.
- Jeśli
I
identyfikuje właściwość instancji, wówczas wynikiem jest dostęp do tej właściwości z przypisanym wyrażeniem instancjiE
oraz przypisanym typem, który odpowiada typowi właściwości. JeśliT
jest typem klasy, skojarzony typ jest wybierany z pierwszej deklaracji lub zastępowania właściwości znalezionej podczas rozpoczynania odT
i przeszukiwania jej klas bazowych.
jest wzbogacony o
Jeśli
T
jest typem interfejsu, skojarzony typ jest wybierany z deklaracji lub zastępowania właściwości znalezionej w najbardziej pochodnychT
lub jego bezpośrednich lub pośrednich interfejsów podstawowych. Jest to błąd czasu kompilacji, jeśli nie istnieje unikatowy taki typ.
Podobną zmianę należy wprowadzić w §12.8.12.3 Dostęp indeksatora
W §12.8.10 wyrażenia wywołania uzupełniamy istniejący tekst
- W przeciwnym razie wynik jest wartością ze skojarzonym typem zwracanego typu metody lub delegata. Jeśli wywołanie jest metodą instancji, i odbiornik ma typ klasy
T
, skojarzony typ jest wybierany z pierwszej deklaracji lub nadpisania metody znalezionej, zaczynając odT
i przeszukując jej klasy bazowe.
z
Jeśli wywołanie dotyczy metody instancji, a odbiornik jest typu interfejsu
T
, skojarzony typ jest wybierany z deklaracji lub nadpisania metody znalezionej w najbardziej pochodnym interfejsie spośródT
oraz jego bezpośrednich i pośrednich interfejsów podstawowych. Jest to błąd czasu kompilacji, jeśli taki unikalny typ nie istnieje.
Implementacje interfejsu niejawnego
Ta sekcja specyfikacji
Do celów mapowania interfejsu, element
A
klasy jest zgodny z elementemB
interfejsu, gdy:
A
iB
to metody, a listy nazw, typów i parametrów formalnychA
iB
są identyczne.A
iB
są właściwościami, nazwa i typA
iB
są identyczne, aA
ma takie same akcesory jakB
(A
może mieć dodatkowe akcesory, jeśli nie jest to jawna implementacja członka interfejsu).A
iB
są zdarzeniami, a nazwa i typA
iB
są identyczne.A
iB
są indeksatorami, typy i formalne listy parametrówA
iB
są identyczne, aA
ma takie same metody dostępu jakB
(A
może mieć dodatkowe metody dostępu, jeśli nie jest to jawna implementacja elementu członkowskiego interfejsu).
jest modyfikowany w następujący sposób:
Do celów mapowania interfejsu składowa klasy
A
jest zgodna ze składową interfejsuB
, gdy:
A
iB
są metodami, a nazwy i formalne listy parametrówA
iB
są identyczne, a typ zwracanyA
jest konwertowany na typ zwracanyB
poprzez identyczność niejawnej konwersji odniesienia do typu zwracanegoB
.A
iB
są właściwościami, nazwyA
iB
są identyczne,A
ma takie same metody dostępu jakB
(A
może mieć dodatkowe metody dostępu, jeśli nie jest to jawna implementacja elementu członkowskiego interfejsu), a typA
jest konwertowalny na zwracany typB
za pomocą konwersji tożsamości lub, jeśliA
jest właściwością tylko do odczytu, niejawnej konwersji odwołania.A
iB
są zdarzeniami, a nazwa i typA
iB
są identyczne.A
iB
są indeksatorami, formalne listy parametrówA
iB
są identyczne,A
ma takie same metody dostępu jakB
(A
może mieć dodatkowe metody dostępu, jeśli nie jest to jawna implementacja członka interfejsu), a typA
jest konwertowalny na zwracany typB
poprzez konwersję tożsamościową lub, jeśliA
jest indeksatorem odczytu, niejawna konwersję odwołania.
Jest to technicznie zmiana powodująca niekompatybilność, ponieważ poniższy program wyświetla "C1.M" dzisiaj, ale drukowałby "C2.M" po wprowadzeniu proponowanej poprawki.
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());
}
}
Ze względu na tę istotną zmianę możemy rozważyć zaprzestanie obsługi kowariantnych typów zwracanych dla niejawnych implementacji.
Ograniczenia implementacji interfejsu
Będziemy potrzebować reguły, że jawna implementacja interfejsu musi zadeklarować typ zwracany nie mniej pochodny niż typ zwracany zadeklarowany w jakimkolwiek przesłonięciu w jego interfejsach bazowych.
Implikacje zgodności interfejsu API
Do ustalenia
Otwarte problemy
Specyfikacja nie mówi, w jaki sposób obiekt wywołujący otrzymuje bardziej wyrafinowany typ zwracany. Prawdopodobnie byłoby to wykonywane w sposób podobny do tego, w jaki dzwoniący uzyskują specyfikacje parametrów najbardziej pochodnego nadpisania.
Jeśli mamy następujące interfejsy:
interface I1 { I1 M(); }
interface I2 { I2 M(); }
interface I3: I1, I2 { override I3 M(); }
Należy pamiętać, że w I3
metody I1.M()
i I2.M()
zostały „scalone”. Podczas implementowania I3
należy zaimplementować je razem.
Ogólnie rzecz biorąc, wymagamy jawnej implementacji, aby odwoływać się do oryginalnej metody. Pytanie brzmi: w klasie
class C : I1, I2, I3
{
C IN.M();
}
Co to znaczy tutaj? Czym powinno być N?
Proponuję, aby umożliwić wdrożenie I1.M
lub I2.M
(ale nie obu) i traktuję to jako implementację obu tych elementów.
Wady
- Każda zmiana języka musi się opłacać.
- [ ] Powinniśmy zapewnić, że wydajność jest rozsądna, nawet w przypadku hierarchii głębokiego dziedziczenia
- [ ] Powinniśmy upewnić się, że artefakty strategii tłumaczenia nie mają wpływu na semantyka języka, nawet w przypadku korzystania z nowego języka IL ze starych kompilatorów.
Alternatywy
Możemy nieco złagodzić reguły językowe, aby umożliwić, w źródle,
// 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;
}
}
Nierozwiązane pytania
- [ ] W jaki sposób interfejsy API, które zostały skompilowane w celu korzystania z tej funkcji, działają w starszych wersjach języka?
Spotkania projektowe
- pewna dyskusja na https://github.com/dotnet/roslyn/issues/357.
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-08.md
- Nieformalna dyskusja na temat wsparcia nadpisywania metod klas tylko w języku C# 9.0.
C# feature specifications