Classes vs Structs

Performance Comparison

Due to their memory allocation differences, structs are generally faster than classes. If you’re working with a large amount of data, structs can be more efficient because they don’t require the overhead of heap memory allocation.

However, there are some cases where classes are faster than structs. For example, when copying large objects, classes can be more efficient because they only copy a reference to the object instead of the object itself.

Another advantage of using structs is that they are value types, meaning that they are copied by value rather than by reference. This can be useful in situations where you want to ensure that the original data is not modified by any subsequent operations.

On the other hand, classes are reference types, which means that they are passed by reference. This can be useful in situations where you want to modify the original data without creating a new copy of it.

When to Use Structs

Structs are best used when you need to represent simple data types, such as integers, strings, or other basic data types. They are also useful when you need to work with large datasets, such as arrays or lists, where performance is critical.

You should also use a struct when you need to pass a small amount of data to a method, and you want to avoid the overhead of passing a reference to a class.

Another scenario where structs can be useful is when you need to create a lightweight object that doesn’t require inheritance or polymorphism. Since structs are value types, they can be easily copied and passed around without the need for complex memory management.

When to Use Classes

Classes are best used when you need to represent more complex objects, such as real-world entities like cars, people, or animals. They are also useful when you need to create hierarchies of objects, where one class inherits from another.

You should also use a class when you need to work with large amounts of data, such as when you’re working with a database or other external data source.

Another situation where classes are useful is when you need to encapsulate functionality and data together. By creating a class, you can group related methods and properties into a single unit, making your code more organized and easier to maintain.

Additionally, classes can be used to implement interfaces, which define a set of methods that a class must implement. This allows for greater flexibility and modularity in your code.

Best Practices for Using Structs and Classes

When using structs, it’s important to keep their size small, so they’re stored on the stack instead of the heap. This can help to improve performance and reduce memory usage.

When using classes, it’s a good idea to use inheritance to create hierarchies of objects with shared properties and methods. This can help to reduce code duplication and make your code more maintainable.

Another important consideration when using structs and classes in C# is to use them appropriately based on their intended purpose. Structs are best used for small, simple data structures that don’t require a lot of functionality, while classes are better suited for more complex objects with behavior and functionality.

Common Mistakes to Avoid When Using Structs and Classes

One common mistake when using structs is to make them too large. If your struct is too large, it will be stored on the heap instead of the stack, which can cause performance issues.

Another mistake is to use a class when a struct would be more appropriate. If you’re working with simple data types, using a struct can be more efficient and improve performance.

It’s also important to note that structs are value types, while classes are reference types. This means that when you pass a struct to a method or assign it to a variable, a copy of the struct is created. This can lead to unexpected behavior if you’re not careful.

Additionally, structs cannot inherit from other structs or classes, and they cannot be used as a base for other types. If you need to create a more complex data structure, a class may be a better choice.

Examples of Structs

We just have to look at the .NET Framework types to get some examples of structures.

int, long, byte, bool, float, double, char, decimal, DateTime, TimeSpan are all structures. They are all lightweight types represented with at most 8 bytes. Other lightweight types that would be good candidates for being declared as a structure are:

public readonly struct Point2D {
   public int X { get; init; }
   public int Y { get; init; }
}

public readonly struct Color {
   public byte R { get; init; }
   public byte G { get; init; }
   public byte B { get; init; }
}

public readonly struct Money {
   public decimal Amount { get; init; }
   public string CurrencyCode { get; init; }
}

Notice that all those types are good candidate to be declared as readonly. We say these are immutable structures. Once a value has been created it cannot be modified. Instead a new value can be obtained from existing values like in int i1 = i2 + i3.

Examples of Classes

Classes are well suited to implement complex dataset like this one:

public class Person {
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public int YearOfBirth { get; set; }
   public string Address { get; set; }
   public string Email { get; set; }
}

Here it would make sense to have a class Employee that derives from the Person class, since an employee is-a person.

Classes are also well suited to implement complex behaviors like a BankAccount that can present many operations:

public class BankAccount {
   public string AccountNumber { get; }
   public string AccountHolderName { get; }
   public decimal Balance { get;  }
   public decimal InterestRate { get; }

   // Behaviors
   public void Deposit(decimal amount) {
      // ...
   }
   public void Withdraw(decimal amount) {
      // ...
   }
   public decimal CalculateInterest() {
      // ...
   }
   public void ApplyInterest() {
      // ...
   }
}

Performance comparison

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1415 (21H2)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=6.0.101
  [Host]     : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
  DefaultJob : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT


|       Method |      Mean |    Error |    StdDev |  Gen 0 | Allocated |
|------------- |----------:|---------:|----------:|-------:|----------:|
|  SmallStruct |  39.16 ns | 0.069 ns |  0.061 ns |      - |         - |
| MediumStruct |  39.30 ns | 0.254 ns |  0.238 ns |      - |         - |
|   SmallClass | 215.30 ns | 3.048 ns |  2.994 ns | 0.1433 |   2,400 B |
|  MediumClass | 487.58 ns | 9.713 ns | 15.959 ns | 0.2389 |   4,000 B |

Source: https://github.com/BenjaminAbt/SustainableCode

Learn More