πŸ”·

C# Type System

Language Fundamentals Beginner 5 min read 1000 words
C#

C# Type System

The C# type system is the foundation of all .NET programming. Understanding value types vs reference types, memory allocation, and type conversions is essential for writing efficient, bug-free code.

Table of Contents


Value Types vs Reference Types

Value Types

Value types directly contain their data. When you assign a value type to another variable, a copy of the data is created.

// Value types are stored on the stack (when local variables)
int a = 42;
int b = a;    // b gets a COPY of a's value
b = 100;      // a is still 42

// Built-in value types
bool isValid = true;           // Boolean
byte smallNumber = 255;        // 8-bit unsigned (0-255)
short mediumNumber = 32767;    // 16-bit signed
int normalNumber = 2147483647; // 32-bit signed
long bigNumber = 9223372036854775807L; // 64-bit signed
float singlePrecision = 3.14f; // 32-bit floating point
double doublePrecision = 3.14159265359; // 64-bit floating point
decimal money = 123.45m;       // 128-bit decimal (financial)
char letter = 'A';             // 16-bit Unicode character

// Structs are value types
DateTime now = DateTime.Now;
TimeSpan duration = TimeSpan.FromHours(2);
Guid id = Guid.NewGuid();

Reference Types

Reference types store a reference (pointer) to the data. Multiple variables can reference the same object.

// Reference types are stored on the heap
string name1 = "John";
string name2 = name1;  // name2 references the SAME string

// Classes are reference types
Person person1 = new Person { Name = "Alice" };
Person person2 = person1;  // Both reference the SAME object
person2.Name = "Bob";      // person1.Name is also "Bob" now!

// Arrays are reference types (even arrays of value types)
int[] numbers1 = { 1, 2, 3 };
int[] numbers2 = numbers1;  // Both reference the SAME array
numbers2[0] = 99;           // numbers1[0] is also 99

// Other reference types
object obj = new object();
dynamic dyn = "hello";
string text = "immutable";

Key Differences

Aspect Value Types Reference Types
Storage Stack (usually) Heap
Assignment Copies data Copies reference
Default Zero/false/null null
Inheritance Cannot inherit Can inherit
Nullable Requires ? Nullable by default
Performance Faster allocation GC overhead

Memory Model: Stack vs Heap

Stack Memory

The stack is a LIFO (Last In, First Out) data structure used for:

  • Method call frames
  • Local value type variables
  • Method parameters
  • Return addresses
void ProcessData()
{
    // All these live on the stack
    int count = 10;           // 4 bytes on stack
    double price = 99.99;     // 8 bytes on stack
    bool isActive = true;     // 1 byte on stack

    // When ProcessData() returns, stack frame is popped
    // All local variables are automatically deallocated
}

Stack characteristics:

  • βœ… Very fast allocation/deallocation
  • βœ… Automatic memory management
  • ❌ Limited size (~1MB default on Windows)
  • ❌ LIFO access pattern only

Heap Memory

The heap is used for:

  • Reference type objects
  • Large data structures
  • Objects with unknown lifetime
void CreateObjects()
{
    // 'person' reference is on stack (8 bytes on x64)
    // Person object is on heap
    Person person = new Person();

    // 'data' reference is on stack
    // int[] array is on heap (even though int is value type!)
    int[] data = new int[1000];

    // String content is on heap
    string message = "Hello, World!";
}

Heap characteristics:

  • βœ… Dynamic size
  • βœ… Objects persist beyond method scope
  • ❌ Slower allocation
  • ❌ Requires Garbage Collection

Visual Representation

STACK                          HEAP
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ person (ref)────┼──────────►│ Person object       β”‚
β”‚ count = 10      β”‚           β”‚ Name: "John"        β”‚
β”‚ price = 99.99   β”‚           β”‚ Age: 30             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ data (ref)β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ isActive = true β”‚           β”‚ int[1000] array     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚ [0]=0, [1]=0, ...   β”‚
                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Boxing and Unboxing

