You are viewing a preview of this lesson. Sign in to start learning
Back to Mastering Memory Management and Garbage Collection in .NET

Value vs Reference Types

Semantic differences, boxing costs, and when each type matters

Value vs Reference Types in .NET

Understanding the difference between value and reference types is fundamental to mastering memory management in .NET. Use our free flashcards with spaced repetition to cement these concepts. This lesson covers memory allocation differences, behavior during assignment and method calls, and performance implicationsβ€”essential knowledge for writing efficient C# applications.

Welcome to Memory Type Fundamentals πŸ’»

Every variable you create in .NET falls into one of two categories: value types or reference types. This distinction determines where data lives in memory, how it behaves when copied, and how the garbage collector interacts with it. Getting this wrong leads to subtle bugs, performance issues, and confusing behavior.

Think of it this way: value types are like sticky notes with information written directly on them, while reference types are like sticky notes with directions to where the real information is stored. Both are useful, but they work very differently!

Core Concepts: The Memory Divide 🧠

The Stack vs The Heap πŸ“¦

.NET uses two primary memory regions:

Memory Region Purpose Characteristics Speed
Stack Local variables, method parameters Fixed size, automatic cleanup, LIFO structure ⚑ Very fast
Heap Objects, dynamic data Variable size, garbage collected, random access 🐒 Slower
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           MEMORY ARCHITECTURE               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                             β”‚
β”‚   STACK (Thread-specific)                   β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”‚
β”‚   β”‚ int x = 5   β”‚ ← Value stored directly   β”‚
β”‚   β”‚ MyClass ref β”‚ ← Address: 0x2A4F8       β”‚
β”‚   β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                          β”‚
β”‚         β”‚                                   β”‚
β”‚         β”‚  (reference points to heap)       β”‚
β”‚         ↓                                   β”‚
β”‚   HEAP (Shared across threads)              β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”‚
β”‚   β”‚ 0x2A4F8:                β”‚              β”‚
β”‚   β”‚ MyClass object data     β”‚              β”‚
β”‚   β”‚ { Name="John", Age=30 } β”‚              β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
β”‚                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Value Types: Data Stored Directly πŸ”’

Value types store their data directly in the variable. When allocated as local variables, they live on the stack. When they're fields in a class, they live inline within that object on the heap.

Common value types in .NET:

  • All numeric types: int, long, float, double, decimal
  • bool, char, byte
  • struct (custom value types)
  • enum (enumeration types)
  • DateTime, Guid, TimeSpan
  • Tuples using struct syntax: ValueTuple<T1, T2>
int age = 25;  // 25 is stored directly in 'age'
Point p = new Point(10, 20);  // If Point is a struct, data stored directly

πŸ’‘ Memory device: "Value types store the VALUE directlyβ€”no treasure map needed!"

Reference Types: Pointers to Data πŸ“

Reference types store a reference (memory address) pointing to the actual data location on the heap. The variable itself contains directions, not the destination.

Common reference types in .NET:

  • All classes: string, object, custom classes
  • Arrays: int[], string[], etc.
  • Delegates and interfaces
  • dynamic type
Person person = new Person();  // 'person' holds address, object lives on heap
string name = "Alice";  // 'name' holds reference to string on heap

πŸ’‘ Memory device: "Reference types REFer you elsewhereβ€”like a pointer on a map!"

Assignment Behavior: Copy vs Reference πŸ“‹

Value Type Assignment: Full Copy

When you assign one value type variable to another, .NET creates a complete independent copy of the data.

int x = 10;
int y = x;  // Copy the value 10 to y
y = 20;     // Changes y only

Console.WriteLine(x);  // Output: 10 (unchanged)
Console.WriteLine(y);  // Output: 20
BEFORE: y = x
β”Œβ”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”
β”‚ x   β”‚     β”‚ y   β”‚
β”‚ 10  β”‚     β”‚  ?  β”‚
β””β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”˜

AFTER: y = x
β”Œβ”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”
β”‚ x   β”‚     β”‚ y   β”‚
β”‚ 10  β”‚     β”‚ 10  β”‚  ← Full copy, independent
β””β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”˜

