Why does a generic type constraint result in a no implicit reference conversion error?
Asked Answered
A

5

61

I have created a couple of interfaces and generic classes for working with agenda appointments:

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

I'm trying to use some constraints on the type parameters to ensure that only valid types can be specified. However, when specifying a constraint defining that T must implement IAppointment<IAppointmentProperties>, the compiler gives an error when using a class that is Appointment<AppointmentProperties>:

class MyAppointment : Appointment<MyAppointmentProperties>
{
}

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

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

The error is:

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

Could anybody explain why this does not work?

Attainder answered 2/7, 2013 at 20:4 Comment(1)
This is odd. BUT: this is a blatant overuse of generics. I can barely read what (I assume to be) very, very simplified code.Cellar
V
158

Let's simplify:

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

Your question is: why is the last line illegal?

Now that I have rewritten the code to simplify it, it should be clear. An ICage<IAnimal> is a cage into which you can place any animal, but a Cage<Tiger> can only hold tigers, so this must be illegal.

If it were not illegal then you could do this:

cage.Enclose(new Fish());

And hey, you just put a fish into a tiger cage.

The type system does not permit that conversion because doing so would violate the rule that the capabilities of the source type must not be less than the capabilities of the target type. (This is a form of the famous "Liskov substitution principle".)

More specifically, I would say that you are abusing generics. The fact that you've made type relationships that are too complicated for you to analyze yourself is evidence that you ought to simplify the whole thing; if you're not keeping all the type relationships straight and you wrote the thing then your users surely will not be able to keep it straight either.

Vardar answered 3/7, 2013 at 5:13 Comment(5)
This is a very good/simple explanation. I remember reading Jon Skeet's book c# in depth it explains c# generic type covariance contravariance very well. Highly recommend.Cassatt
"You just put a fish into a tiger cage." Was it a tiger fish? :-)Carree
@StephenZeng: I was the editor of that book, and it is indeed highly recommended!Vardar
@EricLippert I pre-ordered new version of C# in depth.Mako
I ran into the same issue. Your suggestion - if it is too complicated for you to analyze yourself then your users surely will not be able to keep it straight either - is gold.Merat
C
27

There is already a very good answer from Eric. Just wanted to take this chance to talk about the Invariance, Covariance, and Contravariance here.

For definitions please see https://learn.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance


Let's say there is a zoo.

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

The zoo is relocating, so its animals need to be moved from the old zoo to the new one.

Invariance

Before we move them, we need to put the animals into different containers. The containers all do the same operations: put an animal in it or get an animal out from it.

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

Obviously, for fish we need a tank:

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

So the fish can be put in and get out from the tank(hopefully still alive):

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

Suppose we are allowed to change it to IContainer<Animal>, then you can accidentally put a dove in the tank, in which case tragedy will occur.

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

Contravariance

To improve efficiency, the zoo management team decides to separate the load and unload process (management always does this). So we have two separate operations, one for load only, the other unload.

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

Then we have a birdcage:

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

Covariance

In the new zoo, we have a team for unloading animals.

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

From the team's point of view, it does not matter what it is inside, they just unload the animals from the containers.

Cassatt answered 15/7, 2016 at 11:3 Comment(1)
This answer is a valuable lesson in modern programming!Horripilate
S
7

Because you declared your MyAppointment class using the concrete type rather than the interface. You should declare as follows:

class MyAppointment : Appointment<IAppointmentProperties> {
}

Now the conversion can occur implicitly.

By declaring AppointmentEntry<T> with the constraint where T: IAppointment<IAppointmentProperties> you are creating a contract whereby the unspecified type for AppointmentEntry<T> must accommodate any type that is declared with IAppointmentProperties. By declaring the type with the concrete class you have violated that contract (it implements a type of IAppointmentProperties but not any type).

Stringed answered 2/7, 2013 at 20:12 Comment(2)
Yes, you are right. But I want to declare the MyAppointment class using the concrete type MyAppointmentProperties (it wasn't in the example until now, my apologies) to extend the properties of IAppointmentProperties. Is it possible to specify a contract that allows for that?Attainder
You could declare with two explicit generic type parameters instead of nesting them: class AppointmentEntry<TAppointment, TProperties> : IAppointmentEntry<TAppointment> where TAppointment: IAppointment<TProperties> However, I caution you (as have others) not to overconstrain your type hierarchy unless there is a very compelling reason to do so.Stringed
W
1

It will work if you re-define the sample interface from:

interface ICage<T>

to

interface ICage<out T>

(please notice the out keyword)

then the following statement is correct:

ICage<IAnimal> cage = new Cage<Tiger>();
Wageworker answered 15/7, 2016 at 8:51 Comment(0)
P
1

In case someone else also has this error message: I found the same interface defined twice in different namespaces and the classes that have been tryed to be linked together did not use the same interface.

Perspicacity answered 5/3, 2019 at 11:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.