Array Declaration & Initialization- Inside and Outside Functions

What is the difference in declaring arrays at class scope and then initializing the container in the Start() method, and just declaring & initializing at class scope?

int[] numbers = new int[] { 1, 2 };

versus

int[] numbers;

// Use this for initialization
void Start () {
    numbers = new int[] { 1, 2 };
}

Well since your array is private it is not serialized so there’s actually little difference. It’s only about the order in which things get initialized. When you initialize your array in a field initializer it’s actually initialized before the constructor of the class is called. So even before Awake is called. Start is called relatively late from that perspective as Start is called right before the object would receive it’s first Update call. If the object is instantiated at runtime this could be even the next frame.

So when initializing the array in Start it would mean it is still null inside Awake and it’s also null right after you instantiate a prefab at runtime.

It gets more interesting when your variable is serialized. So when you declare a variable public (or you use the SerializeField attribute) the field initializer becomes rather unimportant. It’s only used when you create the object in the editor for the first time. After it’s created the values are serialized. At runtime when the object is loaded the field initializer is still executed, however the serialization system kicks in right after the object has been created but before Awake is called and it will overwrite whatever you set in the field initializer with the values that got serialized. The serialized values are those you see in the inspector.

Initializing fields in OnEnable, Start, or Awake provides no benefit if those contexts aren’t necessary for the data, such as communication between objects or timing-based calls. It also has some disadvantages:

  1. If you initialize types like strings or arrays in Start() or Awake() instead of initializing it in the class declaration scope, there will be a small runtime penalty for the Start() or Awake() method itself. Method call overhead is usually not a big deal, but does show up when being stress-tested with thousands of calls.

  2. Delaying proper initialization until method calls that take place after instantiation also increases the chance of fragmenting the heap, which can cause the garbage collector to run more often. This risk increases the more objects are created and destroyed, as you can create a chunk of pointers and data for several new objects all at once, but then fragment that data by initializing some of the fields with “real” data that is created during the later method call, thanks to order of execution. This is always somewhat unavoidable, but the more you increase that risk by staggering memory allocations that you didn’t have to, the more the garbage collector will run and the more risk there is of increasing the size of the heap.

  3. you can make use of static, const, and readonly variables if you initialize them in the class-scope declaration, and in some cases this can be a win for performance and clarity, such as a static const string to define the name of an animation parameter that won’t change between instances.

  4. The Unity serialization system can’t share serialized data to the compiler, which produces compile warnings for public or serialized fields whose values are set in the Inspector and not code. These warnings can be suppressed with #pragma instructions, but this type of warning suppression is usually not a good idea, as it can also suppress needed warnings. Since all C# class-scope variables are automatically initialized as defaults, it’s often a good practice to write out the initialization explicitly, even if it’s just GameObject go = null; or List<int> myInt = new List<int>();. This suppresses the warning, does not lead to any performance penalty, doesn’t require a method call to initialize, and shows clearly what the default value actually is. Plus, it has the benefit of allowing you to set sensible defaults for serialized value types. The habit of initializing a default when declaring is a good one to get into for those reasons, but does not need to be used in cases where a Start or Awake call is always going to be required, like when filling a value with GetComponent. Even then, though, the cheapest methods are the ones that are never run, so try to figure out why you’re using it and if you actually have to.

As people often say, premature optimization is wasteful, and the performance impact is negligible in most cases. However, these are the differences, and it’s relevant in cases where someone might be haphazardly preferring initialization in a sub-optimal way that produces more lines of code and is harder to write, just to incur a performance penalty.