AFTER: y = 20
β”Œβ”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”
β”‚ x   β”‚     β”‚ y   β”‚
β”‚ 10  β”‚     β”‚ 20  β”‚  ← x unaffected
β””β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”˜

Reference Type Assignment: Shared Reference

When you assign one reference type variable to another, .NET copies the reference (address), not the object. Both variables now point to the same object.

Person p1 = new Person { Name = "Alice" };
Person p2 = p1;  // Copy the reference, not the object
p2.Name = "Bob"; // Changes the shared object

Console.WriteLine(p1.Name);  // Output: Bob (changed!)
Console.WriteLine(p2.Name);  // Output: Bob
BEFORE: p2 = p1
β”Œβ”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”
β”‚ p1  │──→ [0x2A4F8] β”‚ p2  │──→ null
β””β”€β”€β”€β”€β”€β”˜    β”‚         β””β”€β”€β”€β”€β”€β”˜
           β”‚
           ↓
      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
      β”‚ Person      β”‚
      β”‚ Name="Alice"β”‚
      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

AFTER: p2 = p1
β”Œβ”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”
β”‚ p1  │──→ [0x2A4F8] β”‚ p2  │──→ [0x2A4F8]
β””β”€β”€β”€β”€β”€β”˜    β”‚         β””β”€β”€β”€β”€β”€β”˜    β”‚
           β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  ↓
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚ Person      β”‚
            β”‚ Name="Alice"β”‚ ← Shared object
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

AFTER: p2.Name = "Bob"
β”Œβ”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”
β”‚ p1  │──→ [0x2A4F8] β”‚ p2  │──→ [0x2A4F8]
β””β”€β”€β”€β”€β”€β”˜    β”‚         β””β”€β”€β”€β”€β”€β”˜    β”‚
           β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  ↓
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚ Person      β”‚
            β”‚ Name="Bob"  β”‚ ← Changed via p2
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ” Did you know? This is why string behaves differentlyβ€”it's a reference type, but immutable. Any modification creates a new object, preventing the shared-reference problem!

Method Parameters: Passing Behavior πŸ”„

Value Types: Pass by Value (Default)

By default, value types are passed by value to methodsβ€”a copy is made.

void Increment(int number)
{
    number = number + 1;  // Modifies the copy only
    Console.WriteLine($"Inside: {number}");  // Output: Inside: 11
}

int x = 10;
Increment(x);
Console.WriteLine($"Outside: {x}");  // Output: Outside: 10 (unchanged)

Reference Types: Pass by Value of the Reference

Reference types pass a copy of the reference. You can modify the object, but not which object the original variable points to.

void ModifyPerson(Person p)
{
    p.Name = "Changed";  // Modifies the shared object βœ“
    p = new Person { Name = "New" };  // Changes local copy of reference only
}

Person person = new Person { Name = "Original" };
ModifyPerson(person);
Console.WriteLine(person.Name);  // Output: Changed (not "New")

Using ref and out Keywords πŸ”‘

To pass by reference (allowing the method to change what variable points to), use ref or out:

Keyword Must Initialize Before Call Must Assign in Method Use Case
ref βœ… Yes ❌ No Modify existing variable
out ❌ No βœ… Yes Return multiple values
void IncrementRef(ref int number)
{
    number = number + 1;  // Modifies original variable
}

int x = 10;
IncrementRef(ref x);
Console.WriteLine(x);  // Output: 11 (changed!)

// Using 'out' for multiple returns
bool TryParse(string input, out int result)
{
    // Must assign result before returning
    result = 0;
    return int.TryParse(input, out result);
}

πŸ’‘ Tip: C# 7.0+ allows inline out variable declarations: int.TryParse("123", out int result)

Example 1: Struct vs Class πŸ—οΈ

Scenario: Creating a 2D point type

// Value type (struct)
struct PointStruct
{
    public int X { get; set; }
    public int Y { get; set; }
    
    public PointStruct(int x, int y)
    {
        X = x;
        Y = y;
    }
}

// Reference type (class)
class PointClass
{
    public int X { get; set; }
    public int Y { get; set; }
    
