Hello! Welcome to your first lesson in our course on advanced C#. I'm excited to get started with you. Given your experience as a full-stack developer, you're already building sophisticated applications. Our goal now is to deepen your conceptual toolkit, enabling you to make more deliberate and expert-level design decisions in C#.
This first lesson tackles a foundational concept that underpins much of advanced C# design: the distinction between reference and value types. Specifically, we'll focus on the learning outcome: to distinguish between reference and value semantics by comparing classes and structs.
You've mentioned that performance-related topics like stack and heap allocation can be tricky. We'll approach this by building the concepts from the ground up, connecting the "what" (memory behavior) to the "why" (design and performance implications). By the end of this lesson, you'll have a clear framework for deciding when to use a class versus a struct in your own code.
1. The Two Memory Arenas: Stack and Heap
Every C# program manages its memory in two primary areas: the stack and the heap. Understanding their roles is the first step to understanding classes and structs. The stack is an efficient, ordered region of memory, while the heap is a more flexible, but less structured, pool.
The following video provides an excellent visual and conceptual breakdown of these two memory regions and how they operate.
Inside C#: Stack & Heap, Value Types, Boxing, stackalloc + More
This video from the channel ABMedia, titled "Inside C#: Stack & Heap," offers a clear and detailed explanation of how C# manages memory.
Please watch the following segments: First, get an overview from the introduction to the stack and heap. Next, dive into how the stack works from the detailed explanation. Note how it's tied to function calls (the "call stack"). Finally, learn about the heap and the role of the Garbage Collector (GC) from the heap explanation.
To summarize the key points from the video:
- The Stack: Operates in a Last-In, First-Out (LIFO) manner. It's used for storing local variables and parameters for your methods. When a method is called, a "stack frame" containing its local data is pushed onto the stack. When the method returns, that frame is popped off. This process is extremely fast and automatic.
- The Heap: A large, general-purpose memory area for objects whose lifetime isn't tied to a single method call. Allocation on the heap is more complex than on the stack. The Garbage Collector (GC) is the system responsible for automatically finding and freeing up memory on the heap that is no longer being used.
2. The Two Kinds of Types: Value vs. Reference
Now, let's connect these memory concepts to C# types. Every type in C# is either a value type or a reference type. This classification dictates how they are stored in memory and how they behave during assignment.
struct(and primitive types likeint,double,bool) are value types.class(and other types likestring, arrays, delegates) are reference types.
The article below provides a crisp definition of these two categories.
Choosing Between Classes, Records, and Structs in C# – BigO ...
This article from BigO Architecture starts by laying out the fundamental distinction between reference and value types, which is essential for our topic.
Please read the section Reference Types vs Value Types. Pay close attention to the descriptions of memory allocation, copying behavior, and how garbage collection is involved with each.
This distinction is the core of what we're learning today. Let's make it concrete:
Value Semantics (struct)
A variable of a value type directly contains its data. If it's a local variable, its data lives on the stack. When you assign one value-type variable to another, the data is copied. Each variable has its own independent copy.
Reference Semantics (class)
A variable of a reference type doesn't hold the data itself. Instead, it holds a reference (think of it as a memory address or a pointer) to an object living on the heap. When you assign one reference-type variable to another, only the reference is copied, not the object's data. Both variables now point to the exact same object on the heap.
This diagram perfectly illustrates the concept:

