Czy C # obsługuje kowariancję typu zwracanego?

84

Pracuję z platformą .NET i naprawdę chcę mieć możliwość tworzenia niestandardowego typu strony, z której korzysta cała moja witryna. Problem pojawia się, gdy próbuję uzyskać dostęp do strony z kontrolki. Chcę mieć możliwość zwrócenia strony określonego typu zamiast strony domyślnej. Czy jest na to sposób?

public class MyPage : Page
{
    // My own logic
}

public class MyControl : Control
{
    public MyPage Page { get; set; }
}
Kyle Sletten
źródło
możliwy duplikat kowariantnych typów zwracanych
ogólnych

Odpowiedzi:

170

UPDATE: Ta odpowiedź została napisana w 2011 roku. Po dwóch dekadach ludzi proponujących kowariancję typu zwracanego dla C # wygląda na to, że zostanie ostatecznie zaimplementowana; Jestem raczej zaskoczony. Zapowiedź znajduje się na dole strony https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/ ; Jestem pewien, że pojawią się szczegóły.


Wygląda na to, że chcesz kowariancji typu zwracanego. C # nie obsługuje kowariancji typu zwracanego.

Kowariancja typu zwracanego polega na zastąpieniu metody klasy bazowej, która zwraca mniej szczegółowy typ, na taki, który zwraca bardziej szczegółowy typ:

abstract class Enclosure
{
    public abstract Animal Contents();
}
class Aquarium : Enclosure
{
    public override Fish Contents() { ... }
}

Jest to bezpieczne, ponieważ konsumenci Treści za pośrednictwem Obudowy oczekują zwierzęcia, a Akwarium obiecuje nie tylko spełnić ten wymóg, ale co więcej, złożyć bardziej rygorystyczną obietnicę: zwierzę jest zawsze rybą.

Ten rodzaj kowariancji nie jest obsługiwany w języku C # i jest mało prawdopodobne, aby kiedykolwiek był obsługiwany. Nie jest obsługiwany przez CLR. (Jest obsługiwany przez C ++ i przez implementację C ++ / CLI w CLR; robi to poprzez generowanie magicznych metod pomocniczych, które sugeruję poniżej.)

