Skip to content
QSWEQB
mediumConceptCodingMidSenior

When would you make a type a record, a struct, or a class? What does each choose for you?

A class gives reference identity, a struct gives value semantics and lives inline, and a record is a class with value-based equality and a copy-with-changes syntax generated for you. The decision is about identity and copying cost, not about which one is newest.

4 min readUpdated 2026-07-26

What the interviewer is scoring

  • Whether the candidate separates equality semantics from where the data lives
  • That a record is a class by default rather than a value type
  • Does the candidate know when a struct stops being cheap and why
  • Whether the shallow nature of a with expression and of record equality is raised
  • That mutable structs are identified as a trap rather than a style preference

Answer

Two independent choices, not three options

The question looks like a menu and is really two switches.

The first is where the data lives and how it is passed: a reference type is allocated on the heap and passed as a reference, a value type is copied on assignment and lives inline in whatever contains it. class and struct set this.

The second is what equality means: reference equality, meaning the same object, or value equality, meaning the same contents. record sets this, and it also generates a constructor, deconstruction, a readable ToString, and the with expression.

They compose, which is why there are four combinations rather than three options. A record is a class unless you write record struct, which surprises people who assume the word means "value type".

DeclarationLivesEqualityTypical use
classHeap, by referenceReferenceAn entity with identity and a lifetime
recordHeap, by referenceValue, member-wiseA DTO, a message, an immutable domain value
structInline, copiedValue, but slow by defaultSmall numeric-ish values, hot paths
readonly record structInline, copiedValue, generatedSmall immutable values where allocation matters

Choose by identity first

The question that decides it is whether two instances with identical contents are the same thing.

An Order has identity: two orders with the same amount and customer are two orders, and mutating one must not appear to change the other. That is a class. Giving it record equality would make a dictionary of orders behave strangely and would make Equals traverse every field on a type whose fields change.

A Money, a DateRange, a Coordinate, a parsed request body — these have no identity. Two with the same contents are interchangeable, and the code reads better when the language agrees. That is a record.

Once identity says value, the second question is size. Records are the default answer; structs earn their place when the type is small and allocated in quantity.

What a record actually generates

public record Money(decimal Amount, string Currency);

var a = new Money(25.00m, "GBP");
var b = new Money(25.00m, "GBP");

a == b;                     // true - member-wise equality, generated
a.Equals(b);                // true
ReferenceEquals(a, b);      // false - still two heap objects

var c = a with { Amount = 30.00m };   // a is unchanged; c is a new instance
Console.WriteLine(c);       // Money { Amount = 30.00, Currency = GBP }

The generated Equals and GetHashCode are the point. Writing those by hand is the most-skipped and most-wrongly-implemented pair in the language, and getting them inconsistent breaks every dictionary and set the type is used in.

The two shallow behaviours

Both generated features are shallow, and this is the detail interviewers reach for.

with copies the fields, so a reference field points at the same object in both copies. A record holding a List<string> produces a "copy" that shares the list, and mutating it through either reference changes both.

Equality compares fields with their own Equals, so two records holding different arrays with identical contents are not equal — arrays use reference equality, and the generated code does not know you meant otherwise.

public record Basket(string Id, string[] Items);

var x = new Basket("b1", new[] { "apple" });
var y = new Basket("b1", new[] { "apple" });
x == y;    // false. Same contents, different arrays.

The fix is to hold an immutable collection and compare it deliberately, or to override Equals for that member. The trap is that the type looks immutable and value-like, so the behaviour is assumed rather than checked.

When a struct stops being cheap

A struct avoids an allocation and improves locality, which is genuinely valuable in an array of a million of them. It also gets copied on every assignment, every argument pass and every return, so those savings invert as the type grows. The conventional guidance is to keep a struct small, immutable and logically a single value.

Two specific hazards are worth naming.

Mutable structs behave unexpectedly, because you frequently mutate a copy. Retrieving a struct from a list gives you a copy; setting a field on it changes nothing in the list, and the compiler often will not stop you.

Boxing undoes the benefit. Putting a struct into an object, into a non-generic collection, or into an interface allocates a heap object and copies it there. A struct implementing an interface and used through that interface is allocating on every call, which is the opposite of the reason it was a struct.

readonly record struct is the modern shape that avoids most of this: value semantics, generated equality, no allocation, and the compiler enforcing that nothing mutates.

The recognisable wrong answers

Two answers date a candidate. "Records are the new way, so use records for everything" ignores identity, and produces entities whose equality traverses mutable state. "Structs are faster" ignores copying, and produces a sixteen-field struct passed by value through five layers.

The answer an interviewer is listening for names the trade rather than the preference: identity decides class against record, size and allocation volume decide class against struct, and both generated behaviours stop at the first reference.

A record is a class that knows what equality means. A struct is a value that gets copied whether or not you wanted it to.

Likely follow-ups

  • What does `with` copy, and what does it leave shared?
  • Why is a large mutable struct in a list a performance problem?
  • Two records hold the same array. Are they equal?
  • When would you write `readonly record struct`, and what does each word buy?

Related questions

Further reading

recordsstructsvalue-typesequalitycsharp