MODULE 4: .NET RUNTIME DEEP DIVE - Complete Guide

43. Value Types vs Reference Types (Memory Layout)

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

public class ValueVsReferenceTypes
{
// VALUE TYPES - Stored where declared (stack or inline in objects)
// Examples: int, bool, char, double, struct, enum
// REFERENCE TYPES - Always stored on heap, variable has pointer
// Examples: class, array, string, interface, delegate
public void MemoryLayoutDemo()
{
// VALUE TYPE - On stack
int value1 = 10;
int value2 = value1; // COMPLETE COPY - Separate memory
value2 = 20;
Console.WriteLine(value1); // Still 10 (unchanged)
// REFERENCE TYPE - On heap, variables point to same object
Person reference1 = new Person { Name = "John" };
Person reference2 = reference1; // COPY REFERENCE (pointer), not object
reference2.Name = "Jane";
Console.WriteLine(reference1.Name); // "Jane" (changed!)
// STRING - Special reference type (immutable)
string str1 = "Hello";
string str2 = str1;
str2 = "World";
Console.WriteLine(str1); // "Hello" (strings are immutable, new string created)
}
public void StackVsHeapVisualization()
{
// STACK (Fast, limited, LIFO)
int age = 30; // On stack
bool isActive = true; // On stack
double price = 99.99; // On stack
// HEAP (Slower, abundant, managed by GC)
Person person = new Person(); // 'person' reference on stack, object on heap
int[] numbers = new int[1000]; // 'numbers' reference on stack, array on heap
List<int> list = new List<int>(); // Reference on stack, object on heap
// What happens with struct containing reference?
Container container = new Container();
container.Name = "Test"; // 'Name' string on heap, container on stack (or inline)
}
// STRUCT (Value Type) - Pass by copy
public struct PointStruct
{
public int X, Y;
public PointStruct(int x, int y) { X = x; Y = y; }
}
// CLASS (Reference Type) - Pass by reference
public class PointClass
{
public int X, Y;
public PointClass(int x, int y) { X = x; Y = y; }
}
public void PassByCopyVsReference()
{
// Struct - PASS BY COPY (Original unchanged)
PointStruct ps1 = new PointStruct(10, 20);
ModifyStruct(ps1);
Console.WriteLine($"Struct: {ps1.X}, {ps1.Y}"); // Still 10, 20
// Class - PASS BY REFERENCE (Original changed)
PointClass pc1 = new PointClass(10, 20);
ModifyClass(pc1);
Console.WriteLine($"Class: {pc1.X}, {pc1.Y}"); // Now 100, 200
}
private void ModifyStruct(PointStruct p)
{
p.X = 100;
p.Y = 200; // Modifies COPY only
}
private void ModifyClass(PointClass p)
{
p.X = 100;
p.Y = 200; // Modifies ORIGINAL object
}
}

// Memory layout demonstration
[StructLayout(LayoutKind.Sequential)]
public struct StructLayout
{
public int IntValue; // 4 bytes
public short ShortValue; // 2 bytes
public byte ByteValue; // 1 byte
// Total: 7 bytes (but padded to 8 for alignment)
}

public class ClassLayout
{
public int IntValue; // 4 bytes + object header (8 bytes on 64-bit)
public short ShortValue; // 2 bytes + method table pointer (8 bytes)
public byte ByteValue; // 1 byte
// Total object size: ~24-32 bytes overhead + fields
}

44. Boxing and Unboxing (Performance Critical)

public class BoxingUnboxingDemo
{
// BOXING: Converting value type to reference type (object)
// UNBOXING: Converting reference type back to value type
public void BoxingExample()
{
// Boxing occurs
int value = 123;
object boxed = value; // BOXING - int is wrapped in object on heap
// Memory impact:
// - int: 4 bytes on stack
// - boxed int: object header (8-16 bytes) + int (4 bytes) = 12-20 bytes on heap
// Unboxing
int unboxed = (int)boxed; // UNBOXING - must cast to exact type
}
public void CommonBoxingScenarios()
{
// 1. ArrayList (non-generic) - ALWAYS boxes
ArrayList list = new ArrayList();
list.Add(10); // Boxing int to object
list.Add(20); // Boxing again
int value = (int)list[0]; // Unboxing
// 2. Generic List - NO boxing
List<int> genericList = new List<int>();
genericList.Add(10); // No boxing
genericList.Add(20); // No boxing
int value2 = genericList[0]; // No unboxing
// 3. Passing value type to method expecting object
Console.WriteLine(42); // Boxing occurs here (int to object)
// 4. String concatenation with value types
string result = "Value: " + 42; // Boxing, then ToString()
// 5. Enum to object
DayOfWeek day = DayOfWeek.Monday;
object boxedDay = day; // Boxing
}
public void PerformanceImpact()
{
const int iterations = 10_000_000;
// SLOW: With boxing
var start1 = DateTime.Now;
ArrayList boxedList = new ArrayList();
for (int i = 0; i < iterations; i++)
{
boxedList.Add(i); // Boxing each iteration
}
long sum1 = 0;
for (int i = 0; i < iterations; i++)
{
sum1 += (int)boxedList[i]; // Unboxing each iteration
}
var time1 = DateTime.Now - start1;
// FAST: No boxing
var start2 = DateTime.Now;
List<int> genericList = new List<int>();
for (int i = 0; i < iterations; i++)
{
genericList.Add(i); // No boxing
}
long sum2 = 0;
for (int i = 0; i < iterations; i++)
{
sum2 += genericList[i]; // No unboxing
}
var time2 = DateTime.Now - start2;
Console.WriteLine($"Boxing time: {time1.TotalMilliseconds}ms");
Console.WriteLine($"No boxing time: {time2.TotalMilliseconds}ms");
// No boxing is typically 5-10x faster!
}
// How to avoid boxing
public void AvoidBoxingTechniques()
{
// 1. Use generics
List<int> numbers = new List<int>(); // Good
// 2. Use overloaded methods
void Process(int value) { } // Specific overload
void Process(object value) { } // Generic overload
// 3. Use nullable types carefully
int? nullable = 42;
object boxed = nullable; // Still boxes the nullable wrapper
// 4. Use IComparable<T> instead of IComparable
public class MyType : IComparable<MyType> // Good
{
public int CompareTo(MyType other) => 0;
}
// Avoid IComparable (boxes)
public class MyTypeOld : IComparable // Bad
{
public int CompareTo(object obj) => 0;
}
}
// Detect boxing in your code
public void DetectBoxing()
{
// Use a decompiler or IL viewer
// Boxing IL instruction: 'box'
// Unboxing IL instruction: 'unbox' or 'unbox.any'
// Example that boxes:
object obj = 42; // IL: box
// Example that doesn't box:
List<int> list = new List<int>();
list.Add(42); // IL: no box
}
}

