Array fields not initialized to null

I don’t know if there’s something I’m completely missing here or if I’m going nuts, but I’m right now having an issue with array fields not being initialized to null, but to arrays with 0 elements, and this is damned scary. They are private fields, not used anywhere (in fact I just created a few of them to test this), on a plain class (not MonoBehaviour, but Serializable). What on Earth is going on?

4397545--400048--Insanity.png

This is the class file untouched, for example the fields that start as non-null are ae, go and ints, which I just created to test:

using System;
using System.Collections.ObjectModel;
using UnityEngine;

namespace Trisibo
{
    [Serializable]
    public class QuizDataEntry
    {
        #region Parameters


        /// <summary>The question.</summary>
        public QuizQuestion Question => question;
        [SerializeField] QuizQuestion question = null;


        /// <summary>All the answers.</summary>
        public ReadOnlyCollection<AnswerEntry> Answers => _readOnlyAnswers ?? (_readOnlyAnswers = new ReadOnlyCollection<AnswerEntry>(answers));
        ReadOnlyCollection<AnswerEntry> _readOnlyAnswers;
       
        [Space]
        [CollectionDrawer(elementsSeparation: 10)]
        [SerializeField] AnswerEntry[] answers = null;


        #endregion








        #region Data types


        /// <summary>
        /// An answer entry.
        /// </summary>

        [Serializable]
        public class AnswerEntry
        {
            /// <summary>The type of answer.</summary>
            public AnswerType AnswerType => answerType;
            [SerializeField] AnswerType answerType;

            /// <summary>The answer.</summary>
            public QuizAnswer Answer => answer;
            [SerializeField] QuizAnswer answer;
        }




        /// <summary>
        /// The type of answer.
        /// </summary>

        public enum AnswerType
        {
            /// <summary>The answer is wrong.</summary>
            Wrong = 0,
           
            /// <summary>The answer is correct.</summary>
            Correct = 10,
        }


        #endregion








        /// <summary>
        /// Gets the shuffled answers.
        /// Initialize or update them using <see cref="UpdateShuffledAnswers"/>, even the first time the property is accessed.
        /// The order of the answers will be the same for all accesses until the next call to <see cref="UpdateShuffledAnswers"/>.
        /// </summary>

        public ReadOnlyCollection<AnswerEntry> ShuffledAnswers
        {
            get
            {
                if (_shuffledAnswers == null)
                {
                    _shuffledAnswers = new AnswerEntry[answers.Length];
                    Array.Copy(answers, _shuffledAnswers, answers.Length);
                    _readOnlyShuffledAnswers = new ReadOnlyCollection<AnswerEntry>(_shuffledAnswers);
                }

                return _readOnlyShuffledAnswers;
            }
        }
       
        ReadOnlyCollection<AnswerEntry> _readOnlyShuffledAnswers;
        AnswerEntry[] _shuffledAnswers;
       
        private AnswerEntry[] ae;
        private GameObject[] go;
        private int[] ints;








        /// <summary>
        /// Shuffles <see cref="ShuffledAnswers"/>.
        /// </summary>
        /// <param name="maxShownAnswers">The max number of answers that will be shown. Necessary to ensure a correct answer will be shown.</param>

        public void UpdateShuffledAnswers(int maxShownAnswers)
        {
            var _ = ShuffledAnswers;  //-> Ensure that it's initialized.
            _shuffledAnswers.Shuffle();


            // Ensure there's a correct answer in the ones that will be shown:
            if (maxShownAnswers < _shuffledAnswers.Length)
            {
                int correctAnswerIndex;
                for (correctAnswerIndex = 0; correctAnswerIndex < _shuffledAnswers.Length; correctAnswerIndex++)
                {
                    if (_shuffledAnswers[correctAnswerIndex].AnswerType == AnswerType.Correct)
                        break;
                }

                if (correctAnswerIndex < _shuffledAnswers.Length  &&  correctAnswerIndex >= maxShownAnswers)
                {
                    int otherIndex = UnityEngine.Random.Range(0, maxShownAnswers);
                    var tmp = _shuffledAnswers[otherIndex];
                    _shuffledAnswers[otherIndex] = _shuffledAnswers[correctAnswerIndex];
                    _shuffledAnswers[correctAnswerIndex] = tmp;
                }
            }
        }
    }
}

I’m guessing whatever you are serializing this with is (by convention) initializing arrays with a zero length.

You can test this by turning one of your arrays into a getter/setter, making a snippet of code in the setter and putting a breakpoint in that code to see who calls the set function and makes it a zero-length array. I’m guessing it will be a call coming out of your deserializer package, whatever that is.

Actually the serializer is Unity itself. After doing some tests I think I’ve got almost the whole picture of what’s going on.

The issue comes from a part that I didn’t think was relevant so I didn’t mention, the class is being used inside a ScriptableObject, as a private field. Turns out private fields on ScriptableObjects are initialized to non-null values, as well as the members of those fields, because they are being serialized.

So marking those fields as [NonSerialized] solves the issue. But is this actually the expected behaviour? I knew about the persistence of values of ScriptableObject fields (which still gets me from time to time…), but I didn’t expect it was due to serialization. It seems backwards to what Unity usually says in documentation and the behaviour of MonoBehaviour. Even though https://blogs.unity3d.com/2012/10/25/unity-serialization/ says that “Private fields are serialized under some circumstances”, it doesn’t indicate what those circumstances are and what to expect.

Heh, yeah, I love docs like that. It does get complicated though, so I can see them not wanting to try and exhaustively list when and where things happen in all cases in a general purpose engine.

Keep in mind during the normal course of business, loading and saving your scenes and prefabs, pressing play, modifying stuff in your scene, Unity is CONSTANTLY tearing your objects down, serializing them, and then bringing them up again. Apart from saving your scene or assets to disk, every other form of persistence Unity does is via its general serialization mechanism.

I seem to recall having a problem where I had a MonoBehaviour with some field that started public, then later I changed it to private (which wouldn’t normally be serialized), but because Unity had already saved a value for a field of that name when it was public, it still had the old saved value.

I’ve also had issues where I exposed some previously-hidden variable in Unity’s inspector just so I could see its value at runtime for debugging purposes, and inadvertently changed the behavior of the game because Unity automatically converted null arrays/lists into empty ones, but the code treated those as different!

Indeed, but since the conventional wisdom is that private fields are not serialized, and the vague mention of possible exceptions is so buried in the rabbit hole, I just thought the persistence of values on ScriptableObjects was done in some other way, maybe magic™… But after all this time you always find new things you need to be very careful about.

I guess they have their reasons, but I’m not sure serializing private fields on ScriptableObjects is a good idea. It definitely makes doing certain kinds of tests on the editor extremely difficult unless you keep restarting the editor all the time. I gave up time ago trying to add any sort of logic to ScriptableObjects, certainly not recommendable.

This also applies to MonoBehaviour as well.

I have a private array of ScriptableObject in a MonoBehaviour. This array is sometime deserialized as an empty array, sometime deserialized as null.

In my tests, when a scene is loaded and the object got initially deserialized, the array is null.
When going back & forth into play mode, the array is deserialized as an empty array, unless indeed NonSerialized attribute is used.

It’s pretty consistent based on whether that identifier exists in your scene (or prefab), eg whether you have dirty-saved your scene while this field was public or serializable.

If you are using source control you can clearly see the field and reason if you want to commit the change.