Krumelur
Krumelur

Reputation: 33058

What am I doing wrong when seeing "cannot convert expression to type while using a generic method"?

I have a List<GKComponentSystem<GKComponent>> and a generic Add<T>() method with a constraint for T being a GKComponent.

Why can't I add an instance of GKComponentSystem<T> to my list? See code snipped below:

List<GKComponentSystem<GKComponent>> _systems = new List<GKComponentSystem<GKComponent>>();

public void AddSystem<T>(int position = -1) where T : GKComponent
{
    var system = new GKComponentSystem<T>();
    _systems.Add(system);
}

Error:

Argument #1 cannot convert GameplayKit.GKComponentSystem<T> expression to type GameplayKit.GKComponentSystem<GameplayKit.GKComponent>

That’s in the line _systems.Add(system).

I thought I knew C#, but this is one of the situations where I'm happy to have StackOverflow - what the heck am I not understanding here?

system is a GKComponentSystem<T> and T must be a GKComponent, so system is a GKComponentSystem<GKComponent> and I should be able to add it to my list.

Here's GKComponentSystem:

public class GKComponentSystem<T> : NSObject where T : GKComponent

Its T is also a GKComponent...

Is this about contravariance (a topic I definitely have to learn more about)?

Upvotes: 1

Views: 598

Answers (2)

Andrew Shepherd
Andrew Shepherd

Reputation: 45262

Here's a simpler example:

 class Parent
 {
 }

 class Child : Parent
 {
 }


 class GenericClass<T> 
 {
 }


 Parent p;
 p = new Child();  // A child inherits from Parent, so this is allowed.

 GenericClass<Parent> gp;

 gp = new GenericClass<Child>();  // Not allowed! GenericClass<Child> does not inherit from GenericClass<Parent>

In your example, the fact that T inherits from GKComponent, does not translate to a rule that GKComponentSystem<T> can be converted to GKComponentSystem<GKComponent>.

So lets apply this to Lists now.

List<Parent> l = new List<Parent>();
l.Add(new Child());  // A child can be converted to a Parent, this is OK


List<GenericClass<Parent>> gl = new List<GenericClass<Parent>>();

gl.Add(new GenericClass<Child>()); // A GenericClass<Child> does not convert to GenericClass<Parent>, so this is not allowed.

If you really want this to work, you can define a generic interface. These allow you to specify generic parameters with out as follows:

 interface IGenericClass<out T>
 {
 }

 class GenericClass<T> : IGenericClass<T>
 {
 }

 IGenericClass<Child> gcChild = new GenericClass<Child>();
 IGenericClass<Parent> gcParent = gcChild; // This is allowed!

 var l = new List<IGenericClass<Parent>>();
 l.Add(new GenericClass<Child>()); // Also allowed

So to apply this to your example:

 interface IGKComponentSystem<out T> 
 {
 }

 class GKComponentSystem<T> : IGKComponentSystem
 {
 }

 List<IGKComponentSystem<GKComponent>> _systems = new List<IGKComponentSystem<GKComponent>(); 

 // Should work from there...  
 public void AddSystem<T>(int position = -1) where T : GKComponent
 {
    var system = new GKComponentSystem<T>();
    _systems.Add(system);
 }

Upvotes: 5

user743382
user743382

Reputation:

Replacing your custom types with BCL types:

List<List<object>> _systems = new List<List<object>>();

public void AddSystem<T>(int position = -1) where T : class
{
    var system = new List<T>();
    _systems.Add(system);
}

Now, suppose this were valid and I were to call AddSystem<string>();. This would create a List<string>, and add it to _systems.

Now, suppose I call _systems[0].Add(1);. There's nothing preventing this. _systems is a List<List<object>>, therefore _systems[0] is a List<object>, therefore its Add method accepts any object.

But what I created was a List<string>, not a List<object>. It shouldn't be possible to add an int to it.

The only way this can be rejected by the compiler is by making the List<string>-to-List<object> conversion invalid. Or in your case, the GKComponentSystem<T> to GKComponentSystem<GKComponent> conversion.

Is this about contravariance (a topic I definitely have to learn more about)?

Sort of. Interface and delegate types can specify that some additional guarantees are made. The IEnumerable<string>-to-IEnumerable<object> conversion safe, and allowed for types that include the appropriate annotation. But it's not available for class types, and what you can put in such an interface is a bit restricted, anything that could potentially be unsafe is disallowed.

If what you want to put in your GKComponentSystem only allows reads, meaning it doesn't have the problem List does, you could make an IGKComponentSystem<T> interface, and store a List<IGKComponentSystem<GKComponent>>, and add a IGKComponentSystem<T> instance to that without issues.

If what you want to put in your GKComponentSystem also allows writes (such as an Add method), then the conversion is inherently unsafe and you'll need to re-think your design.

Upvotes: 4

Related Questions