Complete Beginner's Guide to C# Types: Class vs Record vs Struct + Pattern Matching


If you're new to C# programming, you've probably wondered: "When should I use a class, record, or struct?" This comprehensive guide will explain everything you need to know about these three fundamental types in C#, plus show you the amazing world of pattern matching with switch statements.

What Are These Types Anyway?

Think of C# types as different containers for your data and behavior:

  • Class: Like a blueprint for complex objects (houses, cars, people)
  • Record: Like a form with pre-filled information (student records, invoices)
  • Struct: Like a simple container for basic data (coordinates, colors)

Understanding the Basics

What is a Class?

A class is a reference type that lives on the heap. It's perfect for complex objects that need to change over time and can inherit from other classes.

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    
    public Person(string firstName, string lastName, int age)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
    }
    
    public void CelebrateBirthday()
    {
        Age++;
        Console.WriteLine($"Happy Birthday! Now {Age} years old.");
    }
}

What is a Record?

A record is designed for immutable data with built-in helpful features. By default, it's a reference type (record class), but you can make it a value type (record struct).

// Simple record - automatically creates properties and useful methods
public record PersonRecord(string FirstName, string LastName, int Age);

// Record struct (value type)
public record struct Point(int X, int Y);

What is a Struct?

A struct is a value type that typically lives on the stack. Perfect for small, simple data containers.

public struct Coordinate
{
    public int X { get; set; }
    public int Y { get; set; }
    
    public Coordinate(int x, int y)
    {
        X = x;
        Y = y;
    }
}

Complete Feature Comparison

Memory and Performance

Feature Class Record Struct
Type Category Reference type Reference type (record class) / Value type (record struct) Value type
Memory Location Heap Heap (record class) / Stack (record struct) Stack (small structs)
Can be null Yes Yes (record class) / No (record struct) No (unless nullable)
Assignment behavior Copies reference Copies reference (record class) / Copies value (record struct) Copies entire value

Object-Oriented Features

Feature Class Record Struct
Inheritance Full support Record class: Yes / Record struct: No No
Can be inherited Yes Record class: Yes / Record struct: No No
Abstract classes Yes Record class: Yes / Record struct: No No
Virtual methods Yes Record class: Yes / Record struct: No No
Interface support Yes Yes Yes

Construction and Initialization

Feature Class Record Struct
Primary constructor C# 12+ (creates fields) Yes (creates properties) C# 12+ (creates fields)
Multiple constructors Yes Yes Yes (but no parameterless)
Property initialization Mutable by default Init-only by default Mutable by default
Destructors Yes Record class: Yes / Record struct: No No

Record-Exclusive Features (The Cool Stuff!)

Records come with amazing built-in features that save you tons of coding:

1. Automatic ToString() Method

var person = new PersonRecord("John", "Doe", 30);
Console.WriteLine(person); 
// Output: PersonRecord { FirstName = John, LastName = Doe, Age = 30 }

2. with Expressions (Non-destructive Mutation)

var originalPerson = new PersonRecord("Alice", "Smith", 25);
var olderPerson = originalPerson with { Age = 30 };
var marriedPerson = originalPerson with { LastName = "Johnson" };

Console.WriteLine(originalPerson); // Alice Smith, 25
Console.WriteLine(olderPerson);    // Alice Smith, 30  
Console.WriteLine(marriedPerson);  // Alice Johnson, 25

3. Automatic Deconstruction

var person = new PersonRecord("Bob", "Wilson", 35);
var (firstName, lastName, age) = person; // Automatic deconstruction!

Console.WriteLine($"Name: {firstName} {lastName}, Age: {age}");

4. Value-Based Equality

var person1 = new PersonRecord("Jane", "Doe", 28);
var person2 = new PersonRecord("Jane", "Doe", 28);

Console.WriteLine(person1 == person2); // True! (compares values)