What is Boxing?

Boxing converts a value type to a reference type by wrapping it in an object on the heap.

int number = 42;
object boxed = number;  // Boxing: copies value to heap

// What happens internally:
// 1. Allocate memory on heap
// 2. Copy value to heap
// 3. Return reference to heap object

What is Unboxing?

Unboxing extracts the value type from the boxed object.

object boxed = 42;      // Boxing
int number = (int)boxed; // Unboxing: copies value back to stack

// What happens internally:
// 1. Check if boxed is actually an int (throws InvalidCastException if not)
// 2. Copy value from heap to stack

Performance Impact

Boxing is expensive! Each boxing operation:

  • Allocates ~12 bytes + value size on heap
  • Copies the value
  • Creates GC pressure
// ❌ BAD: Boxing in a loop
ArrayList list = new ArrayList();
for (int i = 0; i < 1000000; i++)
{
    list.Add(i);  // Boxing happens 1 million times!
}

// βœ… GOOD: Use generic collections
List<int> list = new List<int>();
for (int i = 0; i < 1000000; i++)
{
    list.Add(i);  // No boxing - List<int> stores int directly
}

Common Boxing Scenarios

// 1. Assigning to object
object obj = 42;  // Boxing

// 2. Calling methods that take object
void Log(object value) { }
Log(42);  // Boxing

// 3. String interpolation (sometimes)
int count = 5;
string s = $"Count: {count}";  // May box depending on runtime

// 4. Using non-generic collections
Hashtable ht = new Hashtable();
ht.Add("key", 42);  // Boxing

// 5. Interface dispatch on value types
IComparable comparable = 42;  // Boxing

Avoiding Boxing

// Use generics
public void Process<T>(T value) where T : struct
{
    // No boxing - T is constrained to value types
}

// Use Span<T> for array operations
Span<int> span = stackalloc int[100];

// Use ref struct for stack-only types
ref struct StackOnlyData
{
    public int Value;
}

Nullable Types

Nullable Value Types

Value types cannot be null by default. Use ? to make them nullable.

// Regular value type - cannot be null
int count = 0;
// count = null;  // Compile error!

// Nullable value type - can be null
int? nullableCount = null;
nullableCount = 42;

// Checking for value
if (nullableCount.HasValue)
{
    int value = nullableCount.Value;
    Console.WriteLine(value);
}

// Null-coalescing operator
int result = nullableCount ?? 0;  // Returns 0 if null

// Null-conditional with value types
int? length = text?.Length;  // null if text is null

Nullable Reference Types (C# 8+)

Enable nullable reference types to catch null reference bugs at compile time.

#nullable enable

// Non-nullable reference type - compiler warns if null
string name = "John";
// name = null;  // Warning: assigning null to non-nullable

// Nullable reference type - explicitly allows null
string? nickname = null;

// Working with nullable references
if (nickname != null)
{
    Console.WriteLine(nickname.Length);  // Safe - compiler knows it's not null
}

// Null-forgiving operator (use sparingly!)
string definitelyNotNull = nickname!;  // Tells compiler "trust me"

Null-Coalescing Patterns

// ?? - returns right side if left is null
string name = GetName() ?? "Unknown";

// ??= - assigns only if null
name ??= "Default";

// ?. - null-conditional access
int? length = text?.Length;

// Combined patterns
string result = user?.Profile?.DisplayName ?? "Anonymous";

// Throw if null
string required = value ?? throw new ArgumentNullException(nameof(value));

Struct vs Class

When to Use Struct

Use struct when:

  • Data is small (< 16 bytes ideally)
  • Data is logically a single value
  • Data is immutable
  • You need value semantics
  • Avoiding heap allocation is important
// βœ… GOOD struct examples
public readonly struct Point
{
    public double X { get; }
    public double Y { get; }

    public Point(double x, double y) => (X, Y) = (x, y);

    public double DistanceTo(Point other) =>
        Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
}

public readonly struct Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency) =>
        (Amount, Currency) = (amount, currency);
}

