Back to Blog
C#

C# record struct: Value Semantics with Record Features

c# record struct: Learn how C# record structs combine value-type semantics with record syntax, including equality, mutation, and when to choose them over record classes.

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

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

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 the heap allocation of a reference type. The most important thing to understand before using it: unlike a record class, a record struct is mutable by default.

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 record class, the properties in a record struct are mutable by default. The set accessor is generated, not init. If you want immutability, you must 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 also has value-based Equals, but before C# 10 you had to implement == manually, and the default GetHashCode for a struct was notoriously slow because it used reflection-based hashing for each field. A record struct generates a fast, field-by-field hash code for you.

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. Use a record class when identity matters.

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 copies the entire struct and then assigns the specified 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 get-only properties. Direct assignment fails to compile:

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

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 avoids defensive copies. When a non-readonly struct is passed by in or stored in a readonly field, the compiler may create a copy to prevent mutation. A readonly struct eliminates 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 lives on the stack, and a record struct field lives inline inside its containing object. There is no separate heap allocation for the record itself, which means no GC pressure from creating and discarding them. This is the main performance advantage over a record class.

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 plus the object header. 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 classReferenceinit-onlyValue-basedDomain models, DTOs, objects with identity
record structValueMutableValue-basedSmall data holders, coordinates, ranges, options
readonly record structValueImmutableValue-basedImmutable value objects, dictionary keys, configuration
plain structValueMutableManualInterop, 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:

  • The object has a lifecycle or identity beyond its data
  • 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: Practical Usage and Code Examples | RYUSLOG DEV