it-swarm.com.de

Warum führt eine generische Typeinschränkung zu einem nicht impliziten Referenzkonvertierungsfehler?

Ich habe einige Interfaces und generische Klassen für die Arbeit mit Agenda-Terminen erstellt:

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; }
}

Ich versuche, einige Einschränkungen für die Typparameter zu verwenden, um sicherzustellen, dass nur gültige Typen angegeben werden können. Wenn Sie jedoch eine Einschränkung angeben, die definiert, dass TIAppointment<IAppointmentProperties>, der Compiler gibt einen Fehler aus, wenn eine Klasse verwendet wird, die Appointment<AppointmentProperties>:

class MyAppointment : Appointment<MyAppointmentProperties>
{
}

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

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

Der Fehler ist:

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>'.

Kann jemand erklären, warum dies nicht funktioniert?

37
Rens

Vereinfachen wir:

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>();

Ihre Frage ist: Warum ist die letzte Zeile illegal?

Nun, da ich den Code umgeschrieben habe, um ihn zu vereinfachen, sollte es klar sein. Ein ICage<IAnimal> ist ein Käfig, in den du jedes Tier, aber ein Cage<Tiger> kann nur Tiger halten, das muss also illegal sein.

Wenn es nicht illegal wäre, könnten Sie dies tun:

cage.Enclose(new Fish());

Und hey, du hast gerade einen Fisch in einen Tigerkäfig gelegt.

Das Typsystem erlaubt diese Konvertierung nicht, da dies die Regel verletzen würde, dass die Fähigkeiten des Quelltyps nicht less als die Fähigkeiten des Zieltyps sein dürfen. (Dies ist eine Form des berühmten "Liskov-Substitutionsprinzips".)

Genauer gesagt würde ich sagen, dass Sie Generika missbrauchen. Die Tatsache, dass Sie Typbeziehungen hergestellt haben, die zu kompliziert sind, als dass Sie sich selbst analysieren könnten, ist ein Beweis dafür, dass Sie das Ganze vereinfachen sollten. Wenn Sie nicht alle Typbeziehungen gerade halten und das Ding geschrieben haben, können Ihre Benutzer es sicherlich auch nicht gerade halten.

115
Eric Lippert

Es gibt bereits eine sehr gute Antwort von Eric. Ich wollte diese Gelegenheit nutzen, um über die Invarianz , Kovarianz und zu sprechen Kontravarianz hier.

Definitionen finden Sie unter https://docs.Microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance


Nehmen wir an, es gibt einen Zoo.

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

Der Zoo zieht um, daher müssen seine Tiere vom alten in den neuen Zoo gebracht werden.

Invarianz

Bevor wir sie bewegen, müssen wir die Tiere in verschiedene Behälter füllen. Die Behälter führen alle die gleichen Vorgänge aus: Ein Tier hineinlegen oder ein Tier herausholen.

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

Natürlich brauchen wir für Fische einen Panzer:

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

So kann der Fisch rein und raus aus dem Tank (hoffentlich noch am Leben):

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

Angenommen, wir dürfen es in IContainer<Animal> Ändern, dann können Sie versehentlich eine Taube in den Panzer stecken, was zu einer Tragödie führen wird.

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

Kontravarianz

Um die Effizienz zu verbessern, entscheidet das Zoo-Management-Team, den Lade- und Entladeprozess zu trennen (das Management tut dies immer). Wir haben also zwei getrennte Operationen, eine nur zum Laden, die andere zum Entladen.

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

Dann haben wir einen Vogelkäfig:

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

Kovarianz

Im neuen Zoo haben wir ein Team zum Entladen von Tieren.

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();

Aus Sicht des Teams spielt es keine Rolle, was sich darin befindet, sie entladen einfach die Tiere aus den Containern.

14
Stephen Zeng

Weil Sie Ihre Klasse MyAppointment mit dem konkreten Typ und nicht mit der Schnittstelle deklariert haben. Sie sollten wie folgt deklarieren:

class MyAppointment : Appointment<IAppointmentProperties> {
}

Jetzt kann die Konvertierung implizit erfolgen.

Indem Sie AppointmentEntry<T> mit der Einschränkung where T: IAppointment<IAppointmentProperties> Sie erstellen einen Vertrag wobei der nicht angegebene Typ für AppointmentEntry<T>muss jeden Typ aufnehmen, der mit IAppointmentProperties deklariert ist. Indem Sie den Typ mit der konkreten Klasse deklarieren, haben Sie gegen diesen Vertrag verstoßen (er implementiert a Typ von IAppointmentProperties, aber nicht any Typ).

6
Peter Gluck

Es funktioniert, wenn Sie die Beispielschnittstelle neu definieren von:

interface ICage<T>

zu

interface ICage<out T>

(Bitte beachten Sie das Schlüsselwort out.)

dann ist die folgende Aussage richtig:

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

Für den Fall, dass jemand anderes auch diese Fehlermeldung hat: Ich habe dieselbe Schnittstelle zweimal in verschiedenen Namespaces definiert und die Klassen, die versucht wurden, miteinander verbunden zu werden, haben nicht dieselbe Schnittstelle verwendet.

0
CitrusO2