    public PointClass(int x, int y)
    {
        X = x;
        Y = y;
    }
}

// Testing behavior
PointStruct s1 = new PointStruct(1, 2);
PointStruct s2 = s1;  // Full copy
s2.X = 10;
Console.WriteLine(s1.X);  // Output: 1 (independent)

PointClass c1 = new PointClass(1, 2);
PointClass c2 = c1;  // Reference copy
c2.X = 10;
Console.WriteLine(c1.X);  // Output: 10 (shared object)

Explanation: Structs copy all field data, creating independent instances. Classes copy only the reference, creating shared access to one object. Use structs for small, immutable data (like coordinates, colors, or dimensions). Use classes for complex entities with identity and behavior.

Example 2: Boxing and Unboxing πŸ“¦βž‘οΈπŸ”“

Boxing converts a value type to a reference type (object or interface). The value is copied to the heap and wrapped in an object.

Unboxing extracts the value type from the boxed object.

int value = 42;  // Value type on stack

// Boxing: value type β†’ reference type
object boxed = value;  // Creates heap object, copies 42 into it

Console.WriteLine(boxed.GetType());  // Output: System.Int32

// Unboxing: reference type β†’ value type
int unboxed = (int)boxed;  // Extracts value from heap object

Console.WriteLine(unboxed);  // Output: 42
BOXING PROCESS

STEP 1: Value on stack        STEP 2: Boxing occurs
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”        HEAP
β”‚ value   β”‚                   β”‚ value   β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   42    β”‚                   β”‚   42    β”‚    β”‚ 0x3B8A2  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚ boxed    β”‚
                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚ Type:int β”‚
                              β”‚ boxed   │───→│ Value:42 β”‚
                              β”‚ 0x3B8A2 β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               (reference)

Performance Impact: Boxing allocates heap memory and copies dataβ€”expensive! Unboxing requires type checking and copying. Avoid in tight loops.

// ❌ BAD: Boxing in loop (very slow)
ArrayList list = new ArrayList();
for (int i = 0; i < 10000; i++)
{
    list.Add(i);  // Boxes each int!
}

// βœ… GOOD: Use generic collection (no boxing)
List<int> list = new List<int>();
for (int i = 0; i < 10000; i++)
{
    list.Add(i);  // No boxing, stays as value type
}

Explanation: Boxing creates garbage on the heap and triggers garbage collection. Generics (List<T>) preserve the value type nature, avoiding boxing entirely. This can be 10-100x faster for numeric operations!

Example 3: Null Reference Exceptions 🚫

The problem: Reference types can be null (pointing nowhere), but value types cannot (without Nullable<T>).

// Value type - cannot be null
int number = null;  // ❌ Compiler error!

// Nullable value type - can be null
int? nullableNumber = null;  // βœ… Works (syntactic sugar for Nullable<int>)

if (nullableNumber.HasValue)
{
    Console.WriteLine(nullableNumber.Value);
}
else
{
    Console.WriteLine("No value");
}

// Reference type - can be null
string text = null;  // βœ… Allowed
Console.WriteLine(text.Length);  // πŸ’₯ NullReferenceException!

// Safe check
if (text != null)
{
    Console.WriteLine(text.Length);
}

// C# 8.0+ nullable reference types (opt-in)
string? nullableText = null;  // Explicitly nullable
string nonNullText = "Hello";  // Should not be null (compiler warns)

Explanation: Reference types default to null, requiring defensive checks. Nullable value types (T?) wrap the value in a struct with a boolean flag. C# 8.0's nullable reference types add compile-time safety without runtime overheadβ€”the compiler warns when you might dereference null.

Example 4: Performance Implications 🏎️

Scenario: Processing 1 million coordinates

// Option 1: Class (reference type)
class CoordinateClass
{
    public double X { get; set; }
    public double Y { get; set; }
}

// Option 2: Struct (value type)
struct CoordinateStruct
{
    public double X { get; set; }
    public double Y { get; set; }
}

// Creating 1 million coordinates
var stopwatch = Stopwatch.StartNew();

