Back to Blog
C#

C# Record vs Struct: Choosing the Right Type

c# record vs struct: Compare C# records and structs to decide which fits your data modeling, equality, immutability, and performance needs.

C# recordsC# structsvalue typesreference typesimmutabilityequality semantics
Diagram comparing C# record and struct types, showing value vs reference behavior and equality semantics.

When you need to model data in C#, both records and structs offer value-like behavior, but they differ in reference semantics, equality, and usage patterns. The choice between c# record vs struct affects how your data is copied, compared, and stored in memory. Understanding these differences helps you avoid subtle bugs and performance issues in production code.

Understanding Value Types and Reference Types in C#

Structs are value types. When you assign a struct to another variable, the runtime copies the entire data. Records, on the other hand, are reference types by default, even though they provide value-based equality. This distinction is fundamental because it influences how your data behaves when passed to methods, stored in collections, or returned from functions.

public struct PointStruct { public int X { get; set; } public int Y { get; set; } } public record PointRecord(int X, int Y);

When you write var p1 = new PointStruct { X = 1, Y = 2 }; var p2 = p1; p2.X = 5;, the struct p1 remains unchanged because p2 gets a copy. With a record, p2 would reference the same object unless you use a with expression to create a new instance. This behavior matters when you pass data across method boundaries and expect the original to remain intact.

Syntax Differences Between Record and Struct

Records can be declared with positional parameters, which automatically generate properties, a constructor, and deconstruction support. Structs require explicit property definitions and constructors, though C# 10 introduced record structs that combine positional syntax with value-type semantics.

public record struct Temperature(double Celsius); public struct TemperatureStruct { public double Celsius { get; set; } public TemperatureStruct(double celsius) => Celsius = celsius; }

Both types support with expressions, but for a regular struct you need to enable with support manually if you want non-destructive mutation. Record structs provide it out of the box. The syntax difference is small, but the underlying behavior differs significantly.

Equality Semantics: Value-Based vs Reference-Based

Records implement value equality by default. Two record instances are equal if all their properties have equal values. Structs also use value equality, but only if you implement IEquatable<T> and override Equals and GetHashCode; otherwise, they fall back to reflection-based comparison, which is slow and error-prone.

var r1 = new PointRecord(1, 2); var r2 = new PointRecord(1, 2); Console.WriteLine(r1 == r2); // True var s1 = new PointStruct { X = 1, Y = 2 }; var s2 = new PointStruct { X = 1, Y = 2 }; Console.WriteLine(s1.Equals(s2)); // False without custom implementation

For structs, the default Equals uses reflection, which is both slow and inconsistent. Records generate a proper implementation for you, which is one of the main reasons developers choose records over structs for data models that require comparison.

Immutability and with Expressions

Records are designed for immutability. Positional records generate init-only properties, meaning you cannot modify them after creation. To change a value, you use the with expression to create a copy with modified properties. Structs, by default, are mutable unless you declare properties as readonly or use readonly struct.

var original = new PointRecord(1, 2); var modified = original with { Y = 5 }; // original remains (1, 2), modified is (1, 5)

For structs, you would need to manually copy and assign. This makes records more convenient when you want to enforce immutability without writing boilerplate. However, if you need a mutable value type that is frequently updated in place, a struct may be more appropriate because it avoids allocating new objects on the heap.

Performance and Memory Considerations

Structs are allocated on the stack when they are local variables, and they are stored inline in arrays and other structures, which improves cache locality and reduces garbage collection pressure. Records are reference types allocated on the heap, so they incur allocation overhead and are collected by the GC.

For high-performance scenarios, such as game physics or frequent data processing loops, structs can be significantly faster because they do not require heap allocation. However, passing structs around by value copies the entire data, which can be expensive if the struct is large. Records, being references, only copy the reference, but each new instance adds heap pressure.

// Struct array: contiguous memory, fast iteration PointStruct[] points = new PointStruct[1000]; // Record array: each element is a reference to a heap object PointRecord[] records = new PointRecord[1000];

There is no universal winner. The right choice depends on the size of the data, the frequency of copying, and the lifetime of the objects. If you are working with small, short-lived data, structs often win. If you need value equality and immutability across larger data sets, records provide a better balance.

Choosing Between Record and Struct in Real Scenarios

Use a record when you need value-based equality, immutable data, and a concise syntax for data transfer objects, DTOs, or domain models that are compared by value. Use a struct when you have small, frequently created values that should not cause heap allocations, such as coordinates, ranges, or numeric identifiers.

Consider the following decision criteria:

  • If you need with expressions and automatic equality, choose a record.
  • If you need to avoid heap allocation and have a small data size (typically under 16 bytes), choose a struct.
  • If you need mutable value semantics, a struct is more natural, but you must implement equality carefully.
  • If you are building an API that exposes data to consumers, records make the contract clearer because they enforce immutability.

Record structs combine both worlds: value-type storage with record-like syntax and equality. They are useful when you want the performance of a struct but the convenience of a record. However, they still require careful handling when stored in collections because they are copied on access.

Common Pitfalls and Edge Cases

One common mistake is assuming that records are value types. They are not; they are reference types with value semantics. This means that storing a record in a collection and modifying it through a reference can lead to unexpected behavior if you are not careful. For example, if you have a record property that is mutable, the record's equality can change after the object is created, which breaks dictionary keys.

public record MutableRecord { public List<int> Numbers { get; init; } = new(); } var rec = new MutableRecord(); rec.Numbers.Add(1); // This changes the record's hash code

To avoid this, keep record properties immutable or use collections that are immutable. Another edge case is the default equality of structs. If you do not override Equals, you get reflection-based comparison, which is slow and can produce false negatives if the struct contains reference-type fields that are compared by reference instead of value.

When you implement IEquatable<T> for a struct, you must also override GetHashCode consistently. Records handle this automatically, but structs require manual work. This is a common source of bugs in code that uses structs as dictionary keys.

Finally, consider the impact of boxing. When you pass a struct to a method that expects an interface or object, it gets boxed, which allocates on the heap. Records do not have this issue because they are already reference types. Boxing can degrade performance in hot paths, so be mindful of where structs are used in generic collections or with interfaces.

Understanding the tradeoffs between records and structs allows you to make an informed decision based on your specific requirements. The choice is not about which is better overall, but which fits the data model, performance constraints, and maintainability needs of your application.

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