// Compare with class:
var class1 = new Person("Jane", "Doe", 28);
var class2 = new Person("Jane", "Doe", 28);
Console.WriteLine(class1 == class2); // False! (compares references)

Complete Working Example

Here's a practical example showing all three types in action:

using System;

// CLASS - Complex mutable objects
public class BankAccount
{
    public string AccountNumber { get; set; }
    public decimal Balance { get; set; }
    public string Owner { get; set; }
    
    public BankAccount(string accountNumber, string owner, decimal initialBalance = 0)
    {
        AccountNumber = accountNumber;
        Owner = owner;
        Balance = initialBalance;
    }
    
    public void Deposit(decimal amount)
    {
        Balance += amount;
        Console.WriteLine($"Deposited ${amount}. New balance: ${Balance}");
    }
}

// RECORD - Immutable data models
public record CustomerRecord(string Name, string Email, DateTime JoinDate);

// RECORD STRUCT - Small immutable value types with record benefits
public readonly record struct Money(decimal Amount, string Currency)
{
    public override string ToString() => $"{Amount:C} {Currency}";
}

// STRUCT - Simple mutable value types
public struct Point3D
{
    public double X, Y, Z;
    
    public Point3D(double x, double y, double z)
    {
        X = x; Y = y; Z = z;
    }
    
    public double DistanceFromOrigin => Math.Sqrt(X*X + Y*Y + Z*Z);
}

class Program
{
    static void Main()
    {
        Console.WriteLine("=== CLASS EXAMPLE ===");
        var account = new BankAccount("12345", "John Doe", 1000);
        account.Deposit(500); // Mutable - can change state
        
        Console.WriteLine("\n=== RECORD EXAMPLE ===");
        var customer = new CustomerRecord("Alice Smith", "alice@email.com", DateTime.Now);
        var premiumCustomer = customer with { Name = "Alice Smith (Premium)" };
        Console.WriteLine($"Original: {customer}");
        Console.WriteLine($"Premium:  {premiumCustomer}");
        
        Console.WriteLine("\n=== RECORD STRUCT EXAMPLE ===");
        var price1 = new Money(99.99m, "USD");
        var price2 = new Money(99.99m, "USD");
        Console.WriteLine($"Price 1: {price1}");
        Console.WriteLine($"Equal? {price1 == price2}"); // Value equality
        
        Console.WriteLine("\n=== STRUCT EXAMPLE ===");
        var point = new Point3D(3, 4, 5);
        Console.WriteLine($"Point: ({point.X}, {point.Y}, {point.Z})");
        Console.WriteLine($"Distance from origin: {point.DistanceFromOrigin:F2}");
    }
}

Pattern Matching with Switch - The Modern Way

Pattern matching is like having a super-smart conditional statement that can examine objects in sophisticated ways!

Traditional Switch vs Modern Switch Expression

// OLD WAY: Switch Statement
public static string GetDayTypeOld(DayOfWeek day)
{
    switch (day)
    {
        case DayOfWeek.Monday:
        case DayOfWeek.Tuesday:
        case DayOfWeek.Wednesday:
        case DayOfWeek.Thursday:
        case DayOfWeek.Friday:
            return "Weekday";
        case DayOfWeek.Saturday:
        case DayOfWeek.Sunday:
            return "Weekend";
        default:
            return "Unknown";
    }
}

// NEW WAY: Switch Expression
public static string GetDayTypeNew(DayOfWeek day) => day switch
{
    DayOfWeek.Monday or DayOfWeek.Tuesday or DayOfWeek.Wednesday 
    or DayOfWeek.Thursday or DayOfWeek.Friday => "Weekday",
    DayOfWeek.Saturday or DayOfWeek.Sunday => "Weekend",
    _ => "Unknown"
};

Amazing Pattern Types

1. Type Patterns