// Using class: 1 million heap allocations!
CoordinateClass[] classes = new CoordinateClass[1000000];
for (int i = 0; i < classes.Length; i++)
{
    classes[i] = new CoordinateClass { X = i, Y = i * 2 };
}
stopwatch.Stop();
Console.WriteLine($"Class: {stopwatch.ElapsedMilliseconds}ms");
// Typical: 50-100ms + GC pressure

stopwatch.Restart();

// Using struct: stored inline in array (no individual allocations)
CoordinateStruct[] structs = new CoordinateStruct[1000000];
for (int i = 0; i < structs.Length; i++)
{
    structs[i] = new CoordinateStruct { X = i, Y = i * 2 };
}
stopwatch.Stop();
Console.WriteLine($"Struct: {stopwatch.ElapsedMilliseconds}ms");
// Typical: 10-20ms, no GC pressure

Memory layout comparison:

ARRAY OF CLASSES (reference types)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Array on heap                       β”‚
β”‚ [0]: ref β†’ heap object 1            β”‚
β”‚ [1]: ref β†’ heap object 2            β”‚  Each element = 8 bytes (reference)
β”‚ [2]: ref β†’ heap object 3            β”‚  Plus 1M separate heap objects
β”‚ ...                                 β”‚  = Poor cache locality
β”‚ [999999]: ref β†’ heap object 1000000 β”‚  = GC must track 1M objects
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

ARRAY OF STRUCTS (value types)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Array on heap                       β”‚
β”‚ [0]: {X=0, Y=0}     ← data inline   β”‚
β”‚ [1]: {X=1, Y=2}     ← data inline   β”‚  Each element = 16 bytes (2 doubles)
β”‚ [2]: {X=2, Y=4}     ← data inline   β”‚  = Excellent cache locality
β”‚ ...                                 β”‚  = GC tracks only 1 array
β”‚ [999999]: {X=999999, Y=1999998}     β”‚  = Much faster iteration
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Explanation: Value types stored in arrays or collections have excellent cache localityβ€”data is contiguous in memory. Reference types scatter objects across the heap, causing cache misses. For large collections of small data, structs can be 5-10x faster.

⚠️ Caveat: Large structs (>16 bytes) passed to methods create expensive copies. Use in keyword for read-only references:

struct LargeStruct
{
    public double A, B, C, D, E, F, G, H;  // 64 bytes
}

// ❌ BAD: Copies 64 bytes on each call
void ProcessSlow(LargeStruct data)
{
    // Use data...
}

// βœ… GOOD: Passes by reference (no copy)
void ProcessFast(in LargeStruct data)
{
    // Use data (read-only reference)...
}

Common Mistakes to Avoid ⚠️

Mistake 1: Expecting Value Type Behavior from Classes

// ❌ WRONG: Assuming Person copies like an int
Person p1 = new Person { Name = "Alice" };
Person p2 = p1;
p2.Name = "Bob";
// Surprise! p1.Name is also "Bob" now

// βœ… RIGHT: Explicitly clone if you need a copy
Person p2 = new Person { Name = p1.Name };
// Or implement ICloneable

Mistake 2: Making Large Structs

// ❌ WRONG: Struct with many fields (expensive to copy)
struct HugeStruct
{
    public long Field1, Field2, Field3, Field4, Field5,
                Field6, Field7, Field8, Field9, Field10;
    // 80 bytes - copied on every assignment and method call!
}

// βœ… RIGHT: Use class for large data structures
class LargeData
{
    public long Field1, Field2, Field3, Field4, Field5,
                Field6, Field7, Field8, Field9, Field10;
    // Only 8-byte reference copied
}

Rule of thumb: Keep structs under 16 bytes, immutable, and logically representing a single value.

Mistake 3: Modifying Structs in Collections

struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

var points = new List<Point>();
points.Add(new Point { X = 1, Y = 2 });

// ❌ WRONG: This doesn't work as expected!
points[0].X = 10;  // Compiler error (CS1612)
// You're modifying a COPY returned by the indexer

// βœ… RIGHT: Replace entire struct
Point p = points[0];
p.X = 10;
points[0] = p;

// Or use a class instead

Mistake 4: Forgetting About Boxing