// Real-world example - Generic constraints prevent boxing
public class GenericCalculator<T> where T : struct // T is value type
{
public T Add(T a, T b)
{
// No boxing because T is constrained to struct
dynamic da = a;
dynamic db = b;
return da + db;
}
}

45. Stack vs Heap Allocation

using System;
using System.Buffers;
using System.Runtime.InteropServices;

public class StackVsHeapDemo
{
// STACK - Fast, automatic, limited (1MB default)
// - Value types (local variables)
// - Method parameters
// - Return addresses
// HEAP - Slower, managed, large (up to available memory)
// - Objects
// - Arrays
// - Strings
// - Value types inside objects
public void StackAllocation()
{
// Allocated on STACK
int age = 30; // 4 bytes
bool isActive = true; // 1 byte
double price = 99.99; // 8 bytes
Point point = new Point(10, 20); // Value type - on stack
// Point struct definition
struct Point
{
public int X, Y;
public Point(int x, int y) { X = x; Y = y; }
}
}
public void HeapAllocation()
{
// Allocated on HEAP
Person person = new Person(); // Object on heap
int[] numbers = new int[1000]; // Array on heap
string text = "Hello"; // String on heap
List<int> list = new List<int>(); // List object on heap
// What about value types inside objects?
Person p = new Person();
p.Age = 30; // Age (value type) is stored ON HEAP (inside Person object)
}
// STACK OVERFLOW (Bad recursion)
public void StackOverflowExample()
{
// This will cause StackOverflowException
// void Recursive(int depth)
// {
// int[] localArray = new int[1000]; // Each call allocates on stack
// Recursive(depth + 1);
// }
// Recursive(1);
}
// STACKALLOC - Allocate memory on stack (fast, no GC)
public unsafe void StackAllocExample()
{
// Allocate array on stack (doesn't require GC)
Span<int> numbers = stackalloc int[100]; // 400 bytes on stack
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] = i * i;
}
// Fast! No heap allocation, no GC
// But limited to stack size (1MB default)
// With initializer
Span<int> initialized = stackalloc int[] { 1, 2, 3, 4, 5 };
// Using expression (C# 7.2+)
Span<int> values = stackalloc[] { 10, 20, 30, 40, 50 };
}
// REF STRUCT - Can only live on stack
public ref struct StackOnlyStruct
{
public Span<int> Data { get; set; }
// Cannot be on heap, can't be boxed, can't be in array
// Perfect for high-performance scenarios
}
// SPAN<T> - Stack-only type for safe memory access
public void SpanUsage()
{
// Stack allocated span
Span<int> stackSpan = stackalloc int[10];
// Heap allocated span (points to array)
int[] array = new int[10];
Span<int> heapSpan = array.AsSpan();
// String as span (no allocation)
string text = "Hello World";
ReadOnlySpan<char> charSpan = text.AsSpan();
// Slice without allocation
ReadOnlySpan<char> slice = charSpan.Slice(0, 5); // "Hello"
// Fast, safe, allocation-free operations
foreach (char c in charSpan)
{
Console.WriteLine(c);
}
}
// MEMORY<T> - Heap-based version of Span (can be stored in async/classes)
public void MemoryUsage()
{
Memory<int> memory = new int[10]; // Wraps array
Span<int> span = memory.Span; // Get span from memory
// Memory can be used in async methods
async Task ProcessAsync(Memory<int> data)
{
await Task.Delay(100);
Span<int> span = data.Span;
// Process data
}
}
// ARRAY POOL - Reuse heap allocations
public void ArrayPoolUsage()
{
// Instead of allocating new arrays, rent from pool
int[] array = ArrayPool<int>.Shared.Rent(1000);
try
{
// Use array
for (int i = 0; i < 1000; i++)
array[i] = i;
}
finally
{
// Return to pool for reuse
ArrayPool<int>.Shared.Return(array);
}
}
// ALLOCATION COMPARISON
public void AllocationPerformance()
{
const int iterations = 1_000_000;
// SLOW: Heap allocation each iteration
var start1 = DateTime.Now;
for (int i = 0; i < iterations; i++)
{
int[] heapArray = new int[100]; // Heap allocation
}
var time1 = DateTime.Now - start1;
// FAST: Stack allocation (no GC pressure)
var start2 = DateTime.Now;
for (int i = 0; i < iterations; i++)
{
Span<int> stackArray = stackalloc int[100]; // Stack allocation
}
var time2 = DateTime.Now - start2;
Console.WriteLine($"Heap allocation: {time1.TotalMilliseconds}ms");
Console.WriteLine($"Stack allocation: {time2.TotalMilliseconds}ms");
// Stack is typically 10-50x faster!
}
}

