2013-07-02 13 views
24

Stworzyłem kilka interfejsów i klas generycznych do pracy z mianowania porządkiem obrad:Dlaczego ograniczenie typu ogólnego nie powoduje niejawnego błędu konwersji odniesienia?

interface IAppointment<T> where T : IAppointmentProperties 
{ 
    T Properties { get; set; } 
} 

interface IAppointmentEntry<T> where T : IAppointment<IAppointmentProperties> 
{ 
    DateTime Date { get; set; } 
    T Appointment { get; set; } 
} 

interface IAppointmentProperties 
{ 
    string Description { get; set; } 
} 

class Appointment<T> : IAppointment<T> where T : IAppointmentProperties 
{ 
    public T Properties { get; set; } 
} 

class AppointmentEntry<T> : IAppointmentEntry<T> where T : IAppointment<IAppointmentProperties> 
{ 
    public DateTime Date { get; set; } 
    public T Appointment { get; set; } 
} 

class AppointmentProperties : IAppointmentProperties 
{ 
    public string Description { get; set; } 
} 

Próbuję użyć pewne ograniczenia na parametry typu, aby upewnić się, że tylko poprawne typy mogą zostać określone. Jednak podczas określania ograniczenia określające, że T musi wdrożyć IAppointment<IAppointmentProperties>, kompilator daje błąd podczas korzystania z klasy, która jest Appointment<AppointmentProperties>:

class MyAppointment : Appointment<MyAppointmentProperties> 
{ 
} 

// This goes wrong: 
class MyAppointmentEntry : AppointmentEntry<MyAppointment> 
{ 
} 

class MyAppointmentProperties : AppointmentProperties 
{ 
    public string ExtraInformation { get; set; } 
} 

błędu jest:

The type 'Example.MyAppointment' cannot be used as type parameter 'T' in the generic type or method 'Example.AppointmentEntry<T>'. There is no implicit reference conversion from 'Example.MyAppointment' to 'Example.IAppointment<Example.IAppointmentProperties>'.

Czy ktoś może wyjaśnić, dlaczego to nie działa?

+5

To jest dziwne. ** ALE **: jest to rażące nadużywanie leków generycznych. Ledwo potrafię odczytać (jak mniemam) bardzo, bardzo uproszczony kod. –

Odpowiedz

91

Załóżmy uprościć:

interface IAnimal { ... } 
interface ICage<T> where T : IAnimal { void Enclose(T animal); } 
class Tiger : IAnimal { ... } 
class Fish : IAnimal { ... } 
class Cage<T> : ICage<T> where T : IAnimal { ... } 
ICage<IAnimal> cage = new Cage<Tiger>(); 

Twoje pytanie brzmi: dlaczego to ostatnia linia nielegalne?

Teraz, gdy przepisałem kod, aby go uprościć, powinno być jasne. An ICage<IAnimal> to klatka, w której można umieścić dowolne zwierzę, ale Cage<Tiger> może tylko trzymać tygrysy, więc musi to być nielegalne.

Gdyby nie było nielegalne to można to zrobić:

cage.Enclose(new Fish()); 

I hej, po prostu umieścić rybę w klatce tygrysa.

System typów nie pozwala na taką konwersję, ponieważ naruszyłoby to zasadę, że możliwości typu źródłowego nie mogą być mniejsze niż możliwości typu docelowego. (Jest to forma słynnej "zasady Liskov").

Dokładniej, powiedziałbym, że nadużywasz leków generycznych. Fakt, że stworzyłeś relacje typu, które są zbyt skomplikowane, abyś mógł je analizować, jest dowodem, że powinieneś uprościć całość; jeśli nie utrzymujesz wszystkich relacji typu prosto i napisałeś to, to twoi użytkownicy na pewno nie będą w stanie utrzymać tego prosto.

+0

Twoje uproszczenie czyni to bardziej zrozumiałym! Spróbuję przeprojektować klasy, aby użyć generycznych w czystszy sposób. Oznaczone jako odpowiedź. – Rens

+1

To bardzo dobre/proste wyjaśnienie. Pamiętam, że czytając książkę Jona Skeeta C# dokładniej wyjaśnia bardzo dobrze kowariancję kowariancji C# rodzajowej. Wysoce zalecane. –

+0

"Po prostu umieściłeś rybę w klatce tygrysa." Czy to tygrysia ryba? :-) – twelveFunKeys

6

Ponieważ zadeklarowałeś swoją klasę MyAppointment używając typu betonu zamiast interfejsu. Należy zadeklarować:

class MyAppointment : Appointment<IAppointmentProperties> { 
} 

Teraz konwersja może wystąpić niejawnie.

