/compare/csharp/record-vs-class
C# record vs class: equality, immutability and with expressions
verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash) · 10.0.401updated
- tool
- C#
- compares
- record vs class
A C# record is a class with value-based equality: two records are equal when all their properties are equal, while two class instances are equal only when they are the same object. Records declared with positional parameters (record Point(int X, int Y)) get init-only properties, a readable ToString(), deconstruction and non-destructive copies with with. Use records for data that is compared by value (DTOs, messages, keys); use classes for objects with identity and changing state.
Side by side
export DOTNET_NOLOGO=1 DOTNET_CLI_TELEMETRY_OPTOUT=1
dotnet new console -o app >/dev/null 2>&1
cd app
cat > Program.cs <<'EOF'
var r1 = new PointRecord(1, 2);
var r2 = new PointRecord(1, 2);
var c1 = new PointClass { X = 1, Y = 2 };
var c2 = new PointClass { X = 1, Y = 2 };
Console.WriteLine($"record equal: {r1 == r2}, Equals: {r1.Equals(r2)}");
Console.WriteLine($"class equal: {c1 == c2}, Equals: {c1.Equals(c2)}");
Console.WriteLine($"record ToString: {r1}");
Console.WriteLine($"class ToString: {c1}");
var moved = r1 with { Y = 5 }; // copy with one property changed
Console.WriteLine($"with: {moved}, original: {r1}");
var (x, y) = r1; // positional records deconstruct
Console.WriteLine($"deconstruct: {x} {y}");
record PointRecord(int X, int Y);
class PointClass { public int X { get; set; } public int Y { get; set; } }
EOF
dotnet runOutput
record equal: True, Equals: True
class equal: False, Equals: False
record ToString: PointRecord { X = 1, Y = 2 }
class ToString: PointClass
with: PointRecord { X = 1, Y = 5 }, original: PointRecord { X = 1, Y = 2 }
deconstruct: 1 2Differences
| record (record class) | class | |
|---|---|---|
| == and Equals | Compare property values | Compare references (same object) |
| GetHashCode | Based on property values | Based on identity |
| Positional properties | init-only, set once at construction | Declared by hand, often get; set; |
| with expression | Yes: copy with changes | No (C# 10+ allows with on structs and anonymous types) |
| ToString() | Type name and property values | Type name only |
| Deconstruct | Generated for positional records | Write it yourself |
| Inheritance | From other records only | From any class |
| Memory | Reference type (heap); record struct is a value type | Reference type (heap) |
Which one to use
- record: immutable data, values compared by content, API request and response models, events, dictionary keys.
- record struct (C# 10+): small value types where you want the generated equality and
ToStringwithout heap allocations. - class: entities with identity (an EF Core entity, a service, a UI view model) and objects whose state changes over time.
Sources
Related pages
errors · csharpC# warning CS8618: Non-nullable property must contain a non-null value when exiting constructorCS8618 warns that a non-nullable property or field can still be null after the constructor runs. Fix it with required, an initializer, a constructor or string?.errors · csharpC# error CS1061: does not contain a definition and no accessible extension methodC# CS1061: the type has no member with that name and no extension method fits. Fix the member name or type, or add using System.Linq. Real dotnet build output.compare · kotlinKotlin val vs var: read-only vs mutable variablesIn Kotlin, val declares a read-only reference assigned once and var a mutable one. A val object can still change inside. When to use each one, and const val.