Choosing Between Classes, Structs, and Records in C#

By FoxLearn 1/2/2025 8:45:29 AM   134
In this post, we will learn how to use classes, structs, and record types in C#.

Classes, structs, and records are fundamental C# types with distinct features.

  • Classes are reference types in C# that support key object-oriented concepts, including encapsulation, inheritance, and polymorphism.
  • Structs are value types that offer better performance but have limitations in size and mutability.
  • Records, introduced in C# 9, combine the advantages of both classes and structs, offering immutability by default.

Using classes in C#

In C#, a class is a reference type, meaning a variable of a class type holds a reference to an object. Multiple references can point to the same object, and modifying the object through one reference will affect the others. A class's members such as fields, properties, methods, and events define the object's behavior and state.

The syntax for defining a class in C# is as follows:

<modifiers> class <className>
{
  // Data members and member functions
}

Here's an example of a class definition for a Product:

public class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }
}

You can instantiate the Product class and set its properties like this:

var product = new Product()
{
    ProductId = 101, 
    Name = "Laptop", 
    Price = 1200.50, 
    Category = "Electronics"
};

In this example, the Product class represents an item with properties like ProductId, Name, Price, and Category. An instance of the Product class is created with specific values for these properties.

Using structs in C#

In C#, a struct is a value type, meaning a variable of a struct type holds an actual instance of the type, not a reference. They can also be made immutable by using the readonly modifier. When structs are used as method parameters or assigned to another variable, they are passed by value, meaning a copy of the struct is made.

Unlike classes, structs do not support object-oriented principles like abstraction, encapsulation, inheritance, or polymorphism.

The syntax for defining a struct in C#:

<modifiers> struct <structName>
{
    // Data members and methods
}

Here’s an example of a Rectangle struct:

struct Rectangle
{
    public int width;
    public int height;

    // Method to calculate area
    public int GetArea()
    {
        return width * height;
    }
}

You can create and initialize an instance of the Rectangle struct and calculate its area like this:

Rectangle rect = new Rectangle();
rect.width = 10;
rect.height = 5;

int area = rect.GetArea();
Console.WriteLine($"Area of the rectangle: {area}");

In this example, the Rectangle struct defines a simple geometric shape with width and height, and a method to calculate its area. The struct is instantiated, and its properties are set to calculate and print the area.

Using records in C#

Record types in C# 9 were introduced to represent immutable values. While they are reference types like classes, they behave similarly to structs by providing value-based equality by default.

Some key features of record types include:

  • Immutability: You can set properties only during initialization using the init accessor.
  • With expressions: To create a new record with modified data.
  • Limited inheritance support: Records support inheritance, but do not support all object-oriented features like abstraction and polymorphism.

Records are commonly used for representing data transfer objects (DTOs) and other structures that require immutability and equality semantics.

Here’s a typical class definition for a Book:

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
    public int YearPublished { get; set; }
}

The same Book type can be represented as a record type like this:

public record Book(string Title, string Author, int YearPublished);

You can create an instance of the Book record type as follows:

var book = new Book("C# Programming", "John Doe", 2021);

With a record, the Book object is immutable, meaning you can’t change the Title, Author, or YearPublished properties once it has been initialized. You can also create a modified copy of the record using the with expression:

var updatedBook = book with { YearPublished = 2022 };

This example shows how record types simplify the creation of immutable data structures, and the value-based equality makes it easy to compare objects based on their data rather than their references.

Using inheritance in record types in C#

In C#, record types can inherit from other record types, but they cannot inherit from classes. This allows for creating a hierarchy of record types where a child record type can extend a parent record type and inherit its properties. However, records cannot extend classes, as they are designed to maintain value-based equality and immutability.

Consider the following example where a Vehicle record is extended by a Car record type:

public record Vehicle(string Make, string Model, int Year)
{
}

public record Car(string Color, int Doors, string Make, string Model, int Year) :
    Vehicle(Make, Model, Year)
{
    public string Color { get; set; }
    public int Doors { get; set; }
}

You can create an instance of the Car record like this:

var myCar = new Car("Red", 4, "Toyota", "Corolla", 2022);

In this example:

  • The Car record inherits from the Vehicle record, allowing it to reuse the Make, Model, and Year properties.
  • The Car record adds its own properties such as Color and Doors.
  • This showcases how inheritance in record types allows for extending shared properties while keeping immutability and value-based equality semantics.

Classes vs structs vs records in C#

Use classes in C# to model complex data structures and implement object-oriented principles like abstraction, encapsulation, inheritance, and polymorphism. However, it's important to consider their performance drawbacks, as classes are reference types and may incur additional memory overhead and garbage collection costs.

Structs, as value types, are more memory-efficient than reference types (classes and records) because they avoid the overhead of garbage collection. They are ideal for creating small composite data types with few members (typically less than 16 bytes) that require value semantics. Using a struct helps avoid garbage collection costs and related overhead.

Record types bridge the gap between reference and value types, promoting clean and readable code. They are ideal for data-focused tasks, such as creating data transfer objects, API responses, configuration objects, immutable models, and value objects in domain-driven design. Record types excel in pattern matching and are ideal for complex data structures. They are immutable by default, supporting functional programming by preventing modifications after creation.

When choosing between classes, structs, and records in C#, consider their usage, features, equality, immutability, and performance. Classes are best for objects with behavior and complex logic, structs are ideal for lightweight values with minimal behavior, and records are perfect for immutable data structures with simple equality rules.