Avoiding Mistakes When Using Structs in C#
Theory
Stack vs Heap
Both the Stack and the Heap are complex topics. Here, Iâll provide only a brief and practical explanation.
Stack
The Stack is a âlast in, first outâ (LIFO) data structure with two core functions: adding data to the top of it (push) and returning data from the top of it (pop).
When you run your program it has small memory space thatâs organized as a Stack.
- When you declare a variable inside a method itâs âpushedâ onto the Stack.
- When youâre accessing that variable later itâs accessed based on its position on the Stack.
- When youâre returning from method, the memory pushed from that method is âpoppedâ.
That also means that objects on the Stack generally have a short lifetime.
Heap
In short, the Heap is simply a large memory space inside the applicationâs memory space thatâs provided for storing various types of data.
There is also a data structure called a Heap, but it is not connected to the memory heap.
More info
- Understanding the Stack and Heap in C# https://endjin.com/blog/2022/07/understanding-the-stack-and-heap-in-csharp-dotnet
- More technical details https://en.wikibooks.org/wiki/Memory_Management/Stacks_and_Heaps
Reference type vs Value type
Difference between Reference and Value time is specified in the ECMA-334 standard:
Value types differ from reference types in that variables of the value types directly contain their data, whereas variables of the reference types store references to their data, the latter being known as objects.
Generally that also means that Reference types are stored in the Heap, and Value types are stored in the Stack or as part of some object inside the Heap.
Letâs illustrate with simple examples:
int a;
SomeClass someClass = new();
public class SomeClass
{
public int a;
}
Practice
Modification of fields
The biggest difference between a class and a struct is noticeable when you modify their fields
ClassExample();
StructExample();
void ClassExample()
{
Console.WriteLine("Classes");
SomeClass class1 = new();
class1.x = 5;
SomeClass class2 = class1;
class1.x = 10;
Console.WriteLine(class1.x);
Console.WriteLine(class2.x);
}
void StructExample()
{
Console.WriteLine("Structs");
SomeStruct struct1 = new();
struct1.x = 5;
SomeStruct struct2 = struct1;
struct1.x = 10;
Console.WriteLine(struct1.x);
Console.WriteLine(struct2.x);
}
public class SomeClass
{
public int x;
}
public struct SomeStruct
{
public int x;
}
Exactly same code for the class and for the struct. Letâs check the console.
This happens because in the line âSomeStruct struct2 = struct1;â the value of struct1 is copied to the struct2 variable. Later by changing struct1 we do not change struct2.
But assigning the class âSomeClass class2 = class1;â copies only reference. Now they both (class1 and class2) refer to the same place in the memory where instance of SomeClass is located. Thus, both of them reference the same instance, and its field is changed at the line âclass1.x = 10â
Passing as argument
It works the same when passing it as an argument to some method
void ClassExample2()
{
Console.WriteLine("Classes");
SomeClass class1 = new();
class1.x = 5;
ClassModify(class1);
Console.WriteLine(class1.x);
}
void ClassModify(SomeClass inputClass)
{
inputClass.x = 25;
}
void StructExample2()
{
Console.WriteLine("Structs");
SomeStruct struct1 = new();
struct1.x = 5;
StructModify(struct1);
Console.WriteLine(struct1.x);
}
void StructModify(SomeStruct inputStruct)
{
inputStruct.x = 25;
}
But we can pass struct as a reference too. For that weâll need to use keyword ref (or in/out)
void StructExample2()
{
Console.WriteLine("Structs");
SomeStruct struct1 = new();
struct1.x = 5;
StructModify(ref struct1);
Console.WriteLine(struct1.x);
}
void StructModify(ref SomeStruct inputStruct)
{
inputStruct.x = 25;
}
Allocation
Creating something in the Stack has a minimal performance impact.
Creating objects on the Heap incurs some small performance effects because of allocation, but the more significant impact on performance arises when these heap-allocated objects participate in the Garbage Collection cycle.
Garbage collection is a broad topic, and it is important to understand how it works in order to write performant code.
You can learn about it here https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals
Learn more about allocations
Avoiding allocations https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/performance/
Common mistakes
Collections of mutable structs
Modifying fields of non-readonly structs stored inside collections will result in compilation error.
You can still do it by creating a new struct and assigning it into the collection, but consider using a class if you need to do that often.
List<SomeClass> classList = new List<SomeClass>();
List<SomeStruct> structList = new List<SomeStruct>();
...
classList[5].x = 5; //ok
structList[5].x = 5; //compilation error
structList[5] = new SomeStruct() { x = 25 }; //ok
Arrays of structs vs Arrays of classes
If you create an array of structs, it allocates memory for the whole array and fills it with empty structs.
But if you create an array of classes it allocates memory only for references. The actual objects (instances of the class) will be allocated in the Heap separately from the array when you create them.
//classes
SomeClass[] classArray = new SomeClass[1000];
classArray[15].x = 55; //runtime error, NullReferenceException
Console.WriteLine(classArray[15].x);
//structs
SomeStruct[] structArray = new SomeStruct[1000];
structArray[15].x = 55; //ok
Console.WriteLine(structArray[15].x);
Fixing NullReferenceException by creating class instances:
for (int i = 0; i < 1000; i++)
classArray[i] = new SomeClass();
Boxing
Structs allow us to achieve better performance. But there are some hidden traps that can affect their performance. One of the biggest is âboxingââ.
Boxing is a process of wrapping a Value type and moving it to the Heap, producing an allocation.
Boxing happens when a Value type is casted to an (object) or interface.
Generally, you should avoid boxing for performance reasons.
Example case of boxing is:
SomeStruct struct1 = new();
object structObj = struct1; //boxing
More about boxing:
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing
Inheritance
Structs can inherit interfaces, but casting them to those interfaces will produce boxing, which should be avoided when possible
SomeStructWithInheritance struct1 = new();
IDisposable structDisposable = struct1; //boxing
List<IDisposable> disposables = new();
disposables.Add(struct1); //boxing
public struct SomeStructWithInheritance : IDisposable
{
public int x;
public void Dispose()
{
//some code
}
}
Lambdas
Lambdas capture Value types by using closure, itâs important to remember that because it may lead to unexpected behaviour
int x = 5;
Action lambda = () => Console.WriteLine(x);
x = 10;
lambda(); //output in the console: 10
If you want to pass only a copy of the value, you can do it like that:
int x = 5;
int y = x;
Action lambda = () => Console.WriteLine(y);
x = 10;
lambda(); //output in the console: 5
You can read more about lambdas here:
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions
Advices
Make readonly structs
To avoid mistakes when working with structs, mark your structs as readonly
SomeStruct struct1 = new(5);
struct1.x = 25; //compilation error
public readonly struct SomeStruct
{
public readonly int x;
public SomeStruct(int x)
{
this.x = x;
}
}
Pass structs by reference for optimization
Use the ref/in/out keyword to pass structs as references. It will increase performance, especially with big-sized structs
void StructPrint(in SomeStruct inputStruct)
{
Console.WriteLine(inputStruct.x);
}
Advanced
If you use Array of structs, you can also modify its elements by accessing them as ref:
SomeStruct[] structs = new SomeStruct[1000000];
ModifySimple(); //slower
ModifyAsRefs(); //faster
//slower, with copying
void ModifySimple()
{
for (int i = 0; i < structs.Length; i++)
{
//modify struct itself, produces copying
structs[i].x = 50;
}
}
//faster, by ref
void ModifyAsRefs()
{
for (int i = 0; i < structs.Length; i++)
{
//take struct as reference
ref SomeStruct structRef = ref structs[i];
//modify reference
structRef.x = 100;
}
}
struct SomeStruct
{
public int x;
public int y;
public int z;
public int w;
}
In this scenario it gives more than 2x performance boost
Thanks for this example to feralferrous
That works only with Arrays, any Collections wouldnât allow that.
Whatâs the difference between List and Array of structs?
https://levelup.gitconnected.com/modifying-struct-in-list-vs-array-6b4035b139b9
Consider âref structsâ when that struct doesnât leave the Stack
If your struct should never leave the Stack, you can use âref structâ, which would add extra restrictions on it and reduce the chance of making mistakes
List<SomeStruct> list = new(); //compilation error because List is on the Heap
public readonly ref struct SomeStruct
{
public readonly int x;
public SomeStruct(int x)
{
this.x = x;
}
}
You can learn more about ref struct and its restrictions here:
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/ref-struct
Check out my other tutorial about structs:
Optimizing Code by Replacing Classes with Structs, Unity C# Tutorial
https://medium.com/@swiftroll3d/optimizing-code-by-replacing-classes-with-structs-unity-c-tutorial-f1dd3a0baf50
Consider following me for more tutorials here or at twitter https://twitter.com/SwiftRollDev
More info
The C# type system https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/
Structure types https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct
Value types https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-types
The Truth About Value Types (blog) https://learn.microsoft.com/en-us/archive/blogs/ericlippert/the-truth-about-value-types