// Real-world performance patterns
public class HighPerformancePatterns
{
// GOOD: Use Span for parsing
public int ParseFirstNumber(ReadOnlySpan<char> text)
{
int index = text.IndexOf(' ');
if (index == -1) return 0;
var numberSpan = text.Slice(0, index);
return int.Parse(numberSpan);
}
// GOOD: Use stackalloc for small temporary arrays
public void ProcessSmallData()
{
Span<int> temp = stackalloc int[32]; // Allocate on stack
// Process data...
}
// BAD: Unnecessary heap allocations
public string BadParseFirstNumber(string text)
{
string[] parts = text.Split(' '); // Heap allocation
return parts[0]; // More allocations
}
}

46. Garbage Collection (Generations, Finalization)

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

public class GarbageCollectionDemo
{
// GC GENERATIONS
// Gen 0: New objects (collected frequently, fast)
// Gen 1: Survivors of Gen 0 (collected less often)
// Gen 2: Long-lived objects (collected rarely, expensive)
// Large Object Heap (LOH): Objects > 85KB (collected with Gen 2)
public void GCGenerationsDemo()
{
Console.WriteLine($"GC Generation for int: {GC.GetGeneration(42)}"); // Gen 0
// Create objects
for (int i = 0; i < 10; i++)
{
var obj = new object();
Console.WriteLine($"Object {i} generation: {GC.GetGeneration(obj)}");
}
// Force GC (not recommended in production)
GC.Collect(); // Collect Gen 0
GC.Collect(1); // Collect Gen 1
GC.Collect(2); // Collect Gen 2
GC.WaitForPendingFinalizers(); // Wait for finalizers
}
public void GCPhases()
{
// 1. MARK PHASE - Find all reachable objects
// GC starts from roots (stack variables, static fields, etc.)
// 2. COMPACT PHASE - Defragment memory
// Move objects to eliminate gaps
// 3. SWEEP PHASE - Free memory of unreachable objects
// For LOH: Only sweep (no compact) for performance
}
// FINALIZER (Destructor) - Called by GC
public class ResourceHolder : IDisposable
{
private IntPtr nativeResource;
private bool disposed = false;
// Finalizer (destructor) - Called by GC when object is collected
~ResourceHolder()
{
Console.WriteLine("Finalizer called");
Dispose(false);
}
// Dispose pattern - Called by developer
public void Dispose()
{
Console.WriteLine("Dispose called");
Dispose(true);
GC.SuppressFinalize(this); // Don't call finalizer
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Free managed resources
// (called from Dispose)
}
// Free unmanaged resources
if (nativeResource != IntPtr.Zero)
{
Marshal.FreeHGlobal(nativeResource);
nativeResource = IntPtr.Zero;
}
disposed = true;
}
}
}
// GC WEAK REFERENCES - Reference that doesn't prevent collection
public void WeakReferenceDemo()
{
var obj = new object();
WeakReference weakRef = new WeakReference(obj);
Console.WriteLine($"Is alive: {weakRef.IsAlive}"); // True
obj = null; // Remove strong reference
GC.Collect(); // Force GC
Console.WriteLine($"Is alive: {weakRef.IsAlive}"); // False
// Use case: Caching with automatic cleanup
object target = weakRef.Target;
if (target != null)
{
// Still alive, use it
}
else
{
// Object was collected, recreate it
}
}
// GC EVENTS - Monitor GC behavior
public void MonitorGC()
{
GC.RegisterForFullGCNotification(10, 10);
Task.Run(() =>
{
while (true)
{
if (GC.WaitForFullGCApproach() == GCNotificationStatus.Succeeded)
{
Console.WriteLine("Full GC approaching!");
}
if (GC.WaitForFullGCComplete() == GCNotificationStatus.Succeeded)
{
Console.WriteLine("Full GC completed!");
}
Thread.Sleep(1000);
}
});
}
// LARGE OBJECT HEAP
public void LargeObjectHeap()
{
// Objects > 85KB go to LOH
byte[] largeArray = new byte[85000]; // Goes to LOH
// LOH is not compacted (only swept) to avoid moving large objects
// Can cause memory fragmentation
// Monitor LOH
Console.WriteLine($"LOH size: {GC.GetTotalMemory(false) / 1024} KB");
}
// GC LATENCY MODES
public void GCLatencyModes()
{
// Batch - Most aggressive, high throughput, high latency
GC.TryStartNoGCRegion(1024 * 1024 * 10); // 10MB no-GC region
try
{
// Critical code that can't have GC interruption
ProcessCriticalData();
}
finally
{
GC.EndNoGCRegion();
}
// LowLatency - Less frequent GC
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
// SustainedLowLatency - Best for UI responsiveness
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
// Interactive - Default (balanced)
GCSettings.LatencyMode = GCLatencyMode.Interactive;
}
private void ProcessCriticalData() { }
// GC STATISTICS
public void GCStats()
{
// How many GCs occurred
int gen0Collections = GC.CollectionCount(0);
int gen1Collections = GC.CollectionCount(1);
int gen2Collections = GC.CollectionCount(2);
Console.WriteLine($"Gen0: {gen0Collections}, Gen1: {gen1Collections}, Gen2: {gen2Collections}");
// Total memory
long totalMemory = GC.GetTotalMemory(false);
Console.WriteLine($"Total memory: {totalMemory / 1024 / 1024} MB");
// Max generation support
int maxGen = GC.MaxGeneration;
Console.WriteLine($"Max generation: {maxGen}");
}
}

