Structs are lightweight, value-type building blocks that live on the stack — they're the secret to efficient, low-allocation code.
Imagine you're writing a game with thousands of bullets on screen. Each bullet has a position, velocity, and a color. If you store each bullet as a class, every bullet is a separate heap object — thousands of allocations, and the garbage collector will work overtime. But if you use a struct, each bullet lives on the stack (or inline inside an array), there's no heap overhead, and no GC pressure. That's the power of struct.
In this lesson, you'll learn exactly what structs are, how they differ from classes, when to use them, and how to write high‑performance, low‑allocation code with features like readonly struct, ref struct, and record struct.
A struct is a value type that can contain data members and methods. It's like a lightweight class — but instead of being stored on the heap, it lives on the stack (or inline inside other objects) and is copied when assigned.
In C#, a struct is a value type defined with the struct keyword. It can have fields, properties, methods, constructors, and even implement interfaces. Unlike classes, structs do not support inheritance (except from ValueType) and are sealed by default.
Key characteristics:
readonly fields or readonly struct.Classes are great for modeling complex, shared entities — but they come with overhead. Every new class instance allocates memory on the heap, which later needs garbage collection. For small, short‑lived data (like coordinates, colors, or money amounts), this overhead is wasteful. You need something lighter.
Structs provide a value‑type alternative that avoids heap allocation. They are ideal for:
Point, Color, Money)By providing structs, .NET gives you the ability to write efficient, allocation‑free code when you need it, without sacrificing the language's object‑oriented features.
Here's how structs fit into the .NET type system and memory model:
struct is the user‑defined value type. Also includes primitives (int, bool, DateTime) and enums.
class, interface, delegate, string, arrays.
readonly struct, ref struct, record struct — giving you immutability, low‑allocation spans, and value‑based equality.
public struct Point
{
public int X;
public int Y;
public Point(int x, int y) { X = x; Y = y; }
}
This defines a value type with two integer fields. It behaves like a primitive — copying it copies both X and Y.
Point p1 = new Point(10, 20);
Point p2 = p1; // p2 gets a copy of p1's values
p2.X = 99;
Console.WriteLine(p1.X); // 10 — unchanged
Both p1 and p2 are separate copies on the stack. No heap allocation, no GC.
void Move(Point p) { p.X += 10; } // modifies a copy
Point p = new Point(5, 5);
Move(p);
Console.WriteLine(p.X); // 5 — unchanged
The method receives a copy of the struct. Modifications don't affect the original.
ref)void Move(ref Point p) { p.X += 10; } // modifies original
Point p = new Point(5, 5);
Move(ref p);
Console.WriteLine(p.X); // 15 — changed!
Using ref passes the struct by reference, avoiding a copy and allowing modifications.
object obj = p; // boxing — copies the struct to the heap
Point p2 = (Point)obj; // unboxing — copies back to stack
When you assign a struct to a reference type (like object or an interface), it gets boxed — a copy is placed on the heap. This is expensive and should be avoided in performance‑critical code.
// A simple, immutable struct
public readonly struct Color
{
public byte R { get; }
public byte G { get; }
public byte B { get; }
public Color(byte r, byte g, byte b) => (R, G, B) = (r, g, b);
public Color Blend(Color other, double factor)
{
byte r = (byte)(R + (other.R - R) * factor);
byte g = (byte)(G + (other.G - G) * factor);
byte b = (byte)(B + (other.B - B) * factor);
return new Color(r, g, b);
}
}
// Usage
var c1 = new Color(255, 0, 0); // Red
var c2 = new Color(0, 0, 255); // Blue
var c3 = c1.Blend(c2, 0.5); // Purple
Console.WriteLine($"R:{c3.R} G:{c3.G} B:{c3.B}");
Code → Meaning → Result
readonly struct Color — immutable value type, all fields are read‑only.Blend returns a new Color — no mutation, thread‑safe.Imagine a financial application that processes thousands of transactions per second. A Transaction struct can be lightweight and avoid GC pressure.
public readonly struct Transaction
{
public int Id { get; }
public decimal Amount { get; }
public DateTime Timestamp { get; }
public string Description { get; } // string is a reference type, but the struct only holds a reference
public Transaction(int id, decimal amount, DateTime timestamp, string description)
{
Id = id;
Amount = amount;
Timestamp = timestamp;
Description = description;
}
public Transaction ApplyDiscount(decimal percent) =>
new Transaction(Id, Amount * (1 - percent / 100), Timestamp, Description + " (discounted)");
}
// Usage in a high‑throughput processor
var tx = new Transaction(1024, 249.99m, DateTime.UtcNow, "Order #123");
var discounted = tx.ApplyDiscount(10);
// No heap allocations for the struct itself — only the string (reference) is on the heap.
// The struct is passed and copied efficiently.
Why this works:
Transaction struct is immutable — each "modification" returns a new instance.Description string is a reference type, but the struct only holds a reference; the string itself lives on the heap, but that's unavoidable for variable‑length text.Think of a struct as a sticky note. You write a small amount of information (e.g., "meeting at 3 PM") on it. If you need another copy, you just write it again on a new sticky note. They are independent. They are cheap to produce and easy to throw away.
A class is like a notebook. Multiple people can share the same notebook (by pointing to it). If someone writes in it, everyone sees the change. Notebooks are more expensive to produce and require more management (like garbage collection).
readonly struct = A laminated sticky noteOnce written, you can't change it — you have to create a new one. This avoids accidental modifications and makes sharing safer.
What happens inside the .NET runtime when you work with structs?
Stack: [Point p] → (X:10, Y:20) stored directly
No separate heap object. The struct's fields are inlined into the stack frame.
Point p2 = p1; → IL: ldloc p1, stloc p2 (copies bytes)
The runtime performs a bitwise copy of the struct's memory.
void Method(Point p) → copies p onto the stack of the called method
The entire struct is copied to the method's stack frame.
object o = p; → allocates on heap, copies struct, updates o to point to it
Boxing creates a heap object that contains a copy of the struct. Unboxing copies it back.
readonly struct OPTIMIZATIONSThe compiler enforces immutability and avoids defensive copies. It also inlines methods and reduces overhead.
Not necessarily. For large structs (e.g., > 16 bytes), copying them around can be more expensive than passing a reference. Also, boxing (e.g., when casting to object) defeats the performance benefit. Profile your code before assuming structs are always faster.
False. Structs can have methods, properties, events, and even implement interfaces. They are first‑class types, just with value semantics.
Mostly true but not always. Structs that are fields of a class live on the heap (inside the class object). Also, boxed structs live on the heap. So while the default is stack, they can end up on the heap in certain contexts.
If you have a struct property in a class, accessing it returns a copy. Modifying that copy does not affect the original. This is a common pitfall:
class Container
{
public Point Location { get; set; }
}
var c = new Container();
c.Location.X = 10; // This modifies a copy! Does not update the stored struct.
Fix: Assign the entire struct: c.Location = new Point(10, c.Location.Y);
Wrong: Using a mutable struct in a collection and modifying an element.
List<Point> points = new();
points.Add(new Point(1, 2));
points[0].X = 10; // This modifies a copy! The list still has the original.
Correct: Make structs immutable, or replace the entire element: points[0] = new Point(10, points[0].Y);
Wrong: Creating a struct with 10+ fields. Copying it becomes expensive.
Correct: Use a class if the struct is large (typically > 16 bytes).
readonly when appropriate Wrong: Allowing mutation when the struct is logically a value object (like Color).
Correct: Use readonly struct and readonly fields to enforce immutability and avoid defensive copies.
Equals and GetHashCodeStructs get a default Equals that uses reflection — slow. Always override them for performance, especially if you use structs in dictionaries or hash sets.
In C# 10+, you can use record struct to get value-based equality automatically.
DateTime, Guid).ref everywhere.readonly struct — immutable, prevents modification and improves performance.ref struct — stack‑only struct that cannot be boxed; used for spans and high‑performance scenarios.record struct — value type with built‑in value‑based equality and ToString.readonly struct for immutability and performance.record struct for value-based equality without boilerplate.ref parameters to avoid copying large structs when you need to modify them.object or interfaces in hot paths.You've seen how structs provide value semantics and stack allocation. Let's test your understanding with some real‑world scenarios.
1. Which of the following is a valid reason to choose a struct over a class?
Correct: C
Why C is correct: Structs are ideal for small, immutable data that is created often — they avoid heap allocations and GC pressure. This matches the recommendation for struct usage.
Why A is incorrect: Structs do not support inheritance. If you need inheritance, use a class.
Why B is incorrect: Large structs are expensive to copy; a class would be more efficient because only the reference is copied.
Why D is incorrect: Both structs and classes can be passed by reference using ref. This is not a reason to choose a struct.
Reinforcement: Use structs for small, immutable, and frequently allocated data to maximize performance.
2. What is the output of the following code?
struct Point { public int X; }
class Wrapper { public Point P; }
var w = new Wrapper();
w.P.X = 5;
Console.WriteLine(w.P.X);
Correct: A
Why A is correct: The Point struct is stored as a field inside the Wrapper object on the heap. Accessing w.P returns a copy of the struct, but then modifying .X on that copy does not update the stored struct. However, the code as written does not produce an error; it simply does nothing. The w.P.X after assignment is still 0 because the struct defaults to 0. But the question asks for the output, and since we assigned w.P.X = 5; that actually works? Wait: in C#, w.P is a property getter if P is a field, it's accessible directly. Actually, the code defines public Point P; as a field. So w.P.X = 5; modifies the field directly, because the struct is a field. That actually works. The struct is stored inline inside the Wrapper object on the heap, and we are modifying the field directly, so the change is persisted. So the output is 5. Good catch. So answer A is correct.
Why B is incorrect: 0 would be the default if no assignment happened, but we did assign 5.
Why C is incorrect: This code compiles.
Why D is incorrect: Structs are not null.
Reinforcement: When a struct is a field of a class, it lives on the heap inside the class object, and you can modify its fields directly if they are mutable. However, it's still a value type and copying occurs when you retrieve it as a value.
3. What does readonly struct guarantee?
Correct: B
Why B is correct: A readonly struct ensures that all instance fields are readonly, and the compiler enforces that no method modifies the struct's state. This makes it immutable.
Why A is incorrect: readonly struct can still be boxed if cast to object or an interface.
Why C is incorrect: Readonly structs can have performance benefits because the compiler avoids defensive copies, but it's not a guarantee of being "faster" in all scenarios.
Why D is incorrect: Structs can implement interfaces, even readonly struct.
Reinforcement: readonly struct is a contract for immutability, which improves correctness and can enable optimizations.
4. Which of the following is a valid use of a ref struct?
Correct: C
Why C is correct: ref struct types like Span<T> can be passed as parameters to methods. They are stack‑only and cannot be boxed or stored on the heap.
Why A is incorrect: ref struct cannot be a field of a class because that would put it on the heap.
Why B is incorrect: Async methods can return ref struct if they are not async? Actually, ref struct cannot be used in async methods because they cannot be stored in the state machine. So B is incorrect.
Why D is incorrect: ref struct cannot be boxed.
Reinforcement: ref struct is a stack‑only type designed for high‑performance scenarios like spans. It has strict limitations.
5. Which of the following statements about boxing is true?
Correct: C
Why C is correct: Boxing occurs when a value type is assigned to a reference type variable. The runtime allocates a heap object, copies the struct's data into it, and returns a reference to that object.
Why A is incorrect: Boxing copies to the heap, not the stack.
Why B is incorrect: Boxing is generally slower because of heap allocation and extra indirection; it should be avoided in performance‑critical code.
Why D is incorrect: Structs are boxed when cast to object or an interface they implement.
Reinforcement: Boxing is an expensive operation that converts a value type to a reference type. Avoid it in hot paths.
You now have a solid understanding of structs — from stack allocation to immutability and performance!
dotnetmadeeasy.com — Learn C# and .NET, the right way.