3. Seeing it in Action: Code and Memory
Let's observe this behavior with code. The difference between copying a value and copying a reference has profound implications for how your program works.
First, let's look at a straightforward code comparison.
Choosing Between Classes, Records, and Structs in C# – BigO ...
The same article provides a very clear, side-by-side code example to demonstrate the behavioral difference.
Study the code block under the heading Struct vs Class behavior. Notice how modifying the VectorClass instance through the vc2 variable also changes what vc1 sees. In contrast, modifying the VectorStruct instance vs2 has no effect on the original vs1.
To see this happening in a more dynamic way, the following video uses a debugger to visualize the memory allocations as the code runs.
When to Use Classes and Structs in .NET
In this video, Nick Chapsas uses the debugger to show exactly where classes and structs are allocated and what happens when they are passed to methods. This is a great way to solidify your understanding.
Watch the following segments: Heap vs. Stack Allocation: He uses the debugger's memory view to show a class instance appearing on the heap while a struct does not. Pass-by-Reference vs. Pass-by-Value: This part demonstrates how modifying a class inside a method affects the original, but modifying a struct does not, because the struct was passed as a copy.
The key takeaway is this: because multiple variables can refer to the same class instance, a change made through one variable is visible to all others. With structs, a change made to a copy is isolated to that copy. This is the essence of reference versus value semantics.
4. The "Why": Performance and Design Implications
You're a developer who wants to know not just how things work, but why you should choose one approach over another. The choice between class and struct is a fundamental design decision with significant performance trade-offs.
Allocation, Garbage Collection, and Cache Locality
As you saw, classes are allocated on the heap. Creating thousands or millions of small objects can put pressure on the Garbage Collector, which periodically has to pause execution to clean up unused memory. Structs, especially when used as local variables or in arrays, can avoid this overhead.
The next video segment uses a benchmark to demonstrate this effect. The performance difference can be dramatic.
Inside C#: Stack & Heap, Value Types, Boxing, stackalloc + More
The "Inside C#" video provides a fantastic, data-driven explanation of the performance impact, including a benchmark comparing an array of one million classes versus an array of one million structs.
Watch from the discussion on allocations to see the benchmark setup and its striking results. Also, pay close attention to the direct memory usage comparison right after, from Memory usage.
The video highlights several advantages for structs in this scenario:
- Fewer Allocations: An array of one million structs is a single, contiguous block of memory on the heap. An array of one million classes involves one million and one allocations: one for the array of references, and one for each object on the heap.
- Less GC Pressure: Fewer objects on the heap means less work for the GC.
- Better Cache Locality: Since the struct data is contiguous in the array, iterating over it is very CPU-cache-friendly. The CPU can pre-fetch memory efficiently. With the class array, the data is scattered across the heap, leading to potential cache misses as the CPU jumps from reference to object.
The Trade-Offs: Copying Cost and Boxing
This performance gain doesn't come for free. Structs have two main performance pitfalls:
-
Copying Cost: Because value types are copied on assignment or when passed to methods, large structs can be expensive. Copying a class reference is always cheap (just copying a memory address, typically 8 bytes), but copying a 64-byte struct means moving all 64 bytes. For this reason, a common guideline is to keep structs small, often suggested as under 16 bytes.
-
Boxing: This is a crucial concept. Boxing is the process of converting a value type instance into a reference type object. This happens when you, for example, cast a struct to
objector to an interface it implements. The runtime allocates a "box" on the heap, copies the struct's data into it, and gives you a reference to the box. This completely negates the performance benefits of using a struct, as it introduces a heap allocation and GC pressure.
The NDepend blog has a clear example of boxing in action.
Class vs Struct in C#: Making Informed Choices
This blog post has a concise section on other differences, including a clear explanation and code sample for boxing.
Read the subsection on interfaces and boxing. Notice how casting the struct to an interface creates a separate, "boxed" copy on the heap. Modifying the boxed version doesn't affect the original struct value.
5. Making the Right Choice: A Practical Framework
So, with these trade-offs in mind, how do you decide? Here is a practical framework to guide your design choices.
The following reading synthesizes all these concepts into an actionable checklist and provides excellent, concrete examples of good and bad usage.
Choosing Between Classes, Records, and Structs in C# – BigO ...
This final part of the BigO Architecture article offers an excellent decision-making framework and solidifies the concepts with practical examples.
Read the section Best Practices and Decision Guidelines. Focus on the numbered questions as a mental checklist for your future designs. Then, review the practical examples that follow, especially: Example 1 (Coordinates), which is a classic use case for a struct. Example 6 (Identity vs Equality), which highlights why you'd use a class even for small data when identity is important.
Here is a summary of the decision-making process:
Default to class. It is the standard, most flexible choice in object-oriented programming. Only choose struct if you have a specific, well-understood reason.
Consider using a struct when your type:
- Logically represents a single, simple value (e.g., a point, a color, a range).
- Is small (generally under 16 bytes).
- Is immutable (its state doesn't change after creation). You should almost always declare structs as
readonly structto enforce this. - Will not be frequently boxed.
You should definitely use a class when your type:
- Has identity. This is a critical concept. If two instances with the same data are still considered distinct entities (like two
Customerobjects who happen to have the same name), you need a class. Reference equality naturally models identity. - Is large or has a state that will be modified after creation.
- Needs to participate in an inheritance hierarchy. Structs do not support inheritance.
Conclusion
In this lesson, we've unpacked the fundamental difference between classes and structs in C#. You learned that this distinction is rooted in their memory behavior—classes as reference types on the heap, and structs as value types stored in-place.
Here are the key takeaways:
- Classes follow reference semantics: variables hold a reference to a shared object on the heap. Copying the variable copies the reference.
- Structs follow value semantics: variables hold the data directly. Copying the variable copies the data, creating an independent instance.
- This leads to a performance trade-off: classes have GC overhead but cheap reference copies, while structs avoid GC pressure but incur costs for copying data and can suffer from boxing.
- The choice between them is a deliberate design decision. Use
classby default, andstructfor small, immutable, value-like types where performance benefits are clear.
In our next lesson, we will explore records. Records are a modern C# feature that builds directly on these concepts. They are specialized classes or structs designed to make creating immutable, data-focused types incredibly simple and robust. This will be your next step in mastering modern C# class design.