// Real-world memory management patterns
public class MemoryOptimizationPatterns
{
// 1. Object Pooling - Reuse objects instead of creating new ones
public class ObjectPool<T> where T : new()
{
private readonly Stack<T> _pool = new Stack<T>();
private readonly int _maxSize;
public ObjectPool(int maxSize = 100)
{
_maxSize = maxSize;
}
public T Get()
{
lock (_pool)
{
return _pool.Count > 0 ? _pool.Pop() : new T();
}
}
public void Return(T obj)
{
lock (_pool)
{
if (_pool.Count < _maxSize)
{
_pool.Push(obj);
}
}
}
}
// 2. Use Structs for Small Data (avoid heap allocation)
public struct SmallData // 16 bytes or less
{
public int Id;
public byte Status;
public short Value;
// Use struct for performance-sensitive code
}
// 3. Avoid Large Object Heap Fragmentation
public void AvoidLOHFragmentation()
{
// BAD: Creates many large objects
for (int i = 0; i < 1000; i++)
{
byte[] temp = new byte[90000]; // Each on LOH
Process(temp);
}
// GOOD: Reuse large array
byte[] reusable = new byte[90000];
for (int i = 0; i < 1000; i++)
{
Array.Clear(reusable, 0, reusable.Length);
Process(reusable);
}
}
private void Process(byte[] data) { }
// 4. Avoid Premature GC Collection
public void AvoidPrematureGC()
{
// BAD: Forcing GC
// GC.Collect(); // Don't do this!
// GOOD: Let GC manage itself
// The GC is self-tuning and handles most scenarios well
}
}

// Performance comparison class
public class GCPerformanceTest
{
public void RunTests()
{
// Test 1: Many small allocations
var sw = Stopwatch.StartNew();
for (int i = 0; i < 1_000_000; i++)
{
var obj = new object();
}
sw.Stop();
Console.WriteLine($"1M small objects: {sw.ElapsedMilliseconds}ms");
// Test 2: Object pooling
var pool = new MemoryOptimizationPatterns.ObjectPool<object>();
sw = Stopwatch.StartNew();
for (int i = 0; i < 1_000_000; i++)
{
var obj = pool.Get();
pool.Return(obj);
}
sw.Stop();
Console.WriteLine($"1M with pooling: {sw.ElapsedMilliseconds}ms");
}
}

47. IDisposable and Using Statements

using System;
using System.IO;
using System.Runtime.InteropServices;

public class IDisposableDemo
{
// PROPER DISPOSABLE PATTERN
public class DatabaseConnection : IDisposable
{
private IntPtr _nativeConnection;
private StreamReader _reader;
private bool _disposed = false;
public DatabaseConnection(string connectionString)
{
Console.WriteLine("Opening connection");
_nativeConnection = Marshal.AllocHGlobal(100);
_reader = File.OpenText("config.txt");
}
public void Query(string sql)
{
if (_disposed) throw new ObjectDisposedException(nameof(DatabaseConnection));
Console.WriteLine($"Executing: {sql}");
}
// Public Dispose method
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // No need to finalize
}
// Protected dispose pattern
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
Console.WriteLine("Disposing managed resources");
_reader?.Dispose();
}
// Dispose unmanaged resources
Console.WriteLine("Freeing unmanaged resources");
if (_nativeConnection != IntPtr.Zero)
{
Marshal.FreeHGlobal(_nativeConnection);
_nativeConnection = IntPtr.Zero;
}
_disposed = true;
}
}
// Finalizer (only if you have unmanaged resources)
~DatabaseConnection()
{
Dispose(false);
Console.WriteLine("Finalizer called (should not happen if Dispose called)");
}
}
// USING STATEMENT (Old way)
public void UsingStatementOld()
{
using (var conn = new DatabaseConnection("Server=localhost"))
{
conn.Query("SELECT * FROM Users");
} // Dispose automatically called here
}
// USING DECLARATION (C# 8+)
public void UsingDeclaration()
{
using var conn = new DatabaseConnection("Server=localhost");
conn.Query("SELECT * FROM Users");
// Dispose called at end of method
}
// NESTED USING
public void NestedUsing()
{
// Old way
using (var conn = new DatabaseConnection("db1"))
using (var conn2 = new DatabaseConnection("db2"))
{
conn.Query("SELECT * FROM T1");
conn2.Query("SELECT * FROM T2");
}
// New way
using var conn3 = new DatabaseConnection("db1");
using var conn4 = new DatabaseConnection("db2");
conn3.Query("SELECT * FROM T1");
conn4.Query("SELECT * FROM T2");
}
// ASYNC DISPOSE (IAsyncDisposable - C# 8)
public class AsyncDatabaseConnection : IAsyncDisposable
{
private StreamReader _reader;
public async ValueTask DisposeAsync()
{
if (_reader != null)
{
await _reader.DisposeAsync();
}
Console.WriteLine("Async disposed");
}
}
public async Task AsyncUsingExample()
{
await using var conn = new AsyncDatabaseConnection();
// Use connection
} // DisposeAsync called automatically
// COMMON PATTERNS
// Pattern 1: Ensure Dispose is called
public void BadPattern() // DON'T DO THIS
{
var conn = new DatabaseConnection("db");
conn.Query("SELECT...");
// If exception occurs, Dispose never called!
}
public void GoodPattern() // DO THIS
{
using var conn = new DatabaseConnection("db");
conn.Query("SELECT...");
// Dispose called even if exception occurs
}
// Pattern 2: IDisposable in fields
public class ServiceWithResources : IDisposable
{
private DatabaseConnection _connection;
private FileStream _fileStream;
private bool _disposed;
public ServiceWithResources()
{
_connection = new DatabaseConnection("db");
_fileStream = File.OpenRead("data.txt");
}
public void Dispose()
{
if (!_disposed)
{
_connection?.Dispose();
_fileStream?.Dispose();
_disposed = true;
}
}
}
// Pattern 3: Factory with dispose
public class ResourceFactory : IDisposable
{
private List<IDisposable> _resources = new List<IDisposable>();
public T CreateResource<T>() where T : IDisposable, new()
{
var resource = new T();
_resources.Add(resource);
return resource;
}
public void Dispose()
{
foreach (var resource in _resources)
{
resource.Dispose();
}
}
}
// Pattern 4: Conditional dispose
public class OptionalDisposable : IDisposable
{
private readonly bool _shouldDispose;
private readonly IDisposable _resource;
public OptionalDisposable(IDisposable resource, bool shouldDispose)
{
_resource = resource;
_shouldDispose = shouldDispose;
}
public void Dispose()
{
if (_shouldDispose)
{
_resource?.Dispose();
}
}
}
}

