Back to Blog
C#

C# Record vs Struct: Choosing the Right Type

Learn the practical differences between C# records and structs: reference vs value semantics, equality, immutability, performance, and when to choose each.

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#, records and structs are both useful for value-like data, but they behave differently. A record class is a reference type with synthesized value semantics; a struct is a value type that is copied by value. The choice affects how instances are 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 value. Records are reference types by default, even though they provide value-based equality. This distinction matters when data is 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;, p1 remains unchanged because p2 gets a copy. With a record, assignment makes both variables refer to the same object; use a with expression to create a new instance with changed values. This behavior matters when you pass data across method boundaries and expect the original to remain intact.

Syntax Differences Between Record and Struct

Records and record structs can be declared with positional parameters, which automatically generate properties, a constructor, and deconstruction support. Ordinary structs require explicit property definitions and constructors. 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; }

Record structs provide with expressions out of the box, and C# 10 and later also support with on ordinary structs. The syntax difference is small, but the underlying behavior differs: records are reference types, while structs are value types.

Equality Semantics: Value-Based vs Reference-Based

Records implement value equality by default. The compiler synthesizes Equals, GetHashCode, and the == and != operators based on the record's properties. Structs use the default ValueType.Equals implementation unless you override it, which compares fields by reflection. That default can work for simple structs, but it is slow and unreliable for structs that contain reference-type fields.

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)); // True here, but default ValueType.Equals can be slow and can compare reference fields by reference

For structs that must support reliable value equality, implement IEquatable<T> and override Equals and GetHashCode. Records generate a proper implementation for you, which makes them easier to use for value-compared data models.

Immutability and with Expressions

Records are designed to support immutable data. Positional records generate init-only properties, so you cannot modify them after creation. To change a value, you use a with expression to create a copy with modified properties. Structs are mutable by default 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)

With C# 10+, with expressions on ordinary structs also produce a modified copy. If you need a mutable value type that is frequently updated in place, a struct may be more appropriate because repeated updates do not allocate a new heap object.

Performance and Memory Considerations

Structs are often allocated on the stack for local variables and are stored inline in arrays and other structures, which improves cache locality and reduces garbage collection pressure. Record classes are reference types allocated on the heap, so new instances incur allocation overhead and are eventually collected by the GC.

Passing structs by value copies the entire data, which can be expensive if the struct is large. Records, being references, copy only 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. Small, short-lived values often work well as structs. When 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 concise syntax for DTOs or domain values. Use a struct when you have small, frequently created values that should not cause heap allocations, such as coordinates, ranges, or numeric identifiers.

Consider these decision criteria:

  • If you want synthesized equality and non-destructive updates via with, choose a record or record struct.
  • If you need to avoid heap allocation and the struct is small, choose a struct, but override equality carefully.
  • If you need mutable value semantics, a struct is more natural, but you must implement equality carefully.
  • If you are exposing data through an API, records can make the contract clearer because positional records are immutable.

Record structs combine value-type storage with record-like syntax and equality. They are useful when you want the performance characteristics of a struct but the convenience of a record. However, record structs are still copied on assignment, so keep that in mind when storing or retrieving them.

Common Pitfalls and Edge Cases

One common mistake is assuming that records are value types. They are not; a record class is a reference type with value-based equality. Also, a record can be mutable if it defines settable properties. Mutating a record used as a dictionary key changes its hash code and can break lookups.

public record MutableRecord { public int Value { get; set; } } var rec = new MutableRecord { Value = 1 }; var lookup = new Dictionary<MutableRecord, string> { [rec] = "one" }; rec.Value = 2; Console.WriteLine(lookup.ContainsKey(rec)); // False

If you must use a record as a dictionary key, keep its properties immutable.

Another edge case is default struct equality. The default ValueType.Equals implementation uses reflection and can compare reference-type fields by reference instead of by value. When you implement IEquatable<T> for a struct, you must also override GetHashCode consistently. This is especially important when structs are used as dictionary keys.

Finally, consider boxing. When you pass a struct to a parameter typed as object or as an interface it implements, the struct is boxed and allocated on the heap. Generic collections such as List<MyStruct> avoid boxing because the struct is the type argument. Records do not have this issue because record classes are already reference types.

Understanding the tradeoffs between records and structs lets you choose based on the data model, performance constraints, and maintainability needs of your application. The choice is not about which is better overall, but which fits your specific scenario.

C# Record vs Struct: Practical Usage and Code Examples | RYUSLOG DEV