// ❌ WRONG: Non-generic collections box value types
Hashtable table = new Hashtable();
table.Add("age", 25);  // Boxing!
int age = (int)table["age"];  // Unboxing!

// βœ… RIGHT: Use generic collections
Dictionary<string, int> table = new Dictionary<string, int>();
table.Add("age", 25);  // No boxing
int age = table["age"];  // No unboxing

Mistake 5: Not Understanding ref vs Value

void BadIncrement(int x)
{
    x++;  // Modifies copy, original unchanged
}

void GoodIncrement(ref int x)
{
    x++;  // Modifies original
}

int number = 5;
BadIncrement(number);
Console.WriteLine(number);  // Still 5

GoodIncrement(ref number);
Console.WriteLine(number);  // Now 6

Mistake 6: String Concatenation in Loops

// ❌ WRONG: Creates new string object on each iteration
string result = "";
for (int i = 0; i < 1000; i++)
{
    result += i.ToString();  // 1000 string objects created!
}

// βœ… RIGHT: Use StringBuilder (mutable reference type)
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
    sb.Append(i);
}
string result = sb.ToString();

Key Takeaways 🎯

  1. Value types store data directly; reference types store addresses pointing to data on the heap.

  2. Assignment behavior:

    • Value types: full copy (independent)
    • Reference types: reference copy (shared object)
  3. Memory locations:

    • Value types: stack (local variables) or inline (fields)
    • Reference types: heap (garbage collected)
  4. Method parameters:

    • Default: pass by value (value types copy data, reference types copy reference)
    • ref: pass by reference (can modify original)
    • out: must assign before returning
  5. Performance considerations:

    • Value types: no heap allocation, better cache locality, but copy overhead
    • Reference types: heap allocation, GC overhead, but cheap to pass around
  6. Guidelines:

    • Use structs for small (<16 bytes), immutable, single-value data
    • Use classes for complex objects with identity and behavior
    • Avoid boxing with generic collections
    • Use in keyword for large struct parameters
  7. Nullability:

    • Value types: not nullable (unless Nullable<T> or T?)
    • Reference types: nullable by default (C# 8.0+ adds nullable reference types)

πŸ“‹ Quick Reference Card

Characteristic Value Types Reference Types
Storage Stack (or inline) Heap
Assignment Copy data Copy reference
Default value 0, false, etc. null
Nullable Use T? By default
Examples int, struct, enum class, string, array
Memory overhead None Object header + reference
GC tracked No (unless boxed) Yes
Best for Small, immutable data Complex objects

🧠 Memory Device

"SCAR" for value types:

  • Stack storage (typically)
  • Copied fully on assignment
  • Always has a value (not nullable)
  • Rapid access (no indirection)

"SHARPEN" for reference types:

  • Shared when assigned
  • Heap allocated
  • Address stored in variable
  • References can be null
  • Pointer indirection
  • Entities with identity
  • Needs garbage collection

πŸ”§ Try This: Identify the Types

For each declaration, determine if it's a value or reference type:

1. int[] numbers = new int[10];
2. DateTime now = DateTime.Now;
3. string message = "Hello";
4. List<int> items = new List<int>();
5. Point p = new Point(1, 2);  // Assume Point is a struct
6. bool isValid = true;
7. object obj = 42;
Click for answers
  1. Reference (array is always a reference type, even array of value types)
  2. Value (DateTime is a struct)
  3. Reference (string is a class)
  4. Reference (List is a class)
  5. Value (specified as struct)
  6. Value (bool is a primitive value type)
  7. Reference (object is the base class for all types)

Note: In #7, the 42 gets boxed (converted from value type int to reference type object).

πŸ“š Further Study

  1. Microsoft Docs - Choosing Between Class and Struct: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/choosing-between-class-and-struct

  2. Eric Lippert's Blog - The Stack is an Implementation Detail: https://ericlippert.com/2009/04/27/the-stack-is-an-implementation-detail/

  3. Microsoft Docs - Memory Management and Garbage Collection: https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/

You now have a solid foundation in value vs reference types! Next, you'll explore how the garbage collector manages reference types on the heap, diving deeper into generational collection, finalization, and memory optimization techniques. πŸš€