public static string ProcessValue(object value) => value switch
{
    string s => $"String with {s.Length} characters",
    int i when i > 0 => $"Positive number: {i}",
    int i => $"Non-positive number: {i}",
    double d => $"Decimal number: {d:F2}",
    bool b => $"Boolean: {b}",
    null => "Nothing here!",
    _ => $"Unknown type: {value.GetType().Name}"
};

2. Property Patterns

public record Student(string Name, int Age, int Grade);

public static string CategorizeStudent(Student student) => student switch
{
    { Age: < 18, Grade: >= 90 } => "Exceptional minor student",
    { Age: >= 18, Grade: >= 85 } => "Excellent adult student",
    { Grade: >= 70 } => "Good student",
    { Grade: >= 60 } => "Average student",
    { Name: "John" } => "It's John (regardless of grades)",
    _ => "Needs improvement"
};

3. Relational Patterns (Numbers and Comparisons)

public static string CategorizeTemperature(int temp) => temp switch
{
    < 0 => "Freezing",
    >= 0 and < 10 => "Very cold",
    >= 10 and < 20 => "Cold",
    >= 20 and < 30 => "Comfortable",
    >= 30 and < 40 => "Hot",
    >= 40 => "Extremely hot",
    _ => "Invalid temperature"
};

4. List Patterns (C# 11+) - Super Cool!

public static string AnalyzeNumbers(int[] numbers) => numbers switch
{
    [] => "Empty list",
    [var single] => $"One number: {single}",
    [1, 2, 3] => "Classic sequence 1,2,3",
    [var first, .., var last] => $"Starts with {first}, ends with {last}",
    [42, ..] => "Starts with the answer to everything!",
    [.., 0] => "Ends with zero",
    [> 0, ..] => "All positive numbers",
    _ => "Mixed numbers"
};

5. Complex Real-World Example

// Shape hierarchy
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
public record Triangle(double Base, double Height) : Shape;

public static class ShapeProcessor
{
    public static double CalculateArea(Shape shape) => shape switch
    {
        Circle(var radius) => Math.PI * radius * radius,
        Rectangle(var width, var height) => width * height,
        Triangle(var baseLength, var height) => baseLength * height / 2,
        _ => 0
    };
    
    public static string DescribeShape(Shape shape) => shape switch
    {
        Circle { Radius: > 10 } => "Large circle",
        Circle { Radius: var r } => $"Small circle (radius: {r})",
        Rectangle { Width: var w, Height: var h } when w == h => "Square",
        Rectangle { Width: > 20 } => "Wide rectangle", 
        Rectangle => "Regular rectangle",
        Triangle { Base: var b, Height: var h } when b * h > 50 => "Big triangle",
        Triangle => "Small triangle",
        _ => "Unknown shape"
    };
}

// Usage example
static void TestShapes()
{
    var shapes = new Shape[]
    {
        new Circle(5),
        new Circle(15),
        new Rectangle(10, 10),
        new Rectangle(25, 5),
        new Triangle(8, 12)
    };
    
    foreach (var shape in shapes)
    {
        var area = ShapeProcessor.CalculateArea(shape);
        var description = ShapeProcessor.DescribeShape(shape);
        Console.WriteLine($"{description} - Area: {area:F2}");
    }
}

When to Use What? Simple Decision Guide

Use a CLASS when:

  • You need complex behavior (methods that do lots of things)
  • The object needs to change over time (mutable)
  • You want inheritance (parent-child relationships)
  • You're building complex business logic
  • Examples: BankAccount, GamePlayer, EmailService

Use a RECORD when:

  • You're storing data that shouldn't change (immutable)
  • You want automatic equality comparisons
  • You need data transfer objects (DTOs)
  • You want concise syntax with powerful features
  • Examples: Customer info, Order details, Configuration settings

Use a STRUCT when:

  • You have simple, small data (≤ 16 bytes recommended)
  • You want value semantics (copying behavior)
  • Performance is critical for small data
  • No inheritance needed
  • Examples: Point coordinates, Color values, Complex numbers

Common Beginner Mistakes to Avoid

1. Large Structs

// DON'T DO THIS - struct is too big!
public struct BadStruct
{
    public string Name;        // 8 bytes reference
    public string Description; // 8 bytes reference
    public int[] Numbers;      // 8 bytes reference
    public DateTime Created;   // 8 bytes
    public decimal Price;      // 16 bytes
    // Total: 48 bytes - too big for a struct!
}

// DO THIS instead - use a record or class
public record GoodRecord(string Name, string Description, int[] Numbers, DateTime Created, decimal Price);

2. Forgetting About Immutability

var person = new PersonRecord("John", "Doe", 30);
// person.Age = 31; // COMPILER ERROR! Records are immutable by default

// Correct way:
var olderPerson = person with { Age = 31 };

3. Reference vs Value Confusion

// Classes (reference types)
var person1 = new Person("Alice", "Smith", 25);
var person2 = person1; // Same reference!
person2.Age = 30;
Console.WriteLine(person1.Age); // 30 - both changed!

// Structs (value types)  
var point1 = new Point(5, 10);
var point2 = point1; // Separate copy!
point2.X = 99;
Console.WriteLine(point1.X); // Still 5 - only point2 changed

Modern C# Best Practices

1. Prefer Records for Data

// Instead of this class:
public class PersonClass
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    
    // Need to manually implement:
    // - ToString()
    // - Equals()
    // - GetHashCode()
    // - Constructor
}

