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
Post a Comment