public readonly struct DateRange
{
    public DateTime Start { get; }
    public DateTime End { get; }

    public TimeSpan Duration => End - Start;
}

When to Use Class

Use class when:

  • Object has complex behavior
  • Object needs inheritance
  • Object is large (> 16 bytes)
  • Reference semantics are needed
  • Object needs to be modified after creation
// βœ… GOOD class examples
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Order> Orders { get; } = new();

    public decimal TotalSpent => Orders.Sum(o => o.Amount);
}

public class FileProcessor
{
    private readonly Stream _stream;

    public FileProcessor(Stream stream) => _stream = stream;

    public async Task ProcessAsync() { /* ... */ }
}

Decision Matrix

Criteria Use Struct Use Class
Size < 16 bytes > 16 bytes
Mutability Immutable Either
Inheritance Not needed Needed
Allocation pattern Frequent, short-lived Long-lived
Identity important No Yes
Passed frequently Yes (value copy OK) Yes (reference copy)

readonly struct

The readonly struct modifier ensures immutability and enables compiler optimizations.

// readonly struct - all fields must be readonly
public readonly struct Vector3
{
    public float X { get; }
    public float Y { get; }
    public float Z { get; }

    public Vector3(float x, float y, float z) =>
        (X, Y, Z) = (x, y, z);

    // Methods cannot modify state
    public Vector3 Normalize() =>
        new Vector3(X / Length, Y / Length, Z / Length);

    public float Length =>
        MathF.Sqrt(X * X + Y * Y + Z * Z);
}

