- Home /
Polymorphism with generic ScriptableObjects?
I have a generic scriptable object field of type 'Effect' showing in the inspector, but I am unable to assign a ScriptableObject of the derived type to that field.
Here's my simplified sudo-code example.
[Serializable]
public class Effect { }
[Serializable]
public class DerivedEffect : Effect { }
public class StatEffect<T> : ScriptableObject where T : Effect
{
public T Effect;
}
// This Scriptable Object can't be assigned to fields of type StatEffect<Effect>
public class DerivedStatEffect : StatEffect<DerivedEffect> { }
public class SomeClass
{
// Unity won't let me assign DerivedStatEffect to this slot in the inspector?
StatEffect<Effect> StatEffect;
}
So is it that polymorphism doesn't work for generic references? No Covariance?
I believe you need to implement an interface on your scriptableObject and use that in combination with your current approach, which can only be done with SerializedReferences in 2019.3.
There should be a way around it though, I recently did something similar and I think I ended up dropping the generics for the interface and using the base class for the inspector level serialization.
Answer by Bunny83 · Feb 18, 2020 at 11:52 PM
Covariance does work, however your setup does not represent a valid covariance case. Your types are incompatible.
First of all covariance and contravariance only apply to array types, delegate types and generic type parameters. Array types are a bit special since they allow covariance which is actually not type safe. However generic type parameters have strict constraints how you can use them.
First of all you should keep in mind that two concrete types of a generic type with different type parameters are fundamentally different types. So a List<int>
and a List<string>
do not have anything in common besides the base class System.Object. The two special contract cases covariance and contravariance only apply to certain "access operations". Covariance does only work on "out" type parameters. That means the generic class can use this type parameter only for return values or out parameters. So it's about the direction of information flow. The prime example for covariance is the IEnumerable<out T>
interface. See the documentation. Note the "out" prefix. That means the type "T" can only be used for return values / readonly properties / out parameters. Any usage which allows information flow in the other direction is not allowed. So you can not use this type to define a method parameter, a writeable property or a field.
Contravariance on the other hand is exactly the opposite in the sense of information flow. They only allowed usage of that type is when information flows into the generic type. So only method parameters (any maybe write only properties?!? which would be pretty crazy but possible i think). The prime example for contravariance is System.Action<in T>
. Again note the "in" prefix and see the documentation.
You can not have a type parameter have both, "in" and "out" and it also wouldn't make any sense. This is all about type safety. As I said earlier generic types with different type arguments are fundamentally different types. Even when one type is derived from the other. Co- / Contravariance provide a type safe way to allow certain assignments which usually wouldn't work. The only requirement is that typesafety is preserved.
Now have a look at your case Your "StatEffect" variable is of type StatEffect<Effect>
. Therefore you will have a field named "Effect" which is of type "Effect". However this is already an issue. You try to assign an instance of your DerivedStatEffect
which is of type StatEffect<DerivedEffect>
. The field "Effect" in this class is of type "DerivedEffect". So imagine you have another class like this:
[Serializable]
public class FooBarEffect : Effect { }
Now imagine this code:
StatEffect.Effect = new FooBarEffect();
Is this allowed? Yes since The Effect field of our StatEffect class is of type Effect so we can assign a FooBarEffect since it is derived from Effect. Would this be possible when the actual instance behind "StatEffect" is an instance of "DerivedStatEffect" ? No, it would not work because the Effect field in our actual class is of type DerivedEffect
and a FooBarEffect is not derived from that. Therefore your types are completely incompatible with each other.
A member field generally models both information flow directions since it can be written to and also read from. So the opposite is also not possible. Imagine that:
StatEffect<Effect> myObj; // assume we have an instance of that class here.
myObj.Effect = new FooBarEffect(); // possible since Effect is of type Effect
StatEffect<DerivedEffect> myDerivedStatEffect = myObj; // this is also NOT possible.
In this case we actually have an isntance of StatEffect<Effect>
so its Effect field can happily hold any Effect derived class. However the assignment to "myDerivedStatEffect" is not possible since the field of that type does only allow DerivedEffects.
Some people think about generics just as some fancy way to define some kind of dynamic types. This is not the case. Generic type parameters generally break polymorphism between concrete classes of the same generic type but with different type parameters. The only exception to this rule are co and contravariance and those only work with either "out" or "in" type parameters.
To me it's not really clear what you wanted to achieve with your class setup. What you want to do clearly is not type safe so it's hard to tell what your goal was.
Thanks for the detailed and throughout answer. $$anonymous$$y goal was to have an effects system where I could drag and drop functionality via scriptable objects in the editor. But now I understand the constraints of generics I will look at a new approach.
Your answer
Follow this Question
Related Questions
An OS design issue: File types associated with their appropriate programs 1 Answer
How to Update ScriptableObject 1 Answer
Null Reference Error With Scriptable Objects 1 Answer
Serialize a list of scriptable objects to Json 0 Answers
How do you save a reference to a prefab's script in a ScriptableObject? 0 Answers