the elements of the list automatically upcast to inherited class

Hi, I wanted to do magic with my older UIManager. I wanted to freely store and modify different kinds of UIElements. so I used a base class(UIElements) and inherited 2 classes for text and sliders from it:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

[System.Flags] public enum UIState { start = 0x1, beforePlay = 0x2,  play = 0x4, pause = 0x8, end = 0x16 };

public class UIManager : MonoBehaviour
{
    public static UIManager Instance;
    public enum UIType {None,Text,Slider};

    [System.Serializable] public class UIElement
    {
        public string ID;
        public UIType Type;
        public GameObject gameObject;
        [SerializeField] [EnumFlags] public UIState state;
        public UIElement()
        {
            Type = UIType.None;
        }

        public void SetText(string txt)
        {
            if (Type != UIType.Text)
            {
                print("requesting Text of an UIElement of another Type");
                return;
            }
            if (((UIText)this).text != null) ((UIText)this).text.text = txt;
        }
        public void SetSlider(float amt)
        {
            if (Type != UIType.Slider)
            {
                print("requesting Slider of an UIElement of another Type");
                return;
            }
            if (((UISlider)this).slider != null) ((UISlider)this).slider.value = amt;
        }
    }

    [System.Serializable]
    public class UIText: UIElement
    {
        public Text text;
        public UIText()
        {
            Type = UIType.Text;
        }
    }


    [System.Serializable]
    public class UISlider: UIElement
    {
        public Slider slider;
        public UISlider()
        {
            Type = UIType.Slider;
        }
    }
    //public int UIElementsSize;
    //public List<string> IDs;
    public List<UIElement> UIElements;


    private void Awake()
    {
        Instance = this;
        // find or set values:
    }
    private void Start()
    {
        State = UIState.start;
        UIElements.Find(item => item.ID == "text_1").SetText("5");
    }

    public void ChangeStateByName(string stateName) //for unityevents
    {
        if (stateName == "start") State = UIState.start;
        else if (stateName == "beforePlay") State = UIState.beforePlay;
        else if (stateName == "play") State = UIState.play;
        else if (stateName == "pause") State = UIState.pause;
        else if (stateName == "end") State = UIState.end;
        else print("passed a wrong UIState name: " + stateName);
    }

    public static UIState State
    {   set
        {
            foreach (UIElement item in Instance.UIElements)
            {
                if (item.gameObject == null) continue;
                if ((item.state & value) == value)
                {
                    item.gameObject.SetActive(true);
                }
                else
                {
                    item.gameObject.SetActive(false);
                }
            }
        }
    }
}

in my editor script I tried to add different kinds of elements to the list:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.UI;

[CustomEditor(typeof(UIManager))]
public class UIManagerEditor : Editor {

    private List<UIManager.UIElement> itemsToRemove = new List<UIManager.UIElement>();

    public override void OnInspectorGUI()
    {
        UIManager theBehavior = (UIManager)target;
        EditorGUILayout.Separator();
        EditorGUILayout.LabelField("UIElements: ");
        EditorGUILayout.BeginHorizontal();
        if (GUILayout.Button("Add Base Element"))
        {
            theBehavior.UIElements.Add(new UIManager.UIElement());
        }
        if (GUILayout.Button("Add Text Element"))
        {
            theBehavior.UIElements.Add(new UIManager.UIText());
        }
        if (GUILayout.Button("Add Slider Element"))
        {
            theBehavior.UIElements.Add(new UIManager.UISlider());
        }
        EditorGUILayout.EndHorizontal();
        foreach (var item in theBehavior.UIElements)
        {
            EditorGUILayout.BeginHorizontal();
            EditorGUILayout.PrefixLabel("ID: ");
            item.ID = EditorGUILayout.TextField(item.ID);
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.BeginHorizontal();
            item.gameObject = (GameObject)EditorGUILayout.ObjectField(item.gameObject, typeof(GameObject), true);
            if (item.GetType() == typeof(UIManager.UIText))
            {
                ((UIManager.UIText)item).text = (Text)EditorGUILayout.ObjectField(((UIManager.UIText)item).text, typeof(Text), true);
            }
            else if (item.GetType() == typeof(UIManager.UISlider))
            {
                ((UIManager.UISlider)item).slider = (Slider)EditorGUILayout.ObjectField(((UIManager.UISlider)item).slider, typeof(Slider), true);
            }
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.BeginHorizontal();
            EditorGUILayout.PrefixLabel("State: ");
            item.state = (UIState)EditorGUILayout.EnumFlagsField(item.state);
            if (GUILayout.Button("Remove")) itemsToRemove.Add(item);
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.Space();
        }

        foreach (var item in itemsToRemove) theBehavior.UIElements.Remove(item);
        itemsToRemove.Clear();
    }

}