// REAL-WORLD EXAMPLE - Repository with proper disposal
public class CustomerRepository : IDisposable
{
private DatabaseConnection _connection;
private bool _disposed;
public CustomerRepository(string connectionString)
{
_connection = new DatabaseConnection(connectionString);
}
public async Task<List<Customer>> GetCustomersAsync()
{
if (_disposed) throw new ObjectDisposedException(nameof(CustomerRepository));
// Use connection
_connection.Query("SELECT * FROM Customers");
return new List<Customer>();
}
public void Dispose()
{
if (!_disposed)
{
_connection?.Dispose();
_disposed = true;
}
}
}

public class Customer { }

// Usage
public class RepositoryUsage
{
public async Task ProcessCustomers()
{
// Proper disposal even on exception
using var repo = new CustomerRepository("Server=localhost");
var customers = await repo.GetCustomersAsync();
// Process customers
}
}

48. Finalizers/Destructors

using System;
using System.Runtime.InteropServices;

public class FinalizersDemo
{
// BASIC FINALIZER
public class NativeResource : IDisposable
{
private IntPtr _handle;
public NativeResource()
{
_handle = Marshal.AllocHGlobal(1024);
Console.WriteLine($"Allocated native memory at {_handle}");
}
// Finalizer (destructor)
~NativeResource()
{
Console.WriteLine($"Finalizer called for {_handle}");
ReleaseResources();
}
public void Dispose()
{
Console.WriteLine($"Dispose called for {_handle}");
ReleaseResources();
GC.SuppressFinalize(this); // Prevent finalizer from running
}
private void ReleaseResources()
{
if (_handle != IntPtr.Zero)
{
Marshal.FreeHGlobal(_handle);
Console.WriteLine($"Freed memory at {_handle}");
_handle = IntPtr.Zero;
}
}
}
// WHEN FINALIZERS RUN
public void FinalizerTiming()
{
Console.WriteLine("Creating resource...");
var resource = new NativeResource();
// Without Dispose, finalizer will run eventually
resource = null; // Make eligible for GC
Console.WriteLine("Forcing GC...");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("GC completed");
// Output order:
// Creating resource...
// Allocated native memory at ...
// Forcing GC...
// Finalizer called for ...
// Freed memory at ...
// GC completed
}
// FINALIZER DRAWBACKS
public void FinalizerProblems()
{
// 1. Slows down GC (objects with finalizers survive collection)
// 2. Not guaranteed to run (app could crash)
// 3. Runs on dedicated thread (can cause deadlocks)
// 4. Can be called multiple times if not suppressed
// ALWAYS implement IDisposable with finalizer for unmanaged resources
}
// SAFE HANDLE PATTERN (Better than finalizers)
public class SafeResource : SafeHandle
{
public SafeResource() : base(IntPtr.Zero, true)
{
}
public override bool IsInvalid => handle == IntPtr.Zero;
protected override bool ReleaseHandle()
{
if (!IsInvalid)
{
Marshal.FreeHGlobal(handle);
SetHandle(IntPtr.Zero);
}
return true;
}
}
// DESTRUCTOR (C# syntax for finalizer)
public class DestructorExample
{
// This is a finalizer
~DestructorExample()
{
// Cleanup code
}
}
// CRITICAL FINALIZER (For critical resources)
public class CriticalResource : CriticalFinalizerObject
{
~CriticalResource()
{
// Will run even on AppDomain unload
// Less likely to be skipped
}
}
// TESTING FINALIZER BEHAVIOR
public void TestFinalizerBehavior()
{
// Object with finalizer
var obj = new FinalizerObject();
// Even after null, still in finalization queue
obj = null;
// First GC puts in finalization queue
GC.Collect();
GC.WaitForPendingFinalizers(); // Wait for finalizer to run
// Object is now dead (if no resurrection)
Console.WriteLine("Done");
}
public class FinalizerObject
{
~FinalizerObject()
{
Console.WriteLine("Finalizer running");
}
}
// OBJECT RESURRECTION (Anti-pattern, but possible)
public class ResurrectionDemo
{
private static ResurrectionDemo _savedInstance;
~ResurrectionDemo()
{
// BAD PRACTICE - Object becomes alive again!
_savedInstance = this;
Console.WriteLine("Object resurrected!");
}
public static void Test()
{
var obj = new ResurrectionDemo();
obj = null;
GC.Collect();
GC.WaitForPendingFinalizers();
if (_savedInstance != null)
{
Console.WriteLine("Object came back to life!");
_savedInstance = null; // Clean up again
}
}
}
}

