← Open in the full interactive course (progress tracking, search & more)

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.

What Is It?

The Simple Explanation

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.

The Technical Definition

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:

Aspect struct (value type) class (reference type)
StorageStack (or inline)Heap
AssignmentCopies the entire valueCopies the reference
InheritanceNo (sealed)Yes
Default constructorAlways present (can be custom in C# 10+)Compiler-provided if none defined
NullabilityCannot be null (unless nullable)Can be null
GC overheadNoneManaged by GC
PerformanceFast, no heap allocationSlower due to heap allocation and GC
Use caseSmall, immutable, frequent creationLarge, mutable, shared objects

Why Does It Exist?

The Problem

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.

The Solution

Structs provide a value‑type alternative that avoids heap allocation. They are ideal for:

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.

Big Picture

Here's how structs fit into the .NET type system and memory model:

STRUCTS IN THE .NET ECOSYSTEM
Value Types
struct is the user‑defined value type. Also includes primitives (int, bool, DateTime) and enums.
Reference Types
class, interface, delegate, string, arrays.
Memory & Performance
Structs live on the stack → fast allocation, no GC. Classes live on the heap → slower, GC managed.
Modern Features (C# 10+)
readonly struct, ref struct, record struct — giving you immutability, low‑allocation spans, and value‑based equality.

How It Works

Step 1 — Defining a struct

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.

Step 2 — Allocating on the stack

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.

Step 3 — Passing to methods (by value)

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.

Step 4 — Passing by reference (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.

Step 5 — Boxing (when value types become reference types)

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.

Simple Example

// 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

Real-World Example

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:

Analogy

Struct = A sticky note

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.

Class = A notebook

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 note

Once written, you can't change it — you have to create a new one. This avoids accidental modifications and makes sharing safer.

Under the Hood

What happens inside the .NET runtime when you work with structs?

INTERNAL BEHAVIOR OF STRUCTS
1. STACK ALLOCATION
Stack:  [Point p] → (X:10, Y:20) stored directly

No separate heap object. The struct's fields are inlined into the stack frame.

2. COPY ON ASSIGNMENT
Point p2 = p1; → IL: ldloc p1, stloc p2 (copies bytes)

The runtime performs a bitwise copy of the struct's memory.

3. PASSING BY VALUE
void Method(Point p) → copies p onto the stack of the called method

The entire struct is copied to the method's stack frame.

4. BOXING
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.

5. readonly struct OPTIMIZATIONS

The compiler enforces immutability and avoids defensive copies. It also inlines methods and reduces overhead.

Common Confusion

1. "Structs are always faster than classes"

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.

2. "Structs cannot have methods or properties"

False. Structs can have methods, properties, events, and even implement interfaces. They are first‑class types, just with value semantics.

3. "Structs are always allocated on the stack"

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.

4. "Modifying a struct property returns a copy"

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);

Common Mistakes

Mistake 1 — Mutable structs in collections

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);

Mistake 2 — Defining a struct with many fields

Wrong: Creating a struct with 10+ fields. Copying it becomes expensive.

Correct: Use a class if the struct is large (typically > 16 bytes).

Mistake 3 — Not marking structs 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.

Mistake 4 — Forgetting to override Equals and GetHashCode

Structs 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.

When Should I Use It?

Use a struct when:

Avoid structs when:

Modern struct variants:

Mental Model

Struct = a lightweight, value‑based data container.
Stack = where it lives (fast, no GC).
Copy = what happens on assignment.
Immutable = preferred for correctness and performance.

Rule of thumb:
· Small, immutable, and frequently created → struct
· Large, mutable, shared → class
· When in doubt, start with a class — it's easier to change later.

Key Takeaway


Check Your Understanding

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?

Show answer

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);
Show answer

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?

Show answer

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?

Show answer

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?

Show answer

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.