(Niektóre języki obsługują również kontrawariancję typu parametru formalnego - można przesłonić metodę pobierającą Fish metodą pobierającą Animal. Ponownie, kontrakt jest spełniony; klasa bazowa wymaga obsługi dowolnego Fisha, a pochodna class obiecuje nie tylko obsługiwać ryby, ale także dowolne zwierzęta. Podobnie, C # i CLR nie obsługują formalnej kontrawariancji typu parametru).

Aby obejść to ograniczenie, wykonaj następujące czynności:

abstract class Enclosure
{
    protected abstract Animal GetContents();
    public Animal Contents() { return this.GetContents(); }
}
class Aquarium : Enclosure
{
    protected override Animal GetContents() { return this.Contents(); }
    public new Fish Contents() { ... }
}

Teraz zyskujesz zarówno zalety zastępowania metody wirtualnej, jak i silniejsze pisanie podczas korzystania z czegoś w rodzaju Aquarium typu czasu kompilacji.

Eric Lippert
źródło
3
Idealny! Zapomniałem o ukrywaniu się!
Kyle Sletten
3
dobra odpowiedź, jak zawsze. Przez chwilę brakowało nam tych odpowiedzi.
Dhananjay,
4
Czy istnieje uzasadnienie, aby nie wspierać kowariancji zwrotów i kontrawariancji parametrów, które są dostępne do przeczytania w dowolnym miejscu?
porusza się
26
@EricLippert Będąc fanem języka C #, zawsze jestem ciekawy „za kulisami”, więc muszę tylko zapytać: czy kiedykolwiek rozważano kowariancję typu zwracanego dla C # i uznano, że ma zbyt niski zwrot z inwestycji? Rozumiem ograniczenia czasowe i zestawienia błędów, ale z Twojego stwierdzenia „mało prawdopodobne, aby kiedykolwiek był obsługiwany”, wygląda na to, że zespół projektowy faktycznie wolałby NIE mieć tego w języku. Z drugiej strony Java wprowadziła tę funkcję języka w Javie 5, więc zastanawiam się, jaka jest różnica w większych, nadrzędnych zasadach rządzących procesem projektowania języka.
Cristian Diaconescu,
7
@ Cyral: poprawne; jest na liście w segmencie „umiarkowanego zainteresowania” - czyli od dawna! To może się zdarzyć, ale jestem dość sceptyczny.
Eric Lippert
8

Dzięki interfejsom obejrzałem to, jawnie implementując interfejs:

public interface IFoo {
  IBar Bar { get; }
}
public class Foo : IFoo {
  Bar Bar { get; set; }
  IBar IFoo.Bar => Bar;
}
ChetPrickles
źródło
2

Umieszczenie tego w obiekcie MyControl zadziałałoby:

 public new MyPage Page {get return (MyPage)Page; set;}'

Nie możesz zastąpić właściwości, ponieważ zwraca ona inny typ ... ale możesz ją przedefiniować.

W tym przykładzie nie potrzebujesz kowariancji, ponieważ jest ona stosunkowo prosta. Jedyne, co robisz, to dziedziczenie obiektu podstawowego Pagez MyPage. Wszystko Control, co chcesz zwrócić, MyPagezamiast tego, Pagemusi ponownie zdefiniować PagewłaściwośćControl

Zhais
źródło
1

Tak, obsługuje kowariancję, ale zależy to od dokładnej rzeczy, którą próbujesz osiągnąć.

Często też używam generycznych do rzeczy, co oznacza, że ​​kiedy robisz coś takiego:

class X<T> {
    T doSomething() {
    }

}

class Y : X<Y> {
    Y doSomethingElse() {
    }
}

var Y y = new Y();
y = y.doSomething().doSomethingElse();

I nie „gub” swoich typów.

Cade Roux
źródło
1

Jest to funkcja nadchodzącej wersji C # 9.0 (.Net 5), z której można teraz pobrać wersję zapoznawczą.

Poniższy kod teraz buduje pomyślnie (bez podania: error CS0508: 'Tiger.GetFood()': return type must be 'Food' to match overridden member 'Animal.GetFood()')

class Food { }
class Meat : Food { }

abstract class Animal {
    public abstract Food GetFood();
}

class Tiger : Animal {
    public override Meat GetFood() => default;
}

class Program {
    static void Main() => new Tiger();
}
Neuron
źródło
0

Nie próbowałem tego, ale czy to nie działa?

YourPageType myPage = (YourPageType)yourControl.Page;
AGoodDisplayName
źródło
1
Tak, to działa. Chcę tylko spróbować uniknąć zarzucania wszędzie.
Kyle Sletten
0

Tak. Można to zrobić na wiele sposobów, a to tylko jedna opcja:

Możesz sprawić, aby Twoja strona zaimplementowała jakiś niestandardowy interfejs, który ujawnia metodę o nazwie „GetContext” lub coś w tym rodzaju i zwraca określone informacje. Wtedy Twoja kontrola może po prostu zażądać strony i przesłać:

var myContextPage = this.Page as IMyContextGetter;

if(myContextPage != null)
   var myContext = myContextPage.GetContext();

Następnie możesz użyć tego kontekstu, jak chcesz.

Tejs
źródło
0

Możesz uzyskać dostęp do swojej strony z dowolnej kontrolki, przechodząc w górę drzewa nadrzędnego. To jest

myParent = this;

while(myParent.parent != null)
  myParent = myParent.parent;

* Nie skompilowano ani nie testowano.

Lub pobierz stronę nadrzędną w bieżącym kontekście (w zależności od wersji).


W takim razie lubię to robić: tworzę interfejs z funkcjami, których chcę użyć w kontrolce (na przykład IHostingPage)

Następnie rzutuję stronę nadrzędną „IHostingPage host = (IHostingPage) Parent;” i jestem gotowy do wywołania funkcji na stronie, której potrzebuję z mojej kontroli.

Hogan
źródło
0

Zrobię to w ten sposób:

class R {
    public int A { get; set; }
}

class R1: R {
    public int B { get; set; }
}

class A
{        
    public R X { get; set; }
}

class B : A 
{
    private R1 _x;
    public new R1 X { get => _x; set { ((A)this).X = value; _x = value; } }
}
Nieposkromiony
źródło