// Use this record:
public record Person(string FirstName, string LastName, int Age);
// Gets everything automatically!

2. Use Pattern Matching for Complex Logic

// Instead of long if-else chains:
public string GetDiscountOld(Customer customer)
{
    if (customer.Age >= 65)
        return "Senior discount: 20%";
    else if (customer.Age < 18)
        return "Student discount: 15%";
    else if (customer.MembershipYears >= 5)
        return "Loyalty discount: 10%";
    else if (customer.TotalSpent > 1000)
        return "VIP discount: 12%";
    else
        return "No discount";
}

// Use pattern matching:
public string GetDiscount(Customer customer) => customer switch
{
    { Age: >= 65 } => "Senior discount: 20%",
    { Age: < 18 } => "Student discount: 15%", 
    { MembershipYears: >= 5 } => "Loyalty discount: 10%",
    { TotalSpent: > 1000 } => "VIP discount: 12%",
    _ => "No discount"
};

3. Use Primary Constructors (C# 12+)

// Old way (lots of boilerplate):
public class PersonOld
{
    public string FirstName { get; }
    public string LastName { get; }
    public int Age { get; }
    
    public PersonOld(string firstName, string lastName, int age)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
    }
}

// New way (much cleaner):
public class Person(string firstName, string lastName, int age)
{
    public string FirstName { get; } = firstName;
    public string LastName { get; } = lastName; 
    public int Age { get; } = age;
}

COMPARISION DEMO

using System;
using System.Text.Json;

