简体   繁体   中英

C# generics with polymorphism

Hello denizens of stack overflow, I'm having an issue (perhaps with understanding?) regarding polymorphism and generics. I want to be able to define a "system" that contains a "component". I also want to be able to extend systems and components to be able to have greater functionality.

I currently have two base classes, Component, and I/A_ComponentSystem (For the sake of brevity, I'm not going to be showing any actual code, just definitions):

public abstract class Component { }

public interface IComponentSystem { }
public interface IComponentSystem<TComponent> : IComponentSystem
    where TComponent : Component { }

// The following are what should should actually be inherited from
public abstract class AComponentSystem : IComponentSystem { }
public abstract class AComponentSystem<TComponent> : AComponentSystem
    where TComponent : Component { }

Below is an example component/system created:

public abstract class ITag : Component { } // This is to allow generating the code in a different binary. Hard to explain in the question, I'll clarify if need be
public class Tag : ITag { }

public abstract class ITagSystem : AComponentSystem<ITag> { }
public class TagSystem : ITagSystem { }

Below are some excerpts of actually trying to use the code/morph between the different objects (please note that the code isn't meant to be used in this way, but I'm writing unit tests to ensure compatibility between layers)

// This is the main system that will be passed around
TagSystem tagSys = new TagSystem();

// This is OK
ITagSystem ITagSys = (ITagSystem)ITagSys;

// This is OK
AComponentSystem A_TagSys = (AComponentSystem)tagSys;

// This is OK
AComponentSystem<ITag> ATag_TagSys = (AComponentSystem<ITag>)tagSys;

// This is OK
IComponentSystem I_TagSys = (IComponentSystem)tagSys;

// This is OK
IComponentSystem<ITag> ITag_TagSys = (IComponentSystem<ITag>)tagSys;

// Even the following is OK (which is why I am confused)
IComponentSystem<Tag> new_ITag_TagSys = (IComponentSystem<Tag>)tagSys;

//***This is where it blows up*** (1)
AComponentSystem<Tag> new_ATag_TagSys = (AComponentSystem<Tag>)tagSys;

I have another interface/class, SystemManager, which is defined thusly:

public interface ISystemManager
{
    TComponent AddNewComponentToEntity<TComponent, TComponentSystem>(Entity e) // Please don't ask about Entity, it shouldn't be required for this snippet and I already feel like I've posted a lot)
        where TComponent : Component, new() // Required for some reason or I get an error
        where TComponentSystem : IComponentSystem<TComponent>;
}

Now, the specific block of code that I have here will throw an error as well:

//*** blows up here as well ***(2)
ISystemManager sysMan = new SystemManager(); // defined elsewhere
sysMan.AddNewComponentToEntity<Tag, ITagSystem>(entity);

As far as the errors that I receive, error (1) is:

Cannot convert type 'TagSystem' to 'AComponentSystem<Tag>'

Error (2) is below:

The type 'ITagSystem' cannot be used as type parameter 'TComponentSystem' in the generic type or method 'ISystemManager.AddNewComponentToEntity<TComponent,TComponentSystem>(Entity)'. There is no implicit reference conversion from 'ITagSystem' to 'IComponentSystem<Tag>'.

Now, as far as my question goes, it is thusly:

  1. Why can I not convert TagSystem to AComponentSystem<Tag> ? This seems like a valid morph.
  2. Why is ITagSystem not converting to IComponentSystem<Tag> ? It appears that Tag should still conform to ITag, which is supported.
  3. Is there any way I could change my hierarchy while preserving my need for that many layers of abstraction?

Thank you to anyone for reading this and assisting me.

Note: Yes, this is for an EntityFramework driven game engine. I'm building it mainly as an exercise for myself, and so I can quickly spin up 3d projects for myself. Yes, I've built a few game projects before, no I'm not interested in "finishing" a game, I'm just tinkering and having fun.

Without a simpler and yet more-complete code example, it's impossible to provide specific advice in your specific scenario. However, the basic problem is that the types are indeed not convertible, just as the compiler says.

  1. Why can I not convert TagSystem to AComponentSystem<Tag> ? This seems like a valid morph.

TagSystem doesn't inherit AComponentSystem<Tag> . It inherits AComponentSystem<ITag> . These two types are not actually the same. Just because Tag inherits/implements ITag , that does not mean that AComponentSystem<Tag> automatically inherits/implements AComponentSystem<ITag> . If it did, then that would mean that a method or property of AComponentSystem<Tag> that normally would return a value of type Tag , could wind up being used in a situation where a Tag value is expected, but some other implementation of ITag is actually returned. This is because you would be able to cast to AComponentSystem<Tag> , and then use that reference to return the non- Tag implementation of ITag , to some code that only wanted Tag .

This is bad for what I hope are obvious reasons, so the compiler doesn't allow you to do that.

  1. Why is ITagSystem not converting to IComponentSystem<Tag> ? It appears that Tag should still conform to ITag, which is supported.

Without a good Minimal, Complete, and Verifiable code example , it's difficult to answer this part of your question, as the types you've shown don't appear consistent with the code you've shown. ITagSystem is declared as inheriting AComponentSystem<ITag> , which in turn implements only IComponentSystem , not IComponentSystem<TComponent> .

So based on the code shown, there's no reason even naively to think that the conversion could work. But let's assume for a moment there's a typo in the type declarations you've shown. Then the answer is basically the same as above: implementing IComponentSystem<ITag> is not the same as implementing IComponentSystem<Tag> .

  1. Is there any way I could change my hierarchy while preserving my need for that many layers of abstraction?

Possibly. It depends on what these types actually do. Since C# 4, we've been able to specify generic type parameters on interfaces with covariance and contravariance. With a type parameter thus restricted, and interface members to match, the interface then can support specific casting scenarios like you're trying to do.

But note that this only works when the interface members really are compatible with such conversions. The compiler still won't let you do anything unsafe.

There are a lot of questions on Stack Overflow already discussing this. Technically your question could even be considered a duplicate of those. I hope the above addresses your immediate concerns, and gives you enough information to do more research and see if generic interface variance will work in your situation. If you need more help, I recommend you post a new question and make sure to include a good MCVE that clearly illustrates your question in the simplest way possible.

  1. TagSystem distantly inherits AComponentSystem<ITag> , but you are trying to convert it to AComponentSystem<Tag> . (Note the lack of an "I" in the generic type.) Those two generic types of AComponentSystem<> are completely different, and you cannot freely cast between the two.
  2. Same as point 1, just because Tag is a child of ITag doesn't mean that IComponentSystem<Tag> is a child of IComponentSystem<ITag> .
  3. The answer is almost certainly yes, though exactly how depends entirely on how you are going to use it. You might also want to ask yourself if you really need this many layers of abstraction.

To give a better example of my first point, take for example a common generic type: the List. If generics followed the same inheritance rules as normal classes, then List<Car> would be a subtype of List<Vehicle> . But the difference between the two is that the first list can only hold cars, while the second list can hold any vehicle . So if these lists were parent and child, you would be able to do the following:

List<Car> cars = new List<Car>();
List<Vehicle> vehicles = (List<Vehicle>)cars;
vehicles.Add(new Truck());

You see the problem? The general rules of inheritance just allowed us to add a non-Car object to out list of cars. Or they would, provided that is a legal cast, which it isn't. In reality, List<Car> and List<Vehicle> are not related in any way, but are actually completely separate classes with no direct relation whatsoever.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM