Classes, structs, and records are fundamental concepts in C# programming. Each is a different kind of type, with different features, capabilities, and limitations. To make matters more confusing, they have features and characteristics in common.
Classes are reference types that provide support for useful object-oriented concepts such as encapsulation, inheritance, and polymorphism. Structs are value types that offer better performance but have limitations in terms of size and mutability. Records, which were introduced in C# 9, combine the best of both classes and structs, with support for immutability by default.
When should you use structs or records instead of classes in your application? In this article we will examine the differences between classes, structs, and record types and how we should work with these different types in C#.
Create a console application project in Visual Studio
First off, let’s create a .NET Core console application project in Visual Studio. Assuming Visual Studio 2022 is installed in your system, follow the steps outlined below to create a new .NET Core console application project.
- Launch the Visual Studio IDE.
- Click on “Create new project.”
- In the “Create new project” window, select “Console App (.NET Core)” from the list of templates displayed.
- Click Next.
- In the “Configure your new project” window, specify the name and location for the new project.
- Click Next.
- In the “Additional information” window shown next, choose “.NET 8.0 (Long Term Support)” as the framework version. Leave the “Do not use top-level statements” and “Enable native AOT publish” check boxes unchecked. We won’t be using those features here.
- Click Create.
We’ll use this .NET 8 console application project to work with examples of classes, structs, and records in the subsequent sections of this article.
Using classes in C#
A class in C# is a reference type. In other words, a variable of a class type holds a reference to an object. Note that you can have multiple references that point to the same object (so modifying the object through one reference will change its value for others). The members of a class (i.e., its fields, properties, methods, events, etc.) define the behavior and state of the instances of the class.
Classes in C# support abstraction, encapsulation, inheritance, and polymorphism. These are the four basic principles of object-oriented programming.
The following code snippet shows the syntax for defining a class in C#.
<modifiers> class <name of the class> { //Data members and member functions }
The following code snippet illustrates a typical C# class.
public class Author { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Address { get; set; } public string Email { get; set; } public string Phone { get; set; } }
The code snippet below shows how you can instantiate the Author class and set values to each of its properties.
var author = new Author() { Id = 1, FirstName = "Joydip", LastName = "Kanjilal", Address = "Hyderabad, Imdia", Email = "joydipkanjilal@yahoo.com", Phone = "1234567890" };
Using structs in C#
A struct in C# is a value type. A variable of a struct type holds an instance of the type (not a reference). You can use structs in C# to build small composite data types while avoiding the garbage collection overhead. Structs can also be made immutable, using the readonly modifier.
Note that instances of structs are passed by value when you use them as a method parameter. Likewise, when you assign one struct variable to another struct variable, its value is copied.
While you can use fields, properties, and methods in a struct, you cannot implement object-oriented concepts such as abstraction, encapsulation, inheritance, and polymorphism using a struct.
The following code snippet shows the syntax for defining a struct in C#.
<modifiers> struct <name of the struct> { //Data members and member functions }
The following code snippet illustrates a typical struct.
struct Coordinate { public int x; public int y; }
You can now use the following piece of code to create an instance of this struct and initialize its data members.
Coordinate point = new Coordinate(); point.x = 10; point.y = 20;
Using records in C#
Record types were introduced in C# 9 for representing immutable values. Records are reference types like classes, but they have value-based equality semantics by default, like structs. You can use record types in C# to build immutable types and thread-safe objects.
Records provide handy built-in features such as a with expression to create a new record with modified data, and a init accessor to set values to its properties only at the time of initialization. Record types can have properties, fields, methods, events, and constructors, and they provide limited support for inheritance. However, they do not support abstraction, encapsulation, and polymorphism.
You can take advantage of record types to represent data transfer objects (DTOs), as well as other data structures that require immutability and equality semantics.
Consider the following code that shows a C# class named Rectangle.
public class Rectangle { public int Length { get; set; } public int Breadth { get; set; } }
You can represent the same data using a record type in a much simpler way:
public record Rectangle(int Length, int Breadth);
You can now create an instance of the Rectangle record type using the following code.
var rectangle = new Rectangle(10, 5);
Using inheritance in record types in C#
A record type in C# can inherit from another record type, but it cannot inherit from a class. The following code snippet shows how one record type can extend another record type in C#.
public record Person(string FirstName, string LastName, string Address) { } public record Author(int id, string LastBookAuthored, string FirstName, string LastName, string Address) : Person(FirstName, LastName, Address) { public int Id { get; set; } public string LastBookAuthored { get; set; } }
You can now create an instance of the record type using the following code.
var author = new Author(1, "Mastering C# 8.0", "Joydip", "Kanjilal", "Hyderabad, India");
Classes vs. structs vs. records in C#
Use classes to represent complex logic and behavior in your application. Classes were designed to model complex data structures that require object-oriented concepts such as abstraction, encapsulation, composition, inheritance, and polymorphism. However, classes have certain performance drawbacks that you should keep in mind when designing your applications.
Value types (structs) are much more cost-effective in terms of memory allocation and deallocation than reference types (classes and records). When you want to create a composite data type with only a few data members, a struct is a good choice. Structs are ideal for small data structures (less than 16 bytes in size) that require value semantics. Using a struct in such cases will help you avoid garbage collection costs and related overheads.
Record types fill a gap between reference types and value types, and help you to write code that is clean, lean, and readable. Choose a record type over a class or a struct when data is your primary concern. Use record types to create data transfer objects, API response objects, configuration objects, immutable models, and value objects in domain-driven design.
Record types provide excellent support for pattern matching, making them a good choice for working with complex data structures. Record types are designed to be immutable data types by default—a feature that facilitates functional programming, where you cannot modify an instance once it has been created.
When deciding whether to use classes, structs, or records in C#, consider their intended usage, features, equality comparison, immutability, and performance characteristics. In a nutshell, classes are suitable when you need objects with behavior and complex logic, structs are suitable for lightweight values and minimal behavior, and records are ideal for immutable data structures with straightforward equality rules.