Deklarując AppointmentEntry<T> z przymusu where T: IAppointment<IAppointmentProperties> tworzysz kontrakt czym nieokreślony typ dla AppointmentEntry<T>musi pomieścić każdy rodzaj, który jest zadeklarowany z IAppointmentProperties. Deklarując typ klasą betonu, naruszyłeś tę umowę (implementuje ona typ typu typu IAppointmentProperties, ale nie o dowolnym typie).

+0

Tak, masz rację. Ale chcę zadeklarować klasę 'MyApointpoint' przy użyciu konkretnego typu' MyAppointmentProperties' (do tej pory nie było w moim przykładzie, moje przeprosiny), aby rozszerzyć właściwości 'IAppointmentProperties'. Czy możliwe jest określenie umowy, która na to pozwala? – Rens

+0

Możesz zadeklarować z dwoma jawnymi rodzajowymi parametrami typu zamiast ich zagnieżdżania: 'klasa TerminZamówienia : IAppointmentEntry gdzie TAppointment: IAppointment ' Jednak ostrzegam (podobnie jak inne), aby nie przekroczyć hierarchii typów chyba że istnieje ku temu ważny powód. –

+0

Dziękuję za wyjaśnienia. Sprawię, że hierarchia typów stanie się mniej skomplikowana. – Rens

0

Będzie działać, jeśli ponownie zdefiniować interfejs próbki z:

interface ICage<T> 

do

interface ICage<out T> 

(proszę zauważyć out słowa kluczowego)

następnie następujące stwierdzenie jest poprawne:

ICage<IAnimal> cage = new Cage<Tiger>(); 
6

Istnieje już bardzo dobra odpowiedź od Erica. Po prostu chciałem skorzystać z tej szansy, aby porozmawiać o tutaj: Invariance, i i.

Definicje proszę zobaczyć https://msdn.microsoft.com/en-us/library/dd799517(v=vs.110).aspx


Można powiedzieć, że jest to zoo.

abstract class Animal{} 
abstract class Bird : Animal{} 
abstract class Fish : Animal{} 
class Dove : Bird{} 
class Shark : Fish{} 

Zoo przenosi się, więc jego zwierzęta muszą zostać przeniesione ze starego zoo do nowego.

Niezmienniczość

Zanim je przenieść, musimy umieścić zwierząt do różnych pojemników. Wszystkie pojemniki wykonują te same operacje: umieszczają w nim zwierzę lub pobierają z niego zwierzę.

interface IContainer<T> where T : Animal 
{ 
    void Put(T t); 
    T Get(int id); 
} 

Oczywiście dla ryb potrzebujemy zbiornika:

class FishTank<T> : IContainer<T> where T : Fish 
{ 
    public void Put(T t){} 
    public T Get(int id){return default(T);} 
} 

więc ryby można wkładać i wydostać się ze zbiornika (mam nadzieję, że jeszcze żyje):

IContainer<Fish> fishTank = new FishTank<Fish>(); //Invariance, the two types have to be the same 
fishTank.Put(new Shark());   
var fish = fishTank.Get(8); 

Załóżmy, że są dozwolone, aby zmienić go na IContainer<Animal>, wtedy możesz przypadkowo włożyć gołębia do zbiornika, co będzie niepomyślnie tragedii.

IContainer<Animal> fishTank = new FishTank<Fish>(); //Wrong, some animal can be killed 
fishTank.Put(new Shark()); 
fishTank.Put(new Dove()); //Dove will be killed 

kontrawariancja

W celu poprawienia skuteczności działania, gdy dicides zespół zarządzający zoo do rozdzielenia obciążenia i zarządzanie procesem (zawsze to zrobić) rozładować. Mamy więc dwie oddzielne operacje, jedną dla obciążenia, a drugą z rozładowaniem.

interface ILoad<in T> where T : Animal 
{ 
    void Put(T t); 
} 

Następnie mamy bird cage:

class BirdCage<T> : ILoad<T> where T : Bird 
{ 
    public void Put(T t) 
    { 
    } 
} 

ILoad<Bird> normalCage = new BirdCage<Bird>(); 
normalCage.Put(new Dove()); //accepts any type of birds 

ILoad<Dove> doveCage = new BirdCage<Bird>();//Contravariance, Bird is less specific then Dove 
doveCage.Put(new Dove()); //only accepts doves 

kowariancji

in nowym zoo mamy zespół do rozładunku zwierząt.

interface IUnload<out T> where T : Animal 
{ 
    IEnumerable<T> GetAll(); 
} 

class UnloadTeam<T> : IUnload<T> where T : Animal 
{ 
    public IEnumerable<T> GetAll() 
    { 
     return Enumerable.Empty<T>(); 
    } 
} 

IUnload<Animal> unloadTeam = new UnloadTeam<Bird>();//Covariance, since Bird is more specific then Animal 
var animals = unloadTeam.GetAll(); 

Z punktu widzenia zespołu, nie ma znaczenia, co znajduje się w środku, po prostu wyładowuje zwierzęta z pojemników.

Powiązane problemy