Back to Blog
C#

C# record struct: Value Semantics with Record Features

Record structs combine C# value-type semantics with record syntax. See how positional declarations, value equality, mutation, readonly variants, and performance tradeoffs work, and when to choose one over a record class or plain struct.

record structC# 10value typesvalue equalityimmutabilitystruct
Diagram showing a record struct value being copied between two variables, illustrating value semantics.

A record struct in C# combines record syntax with value-type semantics. Introduced in C# 10, it gives you positional parameters, value equality, and a generated ToString implementation without a separate heap allocation for the record value. Unlike a positional record class, a record struct is mutable by default, so decide whether you want mutable value semantics or the readonly variant described below.

Declaring a record struct

The simplest declaration uses positional parameters:

public record struct Point(double X, double Y);

This single line generates:

  • Two public properties, X and Y
  • A value-based Equals and GetHashCode
  • Overloaded == and != operators
  • A ToString that prints Point { X = 1, Y = 2 }
  • Deconstruction support via var (x, y) = point

You can also use the full property syntax when you need additional members:

public record struct Temperature { public double Celsius { get; set; } public double Fahrenheit => Celsius * 9 / 5 + 32; }

Unlike a positional record class, the properties in a positional record struct are mutable by default: the generated accessors use set, not init. If you want immutability, declare the struct as readonly (covered below).

Value equality and hashing

Record structs override Equals(object), implement IEquatable<T>, and provide GetHashCode and the ==/!= operators. Two record structs are equal when all their fields are equal:

var a = new Point(1, 2); var b = new Point(1, 2); Console.WriteLine(a == b); // True Console.WriteLine(a.Equals(b)); // True Console.WriteLine(a.GetHashCode() == b.GetHashCode()); // True

This is a meaningful improvement over a plain struct. A plain struct has value-based Equals, but it does not generate ==, !=, or a tuned GetHashCode implementation for you. Record structs generate those members from the declared fields.

One consequence of value equality: two record structs with the same data are interchangeable. If your code relies on reference identity—for example, tracking objects in a dictionary by their reference—a record struct will not preserve that distinction. A record class is also value-equal by default, so if reference identity is the actual requirement you need a plain class or an explicit reference comparer.

Mutation and the with expression

Because record structs are mutable, you can reassign properties directly:

var p = new Point(1, 2); p.X = 5;

The with expression creates a modified copy without touching the original:

var original = new Point(1, 2); var moved = original with { X = 10 }; Console.WriteLine(original); // Point { X = 1, Y = 2 } Console.WriteLine(moved); // Point { X = 10, Y = 2 }

For a record struct, with creates a new struct value by copying the original and then writing the listed properties. This is different from a record class, where with creates a new object on the heap. For a small struct like Point, the copy is cheap. For a struct with many fields, each with call duplicates all of them.

readonly record struct for immutability

If you want record syntax but immutable value semantics, declare the struct as readonly:

public readonly record struct Point(double X, double Y);

Now X and Y are init-only properties. You can set them during object initialization, but not after construction:

var p = new Point(1, 2); p.X = 5; // Compile error: cannot assign after initialization

The with expression still works because it constructs a new instance rather than mutating the existing one:

var p = new Point(1, 2); var p2 = p with { X = 5 }; // OK

A readonly record struct also helps avoid defensive copies in readonly contexts. When a non-readonly struct is passed by in or stored in a readonly field, the compiler may create a local copy before calling an instance member that could mutate the struct. A readonly record struct cannot be mutated after construction, so the compiler can often skip that copy, which matters in hot paths that pass structs by reference.

Performance and memory behavior

Record structs are value types. A local record struct does not require a separate heap allocation by itself, and a record struct field lives inline inside its containing object. If the struct is boxed, the normal value-type boxing rules apply, but for ordinary local and inline usage there is no extra GC pressure from the record itself.

The tradeoff is copy cost. Assigning a record struct to another variable copies every field:

var a = new Point(1, 2); var b = a; // Copies X and Y

For a two-double struct, that is trivial. For a struct with ten string fields, each copy duplicates ten references, not the referenced string objects. If you are creating many copies in a loop, the copy overhead can exceed the GC savings.

The with expression has the same cost profile: it copies the entire struct, then writes the changed fields. There is no way to mutate a single field in place through with; you are always creating a full copy.

If you are designing a data type that will be copied frequently, keep the number of fields small, or prefer a record class when the data is large and reference sharing is acceptable.

Choosing between record struct, record class, and plain struct

The decision depends on semantics, not just syntax convenience.

TypeSemanticsDefault mutabilityEqualityBest fit
record class (positional)Referenceinit-onlyValue-basedDTOs, domain models, data carriers with value-based comparison
record structValueMutableValue-basedSmall data holders, coordinates, ranges, options
readonly record structValueImmutableValue-basedImmutable value objects, dictionary keys, configuration
plain structValueMutableValue-based Equals; manual ==Interop, performance-critical custom types

Use a record struct when:

  • The value is small and self-contained, like a coordinate, a range, or a measurement
  • You want value equality without writing Equals, GetHashCode, and operator overloads by hand
  • You want positional syntax and deconstruction for free
  • You do not need reference identity

Use a record class when:

  • You want a reference type with value-based equality, such as a DTO or message object
  • You want to share a single instance across multiple consumers
  • The data is large enough that copying on every assignment is wasteful

Use a plain struct when:

  • You need explicit control over field layout, such as for interop
  • You are implementing a custom type where the generated record members would not match your requirements
  • You are optimizing a hot path and want to hand-write equality and hashing for maximum control

A common pattern is to use a readonly record struct for value objects that act as keys—for example, a composite key made of an ID and a date. The generated equality and hashing make it work correctly in dictionaries and hash sets without extra code, and the value semantics prevent accidental aliasing.

C# Record Struct: Value Semantics, Mutation, and When to Use It | RYUSLOG DEV