record struct (C# 10+)

Combines struct benefits with record features.

// record struct - value semantics with record features
public record struct Coordinate(double Latitude, double Longitude);

// readonly record struct - immutable record struct
public readonly record struct Temperature(double Celsius)
{
    public double Fahrenheit => Celsius * 9 / 5 + 32;
    public double Kelvin => Celsius + 273.15;
}

ref, out, and in Parameters

ref Parameter

Passes a reference to the variable. Both caller and method can modify.

void Swap(ref int a, ref int b)
{
    int temp = a;
    a = b;
    b = temp;
}

int x = 10, y = 20;
Swap(ref x, ref y);  // x = 20, y = 10

// Must be initialized before passing
// int z;
// Swap(ref z, ref x);  // Error: z must be assigned

out Parameter

Passes a reference that the method MUST assign.

bool TryParse(string input, out int result)
{
    if (int.TryParse(input, out result))
        return true;

    result = 0;  // Must assign even on failure
    return false;
}

// Usage - doesn't need to be initialized
if (TryParse("42", out int value))
{
    Console.WriteLine(value);
}

// Discard if you don't need the value
if (TryParse("42", out _))
{
    Console.WriteLine("Valid number");
}

in Parameter

Passes a readonly reference. Prevents copying while ensuring immutability.

// βœ… GOOD: Large struct passed by readonly reference
double CalculateDistance(in Point3D a, in Point3D b)
{
    // Cannot modify a or b
    // a.X = 0;  // Error: cannot modify 'in' parameter

    return Math.Sqrt(
        Math.Pow(b.X - a.X, 2) +
        Math.Pow(b.Y - a.Y, 2) +
        Math.Pow(b.Z - a.Z, 2));
}

Point3D point1 = new(1, 2, 3);
Point3D point2 = new(4, 5, 6);
double dist = CalculateDistance(in point1, in point2);

ref return and ref local

Return references to avoid copying.

public class Matrix
{
    private double[,] _data = new double[100, 100];

    // Return reference to element
    public ref double GetElement(int row, int col) =>
        ref _data[row, col];
}

Matrix matrix = new Matrix();
ref double element = ref matrix.GetElement(0, 0);
element = 42.0;  // Modifies the matrix directly

Span and Memory

Span

Span<T> is a stack-only type that represents a contiguous region of memory.

// Create Span from array
int[] array = { 1, 2, 3, 4, 5 };
Span<int> span = array;

// Create Span from slice
Span<int> slice = array.AsSpan(1, 3);  // { 2, 3, 4 }

// Modify through Span
span[0] = 100;  // Modifies array[0]

// Stack allocation with Span
Span<int> stackArray = stackalloc int[100];
stackArray[0] = 42;

// ReadOnlySpan for immutable access
ReadOnlySpan<char> text = "Hello, World!".AsSpan();
ReadOnlySpan<char> hello = text.Slice(0, 5);  // "Hello"

Memory

Memory<T> is like Span<T> but can be stored on the heap.

// Memory can be stored in classes
public class DataProcessor
{
    private Memory<byte> _buffer;

    public DataProcessor(byte[] data)
    {
        _buffer = data;
    }

    public async Task ProcessAsync()
    {
        // Get Span for synchronous work
        Span<byte> span = _buffer.Span;

        // Memory can be used in async methods
        await ProcessBufferAsync(_buffer);
    }
}

Performance Benefits

// ❌ BAD: Creates new string allocations
string ExtractYear(string date)
{
    return date.Substring(0, 4);  // Allocates new string
}

// βœ… GOOD: No allocations
ReadOnlySpan<char> ExtractYearSpan(ReadOnlySpan<char> date)
{
    return date.Slice(0, 4);  // Returns slice of existing memory
}

// Usage
string date = "2024-01-15";
ReadOnlySpan<char> year = ExtractYearSpan(date);  // No allocation!

Interview Questions

Q1: What’s the difference between value types and reference types?

A: Value types store data directly and are typically allocated on the stack. When assigned to another variable, the value is copied. Reference types store a reference to data on the heap, and assignment copies only the reference, not the data.

Q2: What is boxing and why should you avoid it?

A: Boxing is converting a value type to a reference type (object), which allocates memory on the heap and copies the value. It should be avoided because it:

  • Causes heap allocations
  • Creates GC pressure
  • Requires type checking during unboxing
  • Is significantly slower than value type operations

Use generics to avoid boxing.

Q3: When would you use a struct instead of a class?

A: Use a struct when:

  • The data is small (< 16 bytes)
  • It represents a single value
  • It’s immutable
  • Value semantics are desired
  • Frequent allocation/deallocation is needed

Q4: What is Span<T> and when would you use it?

A: Span<T> is a stack-only type that provides a type-safe, memory-safe view into a contiguous region of memory. Use it for:

  • Zero-allocation string/array slicing
  • Stack-allocated arrays (stackalloc)
  • High-performance parsing and manipulation
  • Avoiding array copies

Q5: Explain the difference between ref, out, and in parameters.

A:

  • ref: Passes by reference, must be initialized, can be read and modified
  • out: Passes by reference, doesn’t need initialization, must be assigned by method
  • in: Passes by readonly reference, prevents copying large structs while ensuring immutability

Q6: Why are strings immutable in C#?

A: Strings are immutable for:

  • Thread safety (no locking needed)
  • String interning (sharing identical strings)
  • Security (strings can’t be modified after validation)
  • Hashing (hash codes remain consistent)

Any β€œmodification” creates a new string object.


Common Pitfalls

  • ❌ Boxing value types in hot paths
  • ❌ Using mutable structs (confusing behavior when copied)
  • ❌ Large structs (> 16 bytes) causing excessive copying
  • ❌ Ignoring nullable reference type warnings
  • ❌ Using out when the value is always needed (prefer return value)

Best Practices

  • βœ… Use readonly struct for immutable value types
  • βœ… Enable nullable reference types in new projects
  • βœ… Use generic collections to avoid boxing
  • βœ… Use Span<T> for high-performance scenarios
  • βœ… Prefer in parameter for large readonly structs
  • βœ… Use records for data-centric types

Sources

  • Microsoft Docs: Value Types
  • Microsoft Docs: Memory and Span
  • .NET Design Guidelines

πŸ“š Related Articles