// BEST PRACTICE: Finalizer should only release unmanaged resources
public class BestPracticeDisposable : IDisposable
{
private IntPtr _unmanagedResource;
private IDisposable _managedResource;
private bool _disposed;
public BestPracticeDisposable()
{
_unmanagedResource = Marshal.AllocHGlobal(100);
_managedResource = new MemoryStream();
}
// Finalizer - only for unmanaged resources
~BestPracticeDisposable()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources (only in Dispose, not finalizer)
_managedResource?.Dispose();
}
// Free unmanaged resources (in both)
if (_unmanagedResource != IntPtr.Zero)
{
Marshal.FreeHGlobal(_unmanagedResource);
_unmanagedResource = IntPtr.Zero;
}
_disposed = true;
}
}
}

49. Weak References

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

public class WeakReferencesDemo
{
// WEAK REFERENCE - Allows object to be collected
public void BasicWeakReference()
{
var obj = new LargeObject();
WeakReference weakRef = new WeakReference(obj);
Console.WriteLine($"Is alive: {weakRef.IsAlive}"); // True
obj = null; // Remove strong reference
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine($"Is alive: {weakRef.IsAlive}"); // False
// Try to get object (if still alive)
if (weakRef.Target is LargeObject target)
{
target.Process();
}
}
// WEAK REFERENCE TYPES
public void WeakReferenceTypes()
{
// Short weak reference - Collected immediately on GC
var shortWeak = new WeakReference(new object());
// Long weak reference - Survives GC until finalizer runs
var longWeak = new WeakReference(new object(), true);
// Track resurrection (for debugging)
var trackResurrection = new WeakReference<object>(new object(), true);
}
// GENERIC WEAK REFERENCE (C# 4+)
public void GenericWeakReference()
{
var obj = new LargeObject();
var weakRef = new WeakReference<LargeObject>(obj);
obj = null;
GC.Collect();
// Try get target (returns true if alive)
if (weakRef.TryGetTarget(out LargeObject target))
{
target.Process();
}
else
{
Console.WriteLine("Object was collected");
}
}
// USE CASE 1: CACHE WITH AUTOMATIC CLEANUP
public class WeakCache<TKey, TValue> where TValue : class
{
private readonly Dictionary<TKey, WeakReference<TValue>> _cache = new();
public void Add(TKey key, TValue value)
{
_cache[key] = new WeakReference<TValue>(value);
}
public TValue Get(TKey key)
{
if (_cache.TryGetValue(key, out var weakRef))
{
if (weakRef.TryGetTarget(out TValue target))
{
return target;
}
else
{
// Object was collected, remove from cache
_cache.Remove(key);
}
}
return null;
}
public void Cleanup()
{
// Remove dead entries
var deadKeys = new List<TKey>();
foreach (var kvp in _cache)
{
if (!kvp.Value.TryGetTarget(out _))
{
deadKeys.Add(kvp.Key);
}
}
foreach (var key in deadKeys)
{
_cache.Remove(key);
}
}
}
// USE CASE 2: EVENT MEMORY LEAK PREVENTION
public class WeakEventManager
{
// Using weak references to prevent memory leaks
private readonly List<WeakReference<EventHandler>> _handlers = new();
public void AddHandler(EventHandler handler)
{
_handlers.Add(new WeakReference<EventHandler>(handler));
}
public void RaiseEvent(object sender, EventArgs e)
{
var deadHandlers = new List<WeakReference<EventHandler>>();
foreach (var weakRef in _handlers)
{
if (weakRef.TryGetTarget(out var handler))
{
handler?.Invoke(sender, e);
}
else
{
deadHandlers.Add(weakRef);
}
}
// Clean up dead references
foreach (var dead in deadHandlers)
{
_handlers.Remove(dead);
}
}
}
// USE CASE 3: CONDITIONAL CACHING
public class ConditionalWeakTable<TKey, TValue> where TKey : class where TValue : class
{
// .NET's ConditionalWeakTable automatically handles weak references
// Perfect for attaching data to objects without memory leaks
}
public class AttachedProperties
{
private static readonly ConditionalWeakTable<object, Dictionary<string, object>> _properties
= new ConditionalWeakTable<object, Dictionary<string, object>>();
public static void SetProperty(object obj, string name, object value)
{
var props = _properties.GetOrCreateValue(obj);
props[name] = value;
}
public static object GetProperty(object obj, string name)
{
if (_properties.TryGetValue(obj, out var props))
{
return props.GetValueOrDefault(name);
}
return null;
}
}
// USE CASE 4: VIEW MODELS IN MVVM
public class WeakViewModelLocator
{
private readonly Dictionary<Type, WeakReference<object>> _viewModels = new();
public T GetViewModel<T>() where T : class, new()
{
var type = typeof(T);
if (_viewModels.TryGetValue(type, out var weakRef))
{
if (weakRef.TryGetTarget(out T existing))
{
return existing;
}
}
// Create new instance
var newInstance = new T();
_viewModels[type] = new WeakReference<object>(newInstance);
return newInstance;
}
}
// PERFORMANCE COMPARISON
public void PerformanceTest()
{
const int count = 100000;
// Strong references (all kept alive)
var strongList = new List<LargeObject>();
for (int i = 0; i < count; i++)
{
strongList.Add(new LargeObject());
}
// Weak references (can be collected)
var weakList = new List<WeakReference<LargeObject>>();
for (int i = 0; i < count; i++)
{
var obj = new LargeObject();
weakList.Add(new WeakReference<LargeObject>(obj));
// Note: obj goes out of scope and can be collected
}
GC.Collect();
// Count survivors
int survivors = 0;
foreach (var weakRef in weakList)
{
if (weakRef.TryGetTarget(out _)) survivors++;
}
Console.WriteLine($"Strong references: {count} objects kept");
Console.WriteLine($"Weak references: {survivors} objects survived GC");
}
}