namespace RecordFeaturesDemo
{
    // Traditional Class - Manual implementation required
    public class PersonClass
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }

        public PersonClass(string firstName, string lastName, int age)
        {
            FirstName = firstName;
            LastName = lastName;
            Age = age;
        }

        // Manual ToString override needed for readable output
        public override string ToString() => $"PersonClass: {FirstName} {LastName}, Age: {Age}";
        
        // Manual equality implementation needed
        public override bool Equals(object obj) => obj is PersonClass other && 
            FirstName == other.FirstName && LastName == other.LastName && Age == other.Age;
        
        public override int GetHashCode() => HashCode.Combine(FirstName, LastName, Age);
    }

    // Traditional Struct - Manual implementation required
    public struct PersonStruct
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }

        public PersonStruct(string firstName, string lastName, int age)
        {
            FirstName = firstName;
            LastName = lastName;
            Age = age;
        }

        // Manual ToString override needed
        public override string ToString() => $"PersonStruct: {FirstName} {LastName}, Age: {Age}";
    }

    // RECORD with automatic features - no manual implementation needed!
    public record PersonRecord(string FirstName, string LastName, int Age);

    // Record struct with custom properties
    public record struct PointRecord(double X, double Y)
    {
        public double Distance => Math.Sqrt(X * X + Y * Y);
    }

    // Inheritance example - only records and classes support this
    public record Employee(string FirstName, string LastName, int Age, string Department) 
        : PersonRecord(FirstName, LastName, Age);

    class Program
    {
        static void Main()
        {
            Console.WriteLine("=== RECORD-EXCLUSIVE FEATURES DEMONSTRATION ===\n");

            // 1. PRIMARY CONSTRUCTOR WITH AUTOMATIC PROPERTIES
            Console.WriteLine("1. PRIMARY CONSTRUCTOR SYNTAX:");
            Console.WriteLine("   Record: public record Person(string FirstName, string LastName, int Age);");
            Console.WriteLine("   Class:  Requires manual constructor + properties + assignments");
            Console.WriteLine("   Struct: Requires manual constructor + properties + assignments\n");

            var person = new PersonRecord("Ada", "Lovelace", 36);
            Console.WriteLine($"   Created: {person}\n");

            // 2. AUTOMATIC ToString() GENERATION
            Console.WriteLine("2. AUTOMATIC ToString() GENERATION:");
            var personClass = new PersonClass("Ada", "Lovelace", 36);
            var personStruct = new PersonStruct("Ada", "Lovelace", 36);
            
            Console.WriteLine($"   Class:  {personClass}  (manual override required)");
            Console.WriteLine($"   Struct: {personStruct} (manual override required)");
            Console.WriteLine($"   Record: {person} (automatic!)");
            Console.WriteLine("   ↑ Record automatically shows type name + all properties!\n");

            // 3. WITH EXPRESSIONS - RECORD EXCLUSIVE!
            Console.WriteLine("3. WITH EXPRESSIONS (Record Exclusive!):");
            var olderPerson = person with { Age = 40 };
            var marriedPerson = person with { LastName = "King" };
            
            Console.WriteLine($"   Original: {person}");
            Console.WriteLine($"   Older:    {olderPerson}");
            Console.WriteLine($"   Married:  {marriedPerson}");
            Console.WriteLine("   ↑ Non-destructive mutation - creates new instances!\n");

            // 4. AUTOMATIC DECONSTRUCTION
            Console.WriteLine("4. AUTOMATIC DECONSTRUCTION:");
            var (firstName, lastName, age) = person;
            Console.WriteLine($"   Deconstructed: firstName='{firstName}', lastName='{lastName}', age={age}");
            Console.WriteLine("   ↑ Records automatically generate Deconstruct method!\n");

            // 5. VALUE-BASED EQUALITY (automatic for records)
            Console.WriteLine("5. VALUE-BASED EQUALITY:");
            var person1 = new PersonRecord("John", "Doe", 30);
            var person2 = new PersonRecord("John", "Doe", 30);
            var class1 = new PersonClass("John", "Doe", 30);
            var class2 = new PersonClass("John", "Doe", 30);

            Console.WriteLine($"   Record equality: {person1} == {person2} → {person1 == person2}");
            Console.WriteLine($"   Class equality:  {class1} == {class2} → {class1 == class2}");
            Console.WriteLine("   ↑ Records compare VALUES, classes compare REFERENCES!\n");

            // 6. POSITIONAL PATTERN MATCHING
            Console.WriteLine("6. ENHANCED PATTERN MATCHING:");
            var employee = new Employee("Jane", "Smith", 28, "Engineering");
            
            var description = employee switch
            {
                { Age: < 25, Department: "Engineering" } => "Young engineer",
                PersonRecord { Age: > 50 } => "Senior professional",
                Employee(_, _, _, "Engineering") => "Engineer", // Positional pattern!
                _ => "Professional"
            };
            
            Console.WriteLine($"   Employee: {employee}");
            Console.WriteLine($"   Pattern match result: {description}");
            Console.WriteLine("   ↑ Records work excellently with pattern matching!\n");

            // 7. RECORD STRUCT FEATURES
            Console.WriteLine("7. RECORD STRUCT BENEFITS:");
            var point1 = new PointRecord(3, 4);
            var point2 = point1 with { Y = 5 }; // with expressions work on record structs too!
            
            Console.WriteLine($"   Point 1: {point1}, Distance: {point1.Distance:F2}");
            Console.WriteLine($"   Point 2: {point2}, Distance: {point2.Distance:F2}");
            Console.WriteLine($"   Equality: {point1} == {point2} → {point1 == point2}");
            Console.WriteLine("   ↑ Record structs get automatic ToString, Equals, and with expressions!\n");

            // 8. INHERITANCE (Record classes only)
            Console.WriteLine("8. INHERITANCE WITH RECORDS:");
            Console.WriteLine($"   Employee: {employee}");
            Console.WriteLine($"   Is PersonRecord: {employee is PersonRecord}");
            Console.WriteLine($"   Base properties: {((PersonRecord)employee).FirstName} {((PersonRecord)employee).LastName}");
            Console.WriteLine("   ↑ Record classes support inheritance like regular classes!\n");

            // 9. INIT-ONLY PROPERTIES BY DEFAULT
            Console.WriteLine("9. IMMUTABILITY BY DEFAULT:");
            // person.Age = 50; // Compiler error! Properties are init-only
            Console.WriteLine("   Record properties are init-only by default (immutable after creation)");
            Console.WriteLine("   Classes/structs: mutable by default");
            Console.WriteLine("   Records: immutable by default (safer for data models)\n");

            // 10. SERIALIZATION BENEFITS
            Console.WriteLine("10. SERIALIZATION BENEFITS:");
            var json = JsonSerializer.Serialize(person);
            var deserializedPerson = JsonSerializer.Deserialize(json);
            
            Console.WriteLine($"    JSON: {json}");
            Console.WriteLine($"    Deserialized: {deserializedPerson}");
            Console.WriteLine($"    Are equal: {person == deserializedPerson}");
            Console.WriteLine("    ↑ Records work great with serialization due to immutability!\n");

            Console.WriteLine("=== SUMMARY: RECORD-EXCLUSIVE FEATURES ===");
            Console.WriteLine("✅ Primary constructor with automatic properties");
            Console.WriteLine("✅ Automatic ToString() with formatted output");
            Console.WriteLine("✅ 'with' expressions for non-destructive mutation");
            Console.WriteLine("✅ Automatic Deconstruct method generation");
            Console.WriteLine("✅ Value-based equality by default");
            Console.WriteLine("✅ Enhanced pattern matching support");
            Console.WriteLine("✅ Immutable by default (init-only properties)");
            Console.WriteLine("✅ Built-in IEquatable implementation");
            Console.WriteLine("✅ Optimized GetHashCode() generation");
            Console.WriteLine("✅ Perfect for data models and DTOs");
        }
    }
}

Conclusion

Understanding C# types and pattern matching opens up a world of clean, efficient programming:

  • Classes are your go-to for complex, mutable objects with behavior
  • Records are perfect for immutable data with automatic helpful features
  • Structs are great for small, simple value containers
  • Pattern matching makes complex conditional logic elegant and readable

Start with records for most of your data needs, use classes when you need complex behavior or inheritance, and structs for small value types. As you get more comfortable, experiment with the advanced pattern matching features - they'll make your code much more expressive and maintainable!

Remember: the best way to learn is by coding. Try creating small examples with each type and see how they behave differently. Happy coding!

Comments

Popular posts from this blog

Building a Microservices Order Processing System with RabbitMQ and .NET 9.0

What is the Actor Model? A Complete Guide for C# Developers