I think your confusion comes from the fact that there are different “kinds” of memory and when we talk about allocations we specifically talk about memory on the managed heap. The managed heap is controlled and managed by the memory manager / garbage collector. Any reference type lives on the heap. That’s true for all reference types that includes strings. Strings are not value types as you said. Strings are objects and live on the managed heap like all the other reference types.
Value types on the other hand live directly inside the memory of the variable itself. Where that is can vary depending on where the variable is declared. However as soon as the variable exists, the memory that holds the value type exists. So storing a value type inside a class means the data of the value type actually lives on the heap. However it can not be collected or freed independently since the variable belongs to a class that lives on the heap. So as long as the class exists, the variable would exist and you can store and read data in that variable and it won’t allocate any additional memory.
Local variables, which includes method arguments, live on the stack. The stack is NOT managed memory. The stack is a continuous memory section of a fix size. When you enter a method, the stack pointer is just moved to a different place on the stack to reserve a certain memory portion. When you exit that method, the stack pointer is moved back and the whole stack frame is “gone”.
So structs on their own do not allocate managed memory. An array or Dictionary are objects and live on the heap. The creation of an array or Dictionary of course requires an allocation of memory on the heap. An array of value types / structs also live on the heap. So creating an array of size x would allocate memory on the heap. As long as you re-use that array, no additional allocations are taking place.
A Dictionary stores the actual elements inside an internal array. That internal array has a certain length which is the capacity of the dictionary. When that capacity is exceeded, the dictionary would create a new array with a larger size and the old array would be up for garbage collection. Though as long as the dictionary has a large enough capacity to hold all elements, no new garbage / memory would be generated / allocated when you add or remove elements.
Reading and writing values types means that you actually copy the data of that values type from one place to another. As I said earlier, local variables live on the stack and do not allocate memory. Though keep in mind when you “pass around” value types, you always copy the whole content of that value type. That’s why value types should be kept small. Having value types that are several hundreds of bytes in size would not be great for performance as the whole data would be copied all the time.
Iterating through a dictionary with foreach does not allocate memory as the generic Dictionary class as well as the List class have struct enumerators. A common misconception is that foreach works on the IEnumerable / IEnumerator interfaces. However that’s not true. In C# the foreach loop actually uses pattern matching during compile time. So it looks for a method called GetEnumerator on the given object / struct and checks if the returned type has a MoveNext method that returns a boolean and if it has a Current property. That’s all. If it has the compiler will directly call that method, stores the result in a local variable and runs MoveNext in a loop. So a type does not need to implement the IEnumerator / IEnumerable interfaces in order to be used in a foreach loop. (In the past Unity uses an older version of Mono that did allocate memory for struct enumerators because it had a faulty implementation. That’s because IEnumerator implements IDisposable and mono boxed the enumerator just to call Dispose on it).
Note that it makes no sense to declare temporary variables as member variables of a class. They would be fix part of the memory footprint of the class. Just use local variables when you want to store things temporarily. Local variables are implicitly reserved all at once on the stack when you enter a method and are released automatically when you leave the method.
I don’t get where you got the idea from that there are any memory allocations in the code you’ve shown. It makes no difference if the types “Guns” and GunDeck are a structs or classes. Nothing in that code snippet allocates memory since the actual instances already exists. So there is no “optimisation” here, at all.
Passing by value / reference Passing by value / reference
Another common misconception is the difference between passing a variable “by reference” or “by value”. This has been taught and explained wrong for decades, even by some credible sources. A lot confusion comes from comparison to other languages which don’t have the concept of passing by reference like javascript for example. You will still find countless of articles which talk about “passing by reference” in javascript, but that’s not really a thing. Passing a variable by reference is not the same as passing a reference to a reference type instance around.
I just found a great article that talks about how javascript does not support passing by reference. It does a great job explaining the basic difference better than my following paragraphs ^^
In C# ( and C and C++) by default, arguments of methods are passed by value. This is true regardless of the variable type. Value type as well as reference type variables are all passed by value. The key thing you have to understand that we talk about variables here. The value of a reference type variable is the reference that is stored in that variable. That could be a reference to an object or null. When you pass a variable as an argument to a method, you actually copy the value of that variable into a local variable of that method. Just to make that clear: On a 64bit system a reference or pointer has 64 bits, so consists of 8 bytes. So a reference type variable occupies 8 bytes whereever it’s declared. If it’s a member variable of a class, 8 bytes in the memory of that class are designated to hold a reference to some other object. If it’s a local variable 8 bytes on the stack would hold that reference. So inside the method you have a local variable on the stack that holds a copy of what was stored in the variable that got passed into the method.
Passing by reference on the other hand can only be done through the ref or out keyword. When you pass a variable “by reference” it means that we do NOT copy the value of the original variable into the local variable of the method, but an actual pointer to where the original variable is stored. So an actual pointer to the memory where the variable itself is stored. So inside the method the local variable acts like an alias name for the variable that was passed in. The big difference between passing by reference and passing by value is that when you pass by reference, the method can actually change the content of the variable that was passed to the method.
Yes, passing a reference to a class instance either by value or reference both allow the method to “follow the reference” to reach out to the referenced object. However that’s not what passing by reference is about. That’s just how references work in general. Assigning a one reference variable to another reference variable also just copies the value (the reference itself) into the other variable.
In C and C++ we would talk about “double pointers” or pointers to pointers. Though that’s kinda off topic as we talk about C# here.