Avoiding Mistakes When Using Structs in C#

SwiftRoll 🐦
7 min readNov 3, 2023

--

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;
Value type “int a” is stored in the Stack
SomeClass someClass = new();

public class SomeClass
{
public int a;
}
Value type “int a” is stored inside the Reference type

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.

Console output

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;
}
Profiler

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

--

--