Back to Blog
C#

C# Struct vs Class: Choosing the Right Type

c# struct vs class: Understand the differences between C# structs and classes: value vs reference semantics, memory allocation, performance tradeoffs, and when to choo...

C#structclassvalue typesreference typesperformance
Diagram comparing a struct's value copy on assignment with a class's shared reference, illustrating C# value and reference type behavior.

c# struct vs class requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

The decision between a struct and a class in C# affects memory layout, copy semantics, and runtime performance. Getting it wrong can lead to subtle bugs or avoidable GC pressure. This article explains the core differences and gives practical guidance for choosing the right type for your scenario.

Value Types and Reference Types: The Core Difference

A struct is a value type. When you assign a struct variable to another, the runtime copies the entire data. A class is a reference type. Assigning a class variable copies the reference, not the object itself. This fundamental difference drives everything else.

public struct Point { public int X; public int Y; } public class PointClass { public int X; public int Y; }

Consider this code:

var p1 = new Point { X = 1, Y = 2 }; var p2 = p1; p2.X = 10; var c1 = new PointClass { X = 1, Y = 2 }; var c2 = c1; c2.X = 10;

After execution, p1.X remains 1 because p2 is a separate copy. c1.X becomes 10 because c1 and c2 point to the same object. This is the most important behavior to internalize.

Memory Allocation and Copy Behavior

Structs are typically allocated on the stack or inline within their containing object. Classes are allocated on the managed heap, and the variable holds a reference to that memory. This has direct consequences for memory layout and lifetime.

When you pass a struct to a method, the entire struct is copied unless you use ref or in. For large structs, that copy cost can exceed the cost of passing a reference. Classes always pass a reference (typically a pointer), which is cheap regardless of object size.

public void ProcessPoint(Point p) // copies the struct { // ... } public void ProcessPointClass(PointClass p) // copies the reference { // ... }

If a struct is small (say, 16 bytes or less), the copy is cheap and can even be faster than dereferencing a pointer. If it grows beyond that, the copy overhead becomes significant.

When to Use a Struct: Small, Immutable Data

The .NET design guidelines suggest using a struct when the type is small, short-lived, and immutable. Immutability is key because copying a mutable struct can lead to surprising behavior if the copy is modified independently. Examples include DateTime, Guid, and KeyValuePair<TKey, TValue>.

A good struct candidate:

public readonly struct Money { public decimal Amount { get; } public string Currency { get; } public Money(decimal amount, string currency) { Amount = amount; Currency = currency; } }

This struct is immutable, small (though decimal and string make it larger than 16 bytes), and represents a single value. It benefits from value semantics: two Money instances with the same amount and currency are equal by value, which is often desirable.

When to Use a Class: Larger Objects and Shared State

Classes are the default choice for most types. They support inheritance, allow null references, and are more flexible for objects that change state over time. If your type is large, mutable, or needs to be shared across multiple references, a class is almost always the right choice.

public class Customer { public string Name { get; set; } public string Email { get; set; } public List<Order> Orders { get; } = new(); }

Classes also work well when you need to pass the same object to multiple methods and have changes reflected everywhere. That shared identity is essential for many domain models.

Performance Considerations: Allocation, Copying, and GC Pressure

Performance is often the deciding factor. Structs can reduce heap allocations and GC pressure because they are stored inline or on the stack. Classes require heap allocation and eventual garbage collection. However, copying a large struct can be more expensive than allocating a small class object.

Consider a list of points. If Point is a struct, List<Point> stores the data contiguously in a single array. If it were a class, each element would be a separate heap object, and the list would store references. The struct version has better cache locality and fewer allocations.

var points = new List<Point>(); // struct: contiguous memory var pointClasses = new List<PointClass>(); // class: array of references to heap objects

For small, frequently created values, structs can dramatically reduce GC pressure. For large objects that are created rarely, classes are fine. The not measure without profiling, but the mechanism is clear: structs avoid heap allocation, but copying them has a cost.

Another performance trap is boxing. When you cast a struct to object or an interface, the runtime boxes it, creating a heap object. This defeats the purpose of using a struct. Avoid boxing in performance-sensitive paths.

Point p = new Point { X = 1, Y = 2 }; object box = p; // boxes the struct

Common Pitfalls with Structs: Copying, Boxing, and Mutability

Mutable structs are a common source of bugs. If you have a mutable struct and store it in a collection, modifying a copy does not affect the original. This often surprises developers.

public struct MutablePoint { public int X; public int Y; } var list = new List<MutablePoint>(); list.Add(new MutablePoint { X = 1, Y = 2 }); list[0].X = 10; // This works because list indexer returns a reference? Actually, it returns a copy.

In C#, the indexer of List<T> returns the element by value for structs. So list[0].X = 10 compiles but modifies a temporary copy, not the stored element. This is a classic compile-time error that causes silent bugs. To modify a struct in a list, you must use list[0] = new MutablePoint(...) or use an array.

Another pitfall is default equality. Structs use value-based equality by default, but this requires reflection for fields. Overriding Equals and GetHashCode is recommended for performance and correctness. Classes use reference equality unless overridden.

Decision Guidance: Struct or Class for Your Scenario

Use a struct when:

  • The type is small (typically 16 bytes or less).
  • It is immutable or you can make it readonly.
  • Instances are short-lived or created frequently.
  • You want value semantics, where equality is based on data.
  • You need to avoid heap allocation and GC pressure in a hot path.

Use a class when:

  • The type is large or contains many fields.
  • The type is mutable and needs to be shared across references.
  • You need inheritance or polymorphism.
  • You want to allow null references.
  • The object lifetime is long or unpredictable.

These criteria are not absolute. For example, a decimal is 16 bytes and is a struct. A string is a class because it is a variable-length reference type. The The final decision often depends on profiling and specific usage patterns.

Advanced Struct Features: ref struct and readonly struct

Modern C# offers ref struct and readonly struct modifiers. A ref struct can only live on the stack, which prevents boxing and ensures no heap allocation. This is used for types like Span<T> and ReadOnlySpan<T>. A readonly struct enforces immutability at the compiler level, preventing accidental modification of fields.

public readonly struct Temperature { public double Celsius { get; } public Temperature(double celsius) { Celsius = celsius; } }

Combining ref and readonly gives you a stack-only, immutable type that is very efficient for high-performance scenarios. However, these types cannot be used in async methods or as fields in classes, which limits their applicability.

Understanding these advanced features helps you push struct usage further when needed, but they come with constraints. Always verify that the constraints are acceptable before adopting them.

Struct and Class Interchangeability: When It Matters

Sometimes you can change a struct to a class without breaking the API, but the semantics change. Code that relies on value equality will break. Code that passes the type as a parameter will start sharing state instead of copying. This is a breaking change for callers.

If you are designing a public API, decide early whether a type should be a struct or a class. Changing later is difficult because the behavior change is subtle. For internal types, you have more freedom, but you should still document the intended semantics.

The choice between c# struct vs class is not about one being better than the other. It is about matching the type's behavior to the problem. Value semantics and low allocation cost make structs attractive for small, immutable data. Reference semantics and flexibility make classes the default for most domain objects. Evaluate your type's size, mutability, and usage frequency, and you will make the right call.

c# struct vs class: Practical Usage and Code Examples | RYUSLOG DEV