U can see everything is fine before game starts:
123934-capture.png

but after I hit the Play button, the elements of the list automatically upcast to inherited class:
123936-capture2.png

what is the reason of it? can it be fixed? any suggestions to try a different logic?

Well your problem is not upcasting as, like JVene said, that’s the expected behaviour when you store derived instances in a variable / list of a less derived type. Your main issue is that Unity’s serialization system does not support inheritance for custom serializable classes:

No support for polymorphism

If you have a public Animal animals
and you put in an instance of a Dog, a
Cat and a Giraffe, after
serialization, you have three
instances of Animal.

One way to deal with this limitation
is to realize that it only applies to
custom classes, which get serialized
inline. References to other
UnityEngine.Objects get serialized as
actual references, and for those,
polymorphism does actually work. You
would make a ScriptableObject derived
class or another MonoBehaviour derived
class, and reference that. The
downside of this is that you need to
store that Monobehaviour or scriptable
object somewhere, and that you cannot
serialize it inline efficiently.

So if you want to use polymorphism you either have to use MonoBehaviours or use ScriptableObjects. If the instances are related to gameobjects it’s usually easier to use MonoBehaviour derived classes since ScriptableObjects need to be stored explicitly as asset in the project.

The upcast you see is normal for the code given, indeed normal for the theory applied. The List itself holds only the type UIElement, and therefore the items added to that List will be stored from the type perspective of UIElement, no matter what derived type they represent. This is the expected behavior, and is the fundamental property of the concept of using a common base class for storage of various types derived from it. That is the reason this happens, it is fundamental to the language, and applies equally in similar contexts to most any other object oriented language based on the related syntax.


The underlying principle at work here is the fact that there is a common base type, UIElement in this case, which is the one point in the hierarchy of types for each of the derivatives where they can all be recognized by one type. A generic storage container can only be fashioned to store one type, and so it must be that common base type in order to facilitate storage of all the derivative objects. Naturally, as a consequence, the only type which can emerge directly from that container will be that type for which it is instantiated.


The most common, useful scenario for this storage strategy involves having that common base type declared as abstract (or virtual in other languages), so that the objects themselves, when viewed from that common base type, can determine, each on their own, what derivative type was applied. Using the technique of abstract methods (virtual functions in other language), the code using the objects in storage usually does not require and most often should not have to know what the derivative types may be. The objects coming out of the container in this strategy are operated from the base class perspective, relying upon abstract methods to perform derivative specific actions, accessing derivative specific member variables.


Students new to these concepts typically have trouble separating the derivative specific functions from the common base class so as to implement private behavior of the common base, and thus pollute the concept with derivative specific knowledge requirements from the base perspective of many or all of the derivative types. There are techniques for avoiding this problem, but they are subtle, perhaps complex and unfamiliar without experience.


Similarly, students tend to require that code using the derivative types must cast to the derivative type to gain specific operational access to the derivative type, which is only rarely actually required.


For example, in UIElement there is a member called type. It isn’t required knowledge of the common base class. This should be implemented as an abstract method, which can be called from the base class perspective, but is ‘answered’ by a derivative override, returning a constant from that method in each derived type. The ID string, on the other hand, is ‘typeless’ information pertinent to all the derivative types, and belongs in the base.


The summary point is that this isn’t problem, it is the design. You’ll need to adjust your perspective on the technique and consider abstract methods to achieve derivative type access and control. This is a technique that dates back to the origin of virtual functions in C++ (which, itself, was based on more experimental languages before it), and therefore has a long standing history, body of literature and legacy of best practices you’ll need to become familiar with in order to implement the technique.