Understanding Structs
When we dive into the world or memory optimizations you’d likely hear the struct
keyword. As codebases
became larger and I had to start thinking about memory optimizations due to classes becoming large and I notice
there were many objects and there were some sets of data being garbage collected - I found myself writing structs when I need to.
When you create a struct, its memory is allocated on the stack. This makes structs more efficient than classes, which are allocated on the heap. This means that structs are more suitable for functions that require high performance and low memory usage. That is cheaper. No garbage collection needed.
On the other hand, classes are allocated on the heap, which means they have no size limit. This makes them more suitable for complex data structures that require a large amount of memory.
Here is another useful doc to read: Link
Keep in mind that if you take a struct and pass it to a function. Change a value in the struct inside that function. Is the value changed after the call to the function? No, you passed a copy of the struct, not a reference. (you CAN pass a reference, but you have to do that explicitly). They behave different from “normal objects” (reference types) under assignment and when passing as arguments, which can lead to unexpected behavior; this is particularly dangerous if the person looking at the code does not know they are dealing with a struct.
Other factors that you might not have in structs are:
- they cannot be inherited.
- struct members cannot be specified as
abstract
,sealed
,virtual
, orprotected
.
So structs are pass by value by default. That is a difference.
How to use Structs
1public struct Point2{3int x, y;45public Point(int x, int y)6{7this.x = x; this.y = y; // OK8}9}
If we added the following constructor, the struct would not compile, because y
would remain unassigned:
1public Point (int x) { this.x = x; } // Not OK
The Default Constructor
In addition to any constructors that you define, a struct always has an implicit parameterless constructor that performs a bitwise-zeroing of its fields (setting them to their default values):
1Point p = new Point(); // p.x and p.y will be23public struct Point4{5int x, y;6}
Even when you define a parameterless constructor of your own,
the implicit param‐ eterless constructor still exists and can be accessed via the default
keyword
1Point p1 = new Point(); // p1.x and p1.y will be 12Point p2 = default; // p2.x and p2.y will be 034public struct Point5{6int x = 1;7int y;8public Point() => y = 1;9}
readonly
structs
You use the readonly
modifier to declare that a structure type is immutable.
All data members of a readonly
struct must be read-only as follows:
- Any field declaration must have the readonly modifier
- Any property, including auto-implemented ones, must be readonly or init only. That guarantees that no member of a readonly struct modifies the state of the struct.
1// Use init2public readonly struct Coords3{4public Coords(double x, double y)5{6X = x;7Y = y;8}910public double X { get; init; }11public double Y { get; init; }1213public override string ToString() => $"({X}, {Y})";14}1516// Or use readonly17internal readonly struct Details18{19public readonly string Title;2021public readonly IReadOnlyDictionary<Guid, Employee> PerEmployeeInfo;2223public Details(string title, IReadOnlyDictionary<Guid, Employee> perEmployeeInfo)24{25// ...26}27}2829public static void Main()30{31var p1 = new Coords(0, 0);32Console.WriteLine(p1); // output: (0, 0)3334// Non-destructive Mutation:3536var p2 = p1 with { X = 3 };37Console.WriteLine(p2); // output: (3, 0)3839var p3 = p1 with { X = 1, Y = 4 };40Console.WriteLine(p3); // output: (1, 4)41}
ref
structs
Ref structs were introduced in C# 7.2 as a niche feature primarily for the benefit
of the Span<T>
and ReadOnlySpan<T>
structs. These structs help with a micro-optimization technique that aims to reduce memory allocations.
Unlike reference types, whose instances always live on the heap, value types live in-place (wherever the variable was declared). If a value type appears as a parameter or local variable, it will reside on the stack:
1void SomeMethod()2{3Point p; // p will reside on the stack4}56struct Point { public int X, Y; }
But if a value type appears as a field in a class, it will reside on the heap:
1class MyClass2{3Point p; // Lives on heap, because MyClass instances live on the heap4}56struct Point { public int X, Y; }
Adding the ref
modifier to a struct’s declaration ensures that it can only ever reside on the stack. Attempting to use a ref struct in such a way that it could reside on the heap generates a compile-time error:
1var points = new Point[100]; // Error: will not compile!23ref struct Point4{5public int X, Y;6}78class MyClass9{10Point P; // Error: will not compile!11}
Real world Application
Here are some examples of how I use structs:
1public struct Capacity2{3public List<double> Factors { get; set; }45public DateTimeOffset ExpiryDate { get; set; }67public Capacity(List<double> factors, DateTimeOffset expiryDate)8{9Factors = factors;10ExpiryDate = expiryDate;11}12}1314public readonly struct SeatsInfo15{16[JsonProperty]17public readonly double Total;1819[JsonProperty]20public readonly double Available;2122internal SeatsInfo(double total, double available)23{24// validations...2526Total = total;27Available = available;28}29}303132// You use the readonly modifier to declare that a structure type is immutable.33// All data members of a readonly struct must be read-only as follows34internal readonly struct Details35{36public readonly string Title;3738public readonly IReadOnlyDictionary<Guid, Employee> PerEmployeeInfo;3940public Details(string title, IReadOnlyDictionary<Guid, Employee> perEmployeeInfo)41{42// ...43}44}
I hope this was useful!
x, gio