CodeGym /Courses /C# SELF /Differences between record, class, and struct

Differences between record, class, and struct

C# SELF
Level 19 , Lesson 4
Available

1. Why did we even need record?

In C# (and .NET in general), for a long time the main building blocks were classes (class) and structs (struct). But each of them is kinda incomplete for some tasks. Classes are reference types, mutable, and compared by reference (with rare exceptions). Structs are value types (copied when passed), by default compared byte-by-byte and usually mutable (until readonly struct showed up).

But if you wanted a simple and natural way to store data, easy to compare by value, quick to clone, and not worry about someone changing your object somewhere — you had to invent a bunch of workarounds or use libraries like ValueTuple or even System.Tuple. But none of them are as neat and expressive as you'd like.

That's why in C# 9, record appeared — a data type that combines short declarations, immutability safety, and value-based comparison behavior.

2. All four types on one page

class struct record record struct
Category Reference type Value type Reference type Value type
Mutability Default — yes Default — yes Default — no (init) Default — no (init)
Comparison By reference (==) By value (fields) By value (fields/properties) By value (fields)
Cloning Manual only Manual only Built-in support (with) Built-in support (with)
Inheritance Yes No Yes No
Immutability Have to implement Have to implement Super easy to implement Super easy to implement
Syntax Longest Short Shortest (positional) Pretty short
Usage in collections By reference Copies By reference Copies

Visual diagram

+----------------+     +----------------+     +--------------------+
|    class       |     |    struct      |     |      record        |
+----------------+     +----------------+     +--------------------+
| Reference Type |     | Value Type     |     | Reference Type     |
| Mutable        |     | Mutable        |     | Immutable (init)   |
| == : Reference |     | == : By Fields |     | == : By Value      |
+----------------+     +----------------+     +--------------------+

3. The essence of differences between record, class and struct

Memory behavior: reference or value?

  • class and record are reference types. When you pass them to a function, the reference to the object is copied.
  • struct and record struct are value types. They're always copied byte-by-byte (unless you explicitly pass them by reference).

class PointClass { public int X; public int Y; }
struct PointStruct { public int X; public int Y; }
record PointRecord(int X, int Y);
record struct PointRecordStruct(int X, int Y);

void Demo()
{
    var pc = new PointClass { X = 1, Y = 2 };
    var ps = new PointStruct { X = 1, Y = 2 };
    var pr = new PointRecord(1, 2);
    var prs = new PointRecordStruct(1, 2);

    ChangeY(pc);    // pc.Y will change!
    ChangeY(ps);    // ps.Y won't change — it's a copy!
    ChangeY(pr);    // pr.Y will change!
    ChangeY(prs);   // prs.Y won't change — it's a copy!
}

void ChangeY(dynamic p) { p.Y = 99; }

If you think dynamic here is some kind of magic — yeah, it's just for the example, so you can see that with structs the value won't change, but with class or record (reference) — it will.

Comparison: how do you know if two objects are equal?

  • class: compared by reference (== — true only if it's the same object in memory), unless you override Equals.
  • struct: compared by value of all fields (by default).
  • record: compared by value of all fields/properties set in the primary constructor.
class Foo { public int A; public int B; }
record Bar(int A, int B);

var foo1 = new Foo { A = 42, B = 1 };
var foo2 = new Foo { A = 42, B = 1 };
var bar1 = new Bar(42, 1);
var bar2 = new Bar(42, 1);

Console.WriteLine(foo1 == foo2); // False! Different objects
Console.WriteLine(bar1 == bar2); // True! Values match

Fun fact:
With record struct it's even cooler: they compare "by value" like regular structs, but with record syntax and features.

4. Immutability: who guarantees what?

Let's compare object safety:

  • class: By default, easily mutable unless you make all fields readonly.
  • struct: Same, but you can declare as readonly struct, then neither fields nor properties can be changed.
  • record: Usually fields are declared with init modifier, so you can only set them in the constructor or object initializer (with). Super handy for safe data passing.
  • record struct: Same, you can make readonly record struct and get immutability for value types with all the record goodies.

record Person(string Name, int Age);

var p1 = new Person("Alexey", 23);
// p1.Age = 24; // Error! Only init

var p2 = p1 with { Age = 24 }; // Works! Creates a copy with new data

When you have a big project and dozens of entity objects, record helps you avoid tons of bugs like "someone changed a field somewhere — and everything broke".

5. Syntax: what declarations look like and how not to get confused


// class
public class Product
{
    public int Id { get; init; }
    public string Name { get; init; }
}

// struct
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

// record
public record Product(int Id, string Name);

// record struct
public record struct Point(int X, int Y);
Syntax comparison for different type declarations

As you can see, the record type is the shortest. You get constructor, deconstructor, equality, ToString() and a bunch of other stuff "out of the box".

Syntax You get for free Can you inherit
class nothing (almost) Yes
struct nothing (almost) No
record ToString, Equals, Deconstruct, with Yes
record struct ToString, Equals, Deconstruct, with No

6. Cloning and the with operator

Only record and record struct have a special with operator, which lets you copy an object and change some properties without pain.


record User(string Name, int Age);

var user1 = new User("Irina", 28);
var user2 = user1 with { Age = 29 }; // user2.Name == "Irina", user2.Age == 29

With a class (class), you'll have to write a copy method by hand, and if you forget to copy a field — hello, bugs.

7. Inheritance: who can inherit from whom

  • class: Supports standard inheritance (class hierarchies, virtual methods, abstractions, etc.).
  • struct: Doesn't support (can only implement interfaces).
  • record: Supports inheritance, but with some restrictions (for example, inheritance is only possible between record types, not with class).
  • record struct: Doesn't support, just like regular structs.
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name); // Okay!

class Vehicle { }
class Car : Vehicle { } // Okay!

// struct can't inherit from struct

More about inheritance in the next level :P

8. Where these types are used in practice

  • class: Big objects with rich behavior, long lifecycle, mutable state, hierarchy (like business logic, UI components).
  • struct: Small value objects where you want super fast copying, no GC, and minimal overhead (like coordinates, colors, sums — stuff that's easy and fast to clone).
  • record: DTO (Data Transfer Object), parameter objects, config parameters, immutable states, calculation results that are easy to compare by content.
  • record struct: Value types where you want immutability and record-like behavior, but without extra heap allocations.

9. Typical mistakes and gotchas

Sometimes devs think record is just a "class replacement". Nope! If you're writing an object that should change its state during its life — use class.
If you want to compare objects by reference (like in the singleton pattern or when object lifecycle is critical) — use class.
If you're making a value object that should behave like a number or a point on a grid — struct or record struct.
If you're working with immutable, easily comparable objects that are often passed between app layers, stored in collections, logged, and serialized — go with record.

Also remember: if you declare a record class with read-only properties, but forget about nested objects — nested fields can still be changed if they're mutable themselves.


record Student(string Name, List<int> Grades);

var s1 = new Student("Anton", new List<int>() {5,5,5});
var s2 = s1 with { };

s2.Grades.Add(2); // Both objects are "cursed" with a two! s1.Grades == s2.Grades
2
Task
C# SELF, level 19, lesson 4
Locked
Creating a record and comparing objects
Creating a record and comparing objects
2
Task
C# SELF, level 19, lesson 4
Locked
Using the with operator to write data
Using the with operator to write data
1
Survey/quiz
DTO Problem, level 19, lesson 4
Unavailable
DTO Problem
DTO, record, and with
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION