NDepend Blog

Improve your .NET code quality with NDepend

Class vs Struct in C#: Making Informed Choices

April 15, 2024 9 minutes read

C# Class vs Struct

In C# programming, choosing the right data type between classes and structs is a crucial decision that impacts application performance and design. This article presents a concise comparison of Class vs Struct in C# to help you make informed choices for your projects.

Let’s dive in to understand the differences and find the best data type for each specific scenarios.

Fundamental difference: Reference Types vs. Value Types

Structs are lighter than Classes. The difference is that classes are reference types while structs are value types. This drastically impacts their memory allocation:

  • Instances of a class are objects with their data allocated on the heap. The heap is a large region of memory used for dynamic memory allocation, allowing objects to have a more extended lifetime and flexible size. The heap size is only bounded by the current process available memory. The heap can contain Gigabytes of class instances.
  • While instances of structures are values, residing directly on the current thread’s stack. The stack is a region of memory dedicated to the current thread. It is used for storing local variables and function call information. It operates following a Last-In-First-Out (LIFO) approach. In .NET the size of the thread stack size is typically 1MB.

Let’s note that there are some exceptions where structs may not reside on the stack, which we’ll explain in detail later.

The second major difference is that classes’ objects are passed by reference when calling a function while structs’ values are copied on the thread’s stack instead. This is explained in details in the next section.

Objects vs. Values

An instance of a class is named an object while an instance of a struct is named a value.

Objects

Reference Semantics:

  • When an object is created, it resides on the heap, and a reference (memory address) to that object is stored on the stack or within other objects.
  • Multiple references can point to the same object, allowing for data sharing and manipulation across different parts of the program and even different threads.
  • Modifying an object’s data through one reference affects all other references pointing to the same object, as they all refer to the same memory location.

Objects’ Lifetime and Garbage Collection:

  • Objects typically have a more extended lifetime and persist in memory until they are no longer reachable, at which point they become eligible for garbage collection. An object is no longer reachable when no more references point to that object.
  • Garbage collection is a process that automatically reclaims memory occupied by objects that are no longer in use, ensuring efficient memory utilization. The heart of the .NET runtime – the Common Language Runtime (CLR) – houses the Garbage Collector (GC). The GC is a complex beast. Fortunately, you only have to worry about it for critical performance scenarios.

Here is a code example that illustrates the lifetime of an object:

Values

Value Semantics:

  • Values, as instances of structs, adhere to value semantics.
  • When a value is created, it directly resides in the memory location where it is declared or on the thread stack.
  • Unlike objects, values are not shared or referenced by other variables; they exist independently, and each variable holds its own copy of the value.
  • Modifying a value through one variable does not affect other variables that hold copies of the same value.

Lifetime and Stack Management:

  • Values have a limited lifetime, primarily determined by their scope within the program.
  • When the variable representing a value goes out of scope – this typically occurs when a method returns – the value is automatically deallocated from the stack.
  • As a result, values have a shorter lifespan compared to objects residing in the heap.

Here is a code example that illustrates the lifetime of a value:

Class vs Struct: Performance Considerations

Garbage Collection necessarily has an impact on performance:

  • All references to all objects must be tracked by the GC. This is required to determine when an object no longer has any references.
  • Periodically, the GC must look for objects no longer reachable to remove them.
  • When some objects have been removed the GC sometimes compacts the memory, which means that some reachable objects are moved to different memory locations to avoid memory fragmentation and ensure efficient memory utilization.

Due to the absence of GC impact with structures, it is often considered that using structures can lead to the creation of faster code. This is often true, except when the size of a structure becomes too large (like more than 16 bytes footprint for its values). When passing a large value of a struct to a function, it is more costly than just passing a 4 bytes reference to an object. There are 2 solutions to this issue:

  • Passing by reference a value with the keyword ref:

  • Passing by reference the instance of a structure declared with the keyword readonly.

Class vs Struct: Other Considerations

Inheritance:

Classes have support inheritance while structures don’t.

Interface:

Both classes and structures can implement an interface. However, it is advised not to implement an interface with a structure. This is because interfaces have reference semantics, and when a value type (structure) is downcasted to the interface type, it undergoes a process called boxing . Boxing involves converting the value type to an object on the heap, and this additional operation introduces performance overhead and memory allocation, counteracting the benefits of using a structure for its value semantics. Boxing is illustrated by this code sample:

System.Object Inheritance:

In .NET all types derive from the System.Object class. We wrote earlier that structures don’t support inheritance but the truth is that any structure implicitly derives from the Object class. This means that any structure value can be boxed to a reference typed as Object.

Structure living on the heap:

At the beginning, we mentioned that structures’ instances are not always stored on the current thread stack. We just saw that boxing forces a structure to live within an object on the heap. The same situation occurs when a field of a class is typed with a structure:

Comparison:

When comparing two references, true is returned if the two references point to the same object. When comparing two values, true is returned if the state of the values are equals.

Nullability:

A reference can be null while a value cannot. Instead, a not initialized value is made of bytes initialized to 0.

Notice the C# syntax with ? to allow dealing with nullable values:

Collections:

The .NET runtime is specially optimized for collections of structure values. For example, int is a structure with 4 bytes footprint and an array of int of length 10 is a single object with a footprint of 40 bytes. Integer values are stored in this 40-byte memory range contiguously. If Person is a class, an array of 10 persons with no null value, contains 10 references of 4 bytes each, referencing the 10 person’s objects.


Note: Choosing between a class and a struct is a significant structural decision that isn’t easily reversible. Converting a class to a struct or vice versa later may lead to complex refactoring, as nullability, comparison, and argument passing behave differently between the two data types.

Examples of Structs in C#

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

intlongbytebool, 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:

Notice that all those types are good candidates 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.

Let’s note that enumeration are value types. There are structures with a single field.

Examples of Classes in C#

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

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:

Edge case: The System.String class is very special in the sense that it has value based comparison and is immutable, yet it is declared a class. In fact String is so special that the runtime reserves a special heap to store its instances.

Guidelines for Using Classes and Structs in C#

In C#, choosing between classes and structs is a crucial decision that affects code design, memory management, and overall program performance. We can now compile guidelines to choose which one to choose:

1. Consider Performance Implications:

  • Evaluate the performance requirements of your application and identify critical performance scenarios.
  • Use structs in performance-critical sections of code where stack allocation and value semantics can provide efficiency gains.

2. Use Classes for Complex Objects:

  • Classes are suitable for modeling complex objects that require reference semantics, shared state, and a longer lifespan.
  • When dealing with entities that involve business logic, encapsulation, and inheritance, classes are the preferred choice.

3. Prefer Structs for Lightweight Data:

  • Structs are more efficient for representing small, simple data that doesn’t require shared references or frequent modification.
  • Use structs for lightweight entities like coordinates, dates, currencies, or simple aggregates.

4. Avoid Mutable Structs:

  • Structs should generally be immutable to ensure predictable behavior and prevent potential issues with copying and sharing mutable data.
  • If a mutable behavior is necessary, consider using classes or implement carefully designed methods to avoid unexpected behavior.

5. Be Mindful of Copying Overhead:

  • When passing or assigning structs, keep in mind that the entire struct is copied. For large structs, this copying process can introduce performance overhead.
  • Consider using ref modifiers to pass structs by reference when necessary, to minimize copying overhead.

6. Use Interfaces with Classes, Not Structs:

  • While both classes and structs can implement interfaces, prefer implementing interfaces with classes.
  • When a struct is cast to an interface type, it gets boxed, leading to additional performance overhead and memory allocation.

7. Favor Classes for Reference Types:

  • When dealing with nullable types, events, or objects requiring dynamic memory allocation, use classes to leverage reference semantics and the garbage collector.

8. Avoid Excessive Nesting of Structs:

  • Be cautious with deep nesting of structs, as it can lead to increased memory usage and potential copying overhead due to structs being value types.

By following these guidelines, you can make informed decisions about when to use classes and structs in your C# codebase, ultimately leading to well-structured, efficient, and high-performing applications.

Conclusion

Understanding the differences between structs and classes in C# is vital for making informed decisions in your code. By leveraging their unique characteristics, you can optimize performance and maintainability effectively. These decisions play a pivotal role in optimizing your code for enhanced performance, as well as ensuring its long-term maintainability. Embrace the power of these fundamental concepts, and leverage their strengths to craft robust and efficient C# applications.


C# is a versatile language that has evolved significantly in recent years. The C# team worked hard to promote managed pointers to classes and structures through extended usages of the ref keyword, that we just saw in this article to pass a value by-reference. You can read it all in this article :Managed pointers, Span, ref struct, C#11 ref fields and the scoped keyword.

Comments:

  1. Great article. You should write one on Task and ValueTask as well!

  2. An insightful breakdown of the differences between classes and structs in C#. Your article makes it easier for developers to choose the right data type for their specific needs. Well done!

Comments are closed.