I'm working on a card game in Unity right now.
The attributes for cards of different types are very similar, aside from the sets they can belong to, so I have them set up as sibling classes that inherit a generic class which expects an enum for the sets they can be. Like so:
public enum SpellSets
{
STRESS,
BOON,
BUFF
}
public class BaseCardAttributes<TSets> where TSets : System.Enum
{
*** some fields ***
public TSets set;
}
public class SpellCardAttributes : BaseCardAttributes<SpellSets> { }
In another class, I want to be able to check if the attributes passed in belong to a spell card or a non spell card, so I am trying to do the following:
public void InitializeCard(BaseCardAttributes<System.Enum> cardAttributes)
{
title.text = cardAttributes.title;
textBox.text = cardAttributes.cardText;
cardArt.sprite = cardAttributes.cardArt;
if (cardAttributes is SpellCardAttributes spellCardAttributes)
{
switch (spellCardAttributes.positioning)
{
*** switch stuff ***
}
}
}
But it's telling me "An expression of type 'BaseCardAttributes' cannot be handled by a pattern of type 'SpellCardAttributes'". Any advice I how I could setup this sort of type checking? I will need to check this sort of thing multiple places.
I've tried wrapping it all in an interface, and passing that in to the function instead, since it seems to have no problem casting from the interface to spellCardAttributes.
Like so:
public interface CardAttributes<TSets> where TSets : System.Enum { }
[System.Serializable]
public class BaseCardAttributes<TSets> : CardAttributes<TSets> where TSets : System.Enum
public void InitializeCard(CardAttributes<System.Enum> cardAttributes, CardLogic logic)
{
if (cardAttributes is SpellCardAttributes spellCardAttributes) {...}
}
However, that solution doesn't work other places. Like, I would like to have different templates I can fill in for the cards like so:
public abstract class BaseCardTemplate<TAttributes> : ScriptableObject
where TAttributes: CardAttributes<System.Enum>
public abstract class SpellCardTemplate : BaseCardTemplate<SpellCardAttributes>
But I can't derive SpellCardTemplate from BaseCardTemplate that way because that cast between the interface and SpellCardAttributes doesn't work.
The core problem here is that enum
s are value types, and value types do not support inheritence... except in the very narrow scope of when they do, like System.Enum
-> enum
. As basically the only value type that inherits from something other than System.Object
, enum
s are very special. A fact which the compiler really isn't good at dealing with.
Up until C# 7.3 it wasn't possible to use System.Enum
as a generic constraint, since value types cannot (under basically any other circumstance) inherit from other value types. Today we can at least use it as a constraint, but the compiler scoffs at the idea that anything could be derived from a value type like System.Enum
so it won't let you do anything like that.
Fortunately it's not the end of the story for your code. While we can't treat the specialized classes the way you want, there are options.
The first option is to insert a higher base class that isn't generic, or at least is only constrained to class types. Let's keep it simple and go with a plain old base class:
public abstract class BaseCardAttributes
{
// add your common non-generic properties here:
public string Title { get; protected set; }
public string CardText { get; protected set; }
public Image CardArt { get; protected set; }
}
public class BaseCardAttributes<TSets> : BaseCardAttributes
where TSets : System.Enum
{
public TSets set;
}
public void InitializeCard(BaseCardAttributes cardAttributes)
{
// ...
if (cardAttributes is SpellCardAttributes spellCardAttributes)
{
// do whatever here.
}
}
Alternatively you can have a non-generic interface that covers the bulk of the properties instead:
public interface IBaseCardAttributes
{
string Title { get; }
string CardText { get; }
Image CardArt { get; }
}
public class BaseCardAttributes<TSets> : IBaseCardAttributes
where TSets : System.Enum
{
// ...
}
public void InitializeCard(IBaseCardAttributes cardAttributes)
{
// same code as above, slightly different method signature
}
Either way removes the generics from the InitializeCard
method while still doing what you want. But they're a bit more work than necessary.
The short answer - now that you've sat through my long one - is to generalize the InitializeCard
method:
public void InitializeCard<TSets>(BaseCardAttributes<TSets> cardAttributes)
where TSets : System.Enum
{
// Your code above now works.
}
I can't see the rest of your program architecture, but it seems a little clumsy to be building an InitializeCard
method that has to support all of the possible card types. Have you considered moving the initialization logic to the BaseCardAttribute<>
implementations? Sometimes it's better to keep tightly-related logic and data together.