- Home /
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 };
}
Answer by Bunny83 · May 15, 2018 at 02:15 AM
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.
Great explanation! I didn't know about the order of initialization and how it played a part here.
Answer by Tristan-Moore · Jan 07, 2021 at 08:34 PM
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:
If you initialize types like strings or arrays in
Start()
orAwake()
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.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.
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.
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 justGameObject go = null;
orList<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 aStart
orAwake
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.
Sorry but your answer is kinda misleading because your point 1 doesn't make much sense. What does the garbage collector has to do with the initialization of variables? In both cases a an int array will be created on the heap. It will not be collected until the field is overwritten or when the object instance that holds this field is itself collected. Second Start as well as Awake does not run multiple times per object. Actually inside Unity constructors and field initializers may run more often especially in the editor whenever an assembly reload happens and the objects are recreated. Having a field initializer on a serialized array field will actually produce more garbage as the result of the field initializer will be overwritten by the deserialization of the serialized values. As I said for fields that are not serialized you will see almost no difference.
I'm also not sure what you mean by
[...] the class declaration, which is handled only once.
We talk about instance fields here. The field initializer as well as any code in the constructor will also be executed for every single object. Those are not static fields. Yes, static field initializers or the static constructor will only ever run once per type. However those do not belong a class instance.
About your point 2: Not Unity is outputting warnings, the C# compiler does. Unity would suppress those warnings if it could reliably because Unity knows that fields marked with SerializeField are initialized through deserialization. The warning will only appear when you have a private field that is "never" written to. Since private fields can not be accessed legally from anywhere else except the own class, the compiler performs a static code analysis. If it detects that you read that variable but never writing to it, you get this warning. However if you have any code somewhere that actually writes something to the variable (that code does not need to run, it just need to exist) the warning should be gone. You also do not get this warning for protected fields (when the field is actually serialized, protected would be the recommended visibility anyways). If the field is not serialized the warning is actually justified. Blindly initializing a variable with nonsense just hides the warning. The warning is a development tool that should inform you about a potential logical error.
ps: If you don't believe me, try this class:
public class TestClass : $$anonymous$$onoBehaviour
{
private string someField = GetString();
private static string GetString()
{
Debug.Log("GetString was called");
return "SomeString";
}
void Start()
{
Debug.Log("Start was called");
}
}
Just attach this script to two or more objects in the scene. How often do you think you will see the "GetString was called" message? Field initializers usually run way more often in the Unity editor than what you may think. However it's always called once for every instance, that's for sure ^^.
@Bunny83 I'm a self taught programmer who doesn't usually post, because I don't want to get blasted for misstating something, but I also do performance optimization professionally. I did a direct comparison between two variations of simple scripts in a variety of scenarios like the ones I was referring to, using a string containing 1KB of lorem ipsum. https://imgur.com/91uTt5S
SCRIPT A:
public class InitializeStringInStart : $$anonymous$$onoBehaviour{
private string someField;
void Start() {
someField = "Lorem ipsum dolor sit amet, "; //NOT INCLUDING ALL OF IT HERE, 1KB IN SIZE
}
}
SCRIPT B:
public class InitializeStringInConstructor : $$anonymous$$onoBehaviour
{
private string someField = "Lorem ipsum dolor sit amet, "; //NOT INCLUDING ALL OF IT HERE, 1KB IN SIZE
}
In every single scenario, in the editor with 1500 objects, in the editor with 10,000 objects, in a build with 10,000 objects, instantiating in a build with 100,000 objects, and loading scenes additively, initializing in Start produced more overhead. Is it a lot? Not in most reasonable use cases, but it's clearly there when stress testing. What's the point in calling a method to initialize a private variable that is just as easily initialized when it's declared? I already stated in my origin post (the one where I said you were basically correct, btw) that this mainly applies to scenarios where you are doing things like creating immutable types like strings.
@Bunny83 "What does the garbage collector has to do with the initialization of variables?" The issue isn't always how much memory is assigned, it's where it is stored in Unity's non-compacting heap, and whether or not it produces fragmentation. If you load or instantiate and then destroy a bunch of objects, you're going to get fragmentation over the life cycle of the application no matter what, but deferring the initialization of "real" values until a method call later in the application lifecycle increases those odds. There are a lot of stages from initial class instantiation to Start()
with method calls and allocations of their own, meaning there are additional opportunities for your heap to become fragmented. Add in method call overhead, and the fact that you might only have Start()
to initialize variables you could already have initialized without it, and you're just costing yourself. This really has made a difference in projects I've worked on, including open-world and VR titles where we have razor-thin margins for overhead.
"Having a field initializer on a serialized array field will actually produce more garbage as the result" As far as I know, this is totally incorrect. Fields in the class scope in C# are automatically initialized with their default values, whereas the compiler fully disallows unassigned local variables. When an instance of a class is produced, it gets memory allocated for reference type pointers and value types using the default value for a type, or the value you initialize it to be. GameObject go;
and GameObject go = null;
produce the same null value, only the second one eli$$anonymous$$ates erroneous compiler warnings, shows you what the value actually is if not overridden by a serialized object, and allows sensible defaults to be set in the case of value types. If you don't care about any of that, fine, I wasn't attempting to get into a "best practices" debate, but unless you can point me to where my understanding of allocation is wrong, my statement was factually accurate and your critique is not.
"We talk about instance fields here. The field initializer as well as any code in the constructor will also be executed for every single object." I agree I stated this poorly; I was referring to static members which are only initialized once. A common usage for this is defining static const strings that can be shared across instances, which is why I said "if it never changes." However, even if not static, using Start or Awake to initialize does in fact initialize it twice, once with the default and then again with your value. OnEnable, Start, and Awake have very deliberately designed purposes, like when you need external data or communication, not just to do something you could do elsewhere and cheaper.
Well you got some of my points also wrong ^^. The additional garbage I mentioned was about in editor use because objects are more often recreated. Of course when we talk about garbage and memory allocations in relations to fields we don't talk about the memory of the variable, but its content. And this of course only applies to reference types which we were talking about. Of course initializing them with null has no effect and makes no difference. I specifically was referring to your array example. Note that fields of a class are not initialized with their default values explicitly. The memory of the class is simply zero filled. A zero filled memory is always the default value of any type. That's why you can not change the default value for valuetypes. So using field initializers also initialize the variable twice in the same sense.
I highly doubt that your first example has any difference because if you have a literal string it's interned and all those 100k objects would reference the same string instance anyways, in both cases. However if the string you talk about is dynamically loaded, of course you get a new string for each instance. However this would be the same if you did the dynamical creation in the field initializer or in Awake / Start. Note that Awake is as close as it can get to a constructor. It's called pretty close to the actual object creation. So your argument about memory fragmentation seems that it may only apply to very very specific scenarios and you most likely can come up with an opposite case depending on which objects are actually up for garbage collection and which should persist.
Don't get me wrong, I'm not against using field initializer where it makes sense. However field initializers which have no effect or create unnecessary garbage are pretty pointless. Again the whole issue with the warning does only exist when you have serialized private fields. Fields that are not serialized should get a warning when you never assign any value. Initializing serialized fields can and have often caused huge confustion by many people because the values are always overwritten by the deserialized values.
The results here are not at all surprising if you know anything about method call overhead. Unity even did a talk about it. I'm currently working on a game where I'm solving for this exact problem. This isn't new. $$anonymous$$ethod call overhead even shows with empty methods. I think you need to look up how strings work. I literally did a test of this and told you what it does, and read all of the profiled stats including memory allocation, garbage collection, and frame time. It sounds like you just "guessed." So either you don't know what you're talking about or I can't see what point you're trying to make.
Your response shows that you know I'm right though, because you went from not even understanding how garbage collection relates to variable initialization to saying "well YEAH, everyone knows THAT, but how big of a deal is it really?" Not generally that big, which is why I said that in my totally non-confrontation supplement to your answer that started this. However, it doesn't take a genius to realize that, say, additively loading a sub-level with 2000 objects would initialize all of the variables and pointers on load except for what runs in Start, which would all be initialized separately afterword. Just that, takes data organized "AAAA,BBBB,CCCC," and makes it more like "ABCABCABCABC," so that if you delete one object, you get a heap that looks like AB_AB_AB_AB_" instead of "AAAABBBB_ _"
To be honest, this actually is a really hard subject, and I don't expect really hardly anyone to know it, myself included, without a lot of research and investigation. I've had to do a lot of digging and come to places like this all the time learn more. I even learned a lot today, in fact. After doing hours of vetting I thought "I'll just make one post to give something back, since I was just doing this and it might help someone." What I am always amazed by is how quickly people on the internet who don't know what they are talking about jump to insult other people for that impulse. I don't even have the desire to be an unsolicited jerk when I see people say something really stupid, but you jumped into this two year old thread within $$anonymous$$utes of me posting to aggressively demonstrate the Dunning-Kruger effect. Nice job. I'll probably not be posting again any time soon.