// Helper class
public class LargeObject
{
private byte[] _data = new byte[1024 * 1024]; // 1MB
public void Process()
{
Console.WriteLine("Processing large object");
}
}

// REAL-WORLD EXAMPLE - Image caching with weak references
public class ImageCache
{
private readonly Dictionary<string, WeakReference<Bitmap>> _cache = new();
public Bitmap GetImage(string url)
{
if (_cache.TryGetValue(url, out var weakRef))
{
if (weakRef.TryGetTarget(out Bitmap cached))
{
Console.WriteLine($"Cache hit: {url}");
return cached;
}
else
{
// Cache entry expired
_cache.Remove(url);
}
}
// Load image (expensive operation)
Console.WriteLine($"Loading: {url}");
var bitmap = LoadImageFromUrl(url);
_cache[url] = new WeakReference<Bitmap>(bitmap);
return bitmap;
}
private Bitmap LoadImageFromUrl(string url)
{
// Simulate loading
return new Bitmap(100, 100);
}
public void Cleanup()
{
var expired = _cache.Where(kvp => !kvp.Value.TryGetTarget(out _)).Select(kvp => kvp.Key).ToList();
foreach (var key in expired)
{
_cache.Remove(key);
}
Console.WriteLine($"Cleaned up {expired.Count} expired entries. Cache size: {_cache.Count}");
}
}

// Dummy Bitmap class for demonstration
public class Bitmap : IDisposable
{
public int Width { get; }
public int Height { get; }
public Bitmap(int width, int height)
{
Width = width;
Height = height;
}
public void Dispose() { }
}

50. Span and Memory (Modern Memory Management)

using System;
using System.Buffers;
using System.Runtime.InteropServices;

public class SpanMemoryDemo
{
// SPAN<T> - Stack-only type for safe, allocation-free memory access
// Memory<T> - Heap-friendly version of Span
public void SpanBasics()
{
// Span from array
int[] array = [1, 2, 3, 4, 5];
Span<int> spanFromArray = array.AsSpan();
// Span from stack (no allocation!)
Span<int> stackSpan = stackalloc int[100];
// Span from native memory
IntPtr ptr = Marshal.AllocHGlobal(100);
Span<byte> nativeSpan = new Span<byte>(ptr.ToPointer(), 100);
// Span from string (read-only)
string text = "Hello";
ReadOnlySpan<char> charSpan = text.AsSpan();
// Modify through span
spanFromArray[0] = 10; // Modifies original array
Console.WriteLine(array[0]); // 10
}
public void SpanOperations()
{
Span<int> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Slicing (no allocation)
Span<int> firstThree = numbers[0..3]; // [1,2,3]
Span<int> lastThree = numbers[^3..]; // [8,9,10]
Span<int> middle = numbers[3..7]; // [4,5,6,7]
// Fill
numbers.Fill(0);
// Clear
numbers.Clear();
// Copy
Span<int> destination = stackalloc int[10];
numbers.CopyTo(destination);
// Reverse
numbers.Reverse();
// Sort
numbers.Sort();
// Search
int index = numbers.IndexOf(5);
bool contains = numbers.Contains(5);
// Split (simulate)
ReadOnlySpan<char> csv = "1,2,3,4,5".AsSpan();
while (true)
{
int commaIndex = csv.IndexOf(',');
if (commaIndex == -1) break;
ReadOnlySpan<char> item = csv[0..commaIndex];
csv = csv[(commaIndex + 1)..];
}
}
// PERFORMANCE: Parse string without allocation
public int ParseFirstNumberNoAllocation(ReadOnlySpan<char> text)
{
int spaceIndex = text.IndexOf(' ');
if (spaceIndex == -1) return 0;
var numberSpan = text[0..spaceIndex];
// No string allocation during parsing!
if (int.TryParse(numberSpan, out int result))
{
return result;
}
return 0;
}
// PERFORMANCE: Low-allocation CSV parser
public class CsvParser
{
public static List<ReadOnlySpan<char>> ParseLine(ReadOnlySpan<char> line)
{
var fields = new List<ReadOnlySpan<char>>();
int start = 0;
for (int i = 0; i <= line.Length; i++)
{
if (i == line.Length || line[i] == ',')
{
fields.Add(line[start..i]);
start = i + 1;
}
}
return fields;
}
}
// MEMORY<T> - For scenarios where Span can't be used (async, fields)
public class AsyncProcessor
{
private Memory<byte> _buffer;
public AsyncProcessor(int size)
{
_buffer = new byte[size];
}
public async Task ProcessAsync()
{
// Memory can be used in async methods
await Task.Delay(100);
// Get span for synchronous operations
Span<byte> span = _buffer.Span;
span[0] = 255;
}
public Memory<byte> GetBuffer() => _buffer;
}
// MEMORY OWNERSHIP - IMemoryOwner<T> and ArrayPool
public class BufferManager : IDisposable
{
private IMemoryOwner<byte> _memoryOwner;
public BufferManager(int size)
{
// Rent from shared pool
_memoryOwner = MemoryPool<byte>.Shared.Rent(size);
}
public Memory<byte> GetMemory() => _memoryOwner.Memory;
public void Dispose()
{
_memoryOwner?.Dispose(); // Returns to pool
}
}
// USE CASES
// 1. Working with network buffers
public class NetworkParser
{
public void ParsePacket(ReadOnlySpan<byte> packet)
{
// Read without allocating
byte header = packet[0];
ushort length = BitConverter.ToUInt16(packet.Slice(1, 2));
var payload = packet.Slice(3, length);
// Process payload
}
}
// 2. String processing (allocation-free)
public class StringProcessor
{
public bool IsValidEmail(ReadOnlySpan<char> email)
{
// Find @ symbol
int atIndex = email.IndexOf('@');
if (atIndex <= 0 || atIndex == email.Length - 1)
return false;
// Check local part
var localPart = email[0..atIndex];
if (localPart.Length == 0)
return false;
// Check domain
var domain = email[(atIndex + 1)..];
if (domain.Length < 3)
return false;
// Check for dot in domain
return domain.IndexOf('.') > 0;
}
}
// 3. Working with native interop
public class NativeInterop
{
[DllImport("kernel32.dll")]
private static extern unsafe int ReadFile(IntPtr hFile, byte* lpBuffer, int nNumberOfBytesToRead, out int lpNumberOfBytesRead, IntPtr lpOverlapped);
public unsafe void ReadFileExample(IntPtr fileHandle)
{
byte* buffer = stackalloc byte[4096]; // Stack allocation
if (ReadFile(fileHandle, buffer, 4096, out int bytesRead, IntPtr.Zero))
{
ReadOnlySpan<byte> data = new ReadOnlySpan<byte>(buffer, bytesRead);
ProcessData(data);
}
}
private void ProcessData(ReadOnlySpan<byte> data) { }
}
// 4. Fast search without allocations
public class FastSearch
{
public static int IndexOfAny(ReadOnlySpan<char> text, ReadOnlySpan<char> searchChars)
{
for (int i = 0; i < text.Length; i++)
{
if (searchChars.Contains(text[i]))
return i;
}
return -1;
}
public static int CountOccurrences(ReadOnlySpan<char> text, char searchChar)
{
int count = 0;
foreach (char c in text)
{
if (c == searchChar) count++;
}
return count;
}
}
// PERFORMANCE COMPARISON
public class PerformanceComparison
{
private const int Iterations = 1_000_000;
public void CompareStringVsSpan()
{
string text = "123,456,789,101,112,131,415,161,718,192";
// String version (allocates substrings)
var sw = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < Iterations; i++)
{
var parts = text.Split(',');
foreach (var part in parts)
{
int.Parse(part);
}
}
sw.Stop();
Console.WriteLine($"String: {sw.ElapsedMilliseconds}ms");
// Span version (no allocations)
sw.Restart();
ReadOnlySpan<char> spanText = text.AsSpan();
for (int i = 0; i < Iterations; i++)
{
var remaining = spanText;
while (true)
{
int commaIndex = remaining.IndexOf(',');
if (commaIndex == -1)
{
int.Parse(remaining);
break;
}
var part = remaining[0..commaIndex];
int.Parse(part);
remaining = remaining[(commaIndex + 1)..];
}
}
sw.Stop();
Console.WriteLine($"Span: {sw.ElapsedMilliseconds}ms");
// Span is typically 2-5x faster
}
}
}

// ADVANCED: Custom buffer writer with Span
public ref struct BufferWriter
{
private Span<byte> _buffer;
private int _position;
public BufferWriter(Span<byte> buffer)
{
_buffer = buffer;
_position = 0;
}
public bool TryWrite(ReadOnlySpan<byte> data)
{
if (_position + data.Length > _buffer.Length)
return false;
data.CopyTo(_buffer[_position..]);
_position += data.Length;
return true;
}
public void WriteByte(byte value)
{
if (_position < _buffer.Length)
{
_buffer[_position] = value;
_position++;
}
}
public int WrittenBytes => _position;
public ReadOnlySpan<byte> WrittenData => _buffer[0.._position];
}

// Usage of custom writer
public class CustomWriterExample
{
public void UseBufferWriter()
{
Span<byte> buffer = stackalloc byte[256];
var writer = new BufferWriter(buffer);
writer.WriteByte(0x01);
writer.TryWrite("Hello"u8);
var result = writer.WrittenData;
// Use result
}
}

Module 4 Summary

You've learned deep .NET runtime concepts:

  1. Value vs Reference Types - Memory layout, stack vs heap
  2. Boxing/Unboxing - Performance impact, how to avoid
  3. Stack vs Heap - Allocation strategies, stackalloc, Span
  4. Garbage Collection - Generations, finalization, optimization
  5. IDisposable - Proper disposal patterns, using statements
  6. Finalizers - Destructors, critical finalizers, resurrection
  7. Weak References - Memory leaks prevention, caching
  8. Span/Memory - Modern allocation-free programming

Practice Exercises for Module 4

Exercise 1: Memory Leak Detector

// Create a tool that detects potential memory leaks
// - Track object allocations
// - Identify objects that should be disposed
// - Generate report of suspicious patterns

Exercise 2: Object Pool Implementation

// Implement a generic object pool for expensive objects
// - Support minimum/maximum pool size
// - Handle IDisposable objects
// - Thread-safe borrowing and returning

Exercise 3: High-Performance Logging

// Create a logging system that minimizes allocations
// - Use Span<char> for log formatting
// - Use ArrayPool for buffers
// - Implement custom formatting without string allocations

Exercise 4: Cache with Weak References

// Implement LRU cache using WeakReference
// - Automatically expire items when memory pressure increases
// - Track hit/miss ratio
// - Provide cleanup statistics

Ready for Module 5?

Module 5: Asynchronous Programming covers:

  1. Task and Task deep dive
  2. Async/Await pattern internals
  3. Cancellation tokens and progress reporting
  4. ConfigureAwait best practices
  5. Async streams (IAsyncEnumerable)
  6. Parallel programming (Parallel.For, PLINQ)
  7. Concurrent collections (thread-safe)