When to use record vs class vs struct
Asked Answered
C

4

364
  • Should I be using Record for all of my DTO classes that move data between controller and service layer?

  • Should I be using Record for all my request bindings since ideally I would want the request sent to the controller to be immutable for my asp.net API

What is a Record? Anthony Giretti Introducing C# 9: Records

public class HomeController : Controller
{ 
    public async Task<IActionResult> Search(SearchParameters searchParams)
    {
        await _service.SearchAsync(searchParams);
    }
}

should SearchParameters be made a Record?

Chapel answered 13/11, 2020 at 6:58 Comment(1)
note that if you are using wpf and datagrid you may encounter some issues: #68440392Novelist
L
492

Short version

Can your data type be a value type? Go with struct. No? Does your type describe a value-like, preferably immutable state? Go with record.

Use class otherwise. So...

  1. Yes, use records for your DTOs if it is one way flow.
  2. Yes, immutable request bindings are an ideal user case for a record
  3. Yes, SearchParameters are an ideal user case for a record.

For further practical examples of record use, you can check this repo.

Long version

A struct, a class and a record are user data types.

Structures are value types. Classes are reference types. Records are by default immutable reference types.

When you need some sort of hierarchy to describe your data types like inheritance or a struct pointing to another struct or basically things pointing to other things, you need a reference type.

Records solve the problem when you want your type to be a value oriented by default. Records are reference types but with the value oriented semantic.

With that being said, ask yourself these questions...


Does your data type respect all of these rules:

  1. It logically represents a single value, similar to primitive types (int, double, etc.).
  2. It has an instance size under 16 bytes.
  3. It is immutable.
  4. It will not have to be boxed frequently.
  • Yes? It should be a struct.
  • No? It should be some reference type.

Does your data type encapsulate some sort of a complex value? Is the value immutable? Do you use it in unidirectional (one way) flow?

  • Yes? Go with record.
  • No? Go with class.

BTW: Don't forget about anonymous objects. There will be an anonymous records in C# 10.0.

Notes

A record instance can be mutable if you make it mutable.

class Program
{
    static void Main()
    {
        var test = new Foo("a");
        Console.WriteLine(test.MutableProperty);
        test.MutableProperty = 15;
        Console.WriteLine(test.MutableProperty);
        //test.Bar = "new string"; // will not compile
    }
}

record Foo(string Bar)
{
    internal double MutableProperty { get; set; } = 10.0;
}

An assignment of a record is a shallow copy of the record. A copy by with expression of a record is neither a shallow nor a deep copy. The copy is created by a special clone method emitted by C# compiler. Value-type members are copied and boxed. Reference-type members are pointed to the same reference. You can do a deep copy of a record if and only if the record has value type properties only. Any reference type member property of a record is copied as a shallow copy.

See this example (using top-level feature in C# 9.0):

using System.Collections.Generic;
using static System.Console;

var foo = new SomeRecord(new List<string>());
var fooAsShallowCopy = foo;
var fooAsWithCopy = foo with { }; // A syntactic sugar for new SomeRecord(foo.List);
var fooWithDifferentList = foo with { List = new List<string>() { "a", "b" } };
var differentFooWithSameList = new SomeRecord(foo.List); // This is the same like foo with { };
foo.List.Add("a");

WriteLine($"Count in foo: {foo.List.Count}"); // 1
WriteLine($"Count in fooAsShallowCopy: {fooAsShallowCopy.List.Count}"); // 1
WriteLine($"Count in fooWithDifferentList: {fooWithDifferentList.List.Count}"); // 2
WriteLine($"Count in differentFooWithSameList: {differentFooWithSameList.List.Count}"); // 1
WriteLine($"Count in fooAsWithCopy: {fooAsWithCopy.List.Count}"); // 1
WriteLine("");

WriteLine($"Equals (foo & fooAsShallowCopy): {Equals(foo, fooAsShallowCopy)}"); // True. The lists inside are the same.
WriteLine($"Equals (foo & fooWithDifferentList): {Equals(foo, fooWithDifferentList)}"); // False. The lists are different
WriteLine($"Equals (foo & differentFooWithSameList): {Equals(foo, differentFooWithSameList)}"); // True. The list are the same.
WriteLine($"Equals (foo & fooAsWithCopy): {Equals(foo, fooAsWithCopy)}"); // True. The list are the same, see below.
WriteLine($"ReferenceEquals (foo.List & fooAsShallowCopy.List): {ReferenceEquals(foo.List, fooAsShallowCopy.List)}"); // True. The records property points to the same reference.
WriteLine($"ReferenceEquals (foo.List & fooWithDifferentList.List): {ReferenceEquals(foo.List, fooWithDifferentList.List)}"); // False. The list are different instances.
WriteLine($"ReferenceEquals (foo.List & differentFooWithSameList.List): {ReferenceEquals(foo.List, differentFooWithSameList.List)}"); // True. The records property points to the same reference.
WriteLine($"ReferenceEquals (foo.List & fooAsWithCopy.List): {ReferenceEquals(foo.List, fooAsWithCopy.List)}"); // True. The records property points to the same reference.
WriteLine("");

WriteLine($"ReferenceEquals (foo & fooAsShallowCopy): {ReferenceEquals(foo, fooAsShallowCopy)}"); // True. !!! fooAsCopy is pure shallow copy of foo. !!!
WriteLine($"ReferenceEquals (foo & fooWithDifferentList): {ReferenceEquals(foo, fooWithDifferentList)}"); // False. These records are two different reference variables.
WriteLine($"ReferenceEquals (foo & differentFooWithSameList): {ReferenceEquals(foo, differentFooWithSameList)}"); // False. These records are two different reference variables and reference type property hold by these records does not matter in ReferenceEqual.
WriteLine($"ReferenceEquals (foo & fooAsWithCopy): {ReferenceEquals(foo, fooAsWithCopy)}"); // False. The same story as differentFooWithSameList.
WriteLine("");

var bar = new RecordOnlyWithValueNonMutableProperty(0);
var barAsShallowCopy = bar;
var differentBarDifferentProperty = bar with { NonMutableProperty = 1 };
var barAsWithCopy = bar with { };

WriteLine($"Equals (bar & barAsShallowCopy): {Equals(bar, barAsShallowCopy)}"); // True.
WriteLine($"Equals (bar & differentBarDifferentProperty): {Equals(bar, differentBarDifferentProperty)}"); // False. Remember, the value equality is used.
WriteLine($"Equals (bar & barAsWithCopy): {Equals(bar, barAsWithCopy)}"); // True. Remember, the value equality is used.
WriteLine($"ReferenceEquals (bar & barAsShallowCopy): {ReferenceEquals(bar, barAsShallowCopy)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (bar & differentBarDifferentProperty): {ReferenceEquals(bar, differentBarDifferentProperty)}"); // False. Operator with creates a new reference variable.
WriteLine($"ReferenceEquals (bar & barAsWithCopy): {ReferenceEquals(bar, barAsWithCopy)}"); // False. Operator with creates a new reference variable.
WriteLine("");

var fooBar = new RecordOnlyWithValueMutableProperty();
var fooBarAsShallowCopy = fooBar; // A shallow copy, the reference to bar is assigned to barAsCopy
var fooBarAsWithCopy = fooBar with { }; // A deep copy by coincidence because fooBar has only one value property which is copied into barAsDeepCopy.

WriteLine($"Equals (fooBar & fooBarAsShallowCopy): {Equals(fooBar, fooBarAsShallowCopy)}"); // True.
WriteLine($"Equals (fooBar & fooBarAsWithCopy): {Equals(fooBar, fooBarAsWithCopy)}"); // True. Remember, the value equality is used.
WriteLine($"ReferenceEquals (fooBar & fooBarAsShallowCopy): {ReferenceEquals(fooBar, fooBarAsShallowCopy)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (fooBar & fooBarAsWithCopy): {ReferenceEquals(fooBar, fooBarAsWithCopy)}"); // False. Operator with creates a new reference variable.
WriteLine("");

fooBar.MutableProperty = 2;
fooBarAsShallowCopy.MutableProperty = 3;
fooBarAsWithCopy.MutableProperty = 3;
WriteLine($"fooBar.MutableProperty = {fooBar.MutableProperty} | fooBarAsShallowCopy.MutableProperty = {fooBarAsShallowCopy.MutableProperty} | fooBarAsWithCopy.MutableProperty = {fooBarAsWithCopy.MutableProperty}"); // fooBar.MutableProperty = 3 | fooBarAsShallowCopy.MutableProperty = 3 | fooBarAsWithCopy.MutableProperty = 3
WriteLine($"Equals (fooBar & fooBarAsShallowCopy): {Equals(fooBar, fooBarAsShallowCopy)}"); // True.
WriteLine($"Equals (fooBar & fooBarAsWithCopy): {Equals(fooBar, fooBarAsWithCopy)}"); // True. Remember, the value equality is used. 3 == 3
WriteLine($"ReferenceEquals (fooBar & fooBarAsShallowCopy): {ReferenceEquals(fooBar, fooBarAsShallowCopy)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (fooBar & fooBarAsWithCopy): {ReferenceEquals(fooBar, fooBarAsWithCopy)}"); // False. Operator with creates a new reference variable.
WriteLine("");

fooBarAsWithCopy.MutableProperty = 4;
WriteLine($"fooBar.MutableProperty = {fooBar.MutableProperty} | fooBarAsShallowCopy.MutableProperty = {fooBarAsShallowCopy.MutableProperty} | fooBarAsWithCopy.MutableProperty = {fooBarAsWithCopy.MutableProperty}"); // fooBar.MutableProperty = 3 | fooBarAsShallowCopy.MutableProperty = 3 | fooBarAsWithCopy.MutableProperty = 4
WriteLine($"Equals (fooBar & fooBarAsWithCopy): {Equals(fooBar, fooBarAsWithCopy)}"); // False. Remember, the value equality is used. 3 != 4
WriteLine("");

var venom = new MixedRecord(new List<string>(), 0); // Reference/Value property, mutable non-mutable.
var eddieBrock = venom;
var carnage = venom with { };
venom.List.Add("I'm a predator.");
carnage.List.Add("All I ever wanted in this world is a carnage.");
WriteLine($"Count in venom: {venom.List.Count}"); // 2
WriteLine($"Count in eddieBrock: {eddieBrock.List.Count}"); // 2
WriteLine($"Count in carnage: {carnage.List.Count}"); // 2
WriteLine($"Equals (venom & eddieBrock): {Equals(venom, eddieBrock)}"); // True.
WriteLine($"Equals (venom & carnage): {Equals(venom, carnage)}"); // True. Value properties has the same values, the List property points to the same reference.
WriteLine($"ReferenceEquals (venom & eddieBrock): {ReferenceEquals(venom, eddieBrock)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (venom & carnage): {ReferenceEquals(venom, carnage)}"); // False. Operator with creates a new reference variable.
WriteLine("");

eddieBrock.MutableList = new List<string>();
eddieBrock.MutableProperty = 3;
WriteLine($"Equals (venom & eddieBrock): {Equals(venom, eddieBrock)}"); // True. Reference or value type does not matter. Still a shallow copy of venom, still true.
WriteLine($"Equals (venom & carnage): {Equals(venom, carnage)}"); // False. the venom.List property does not points to the same reference like in carnage.List anymore.
WriteLine($"ReferenceEquals (venom & eddieBrock): {ReferenceEquals(venom, eddieBrock)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (venom & carnage): {ReferenceEquals(venom, carnage)}"); // False. Operator with creates a new reference variable.
WriteLine($"ReferenceEquals (venom.List & carnage.List): {ReferenceEquals(venom.List, carnage.List)}"); // True. Non mutable reference type.
WriteLine($"ReferenceEquals (venom.MutableList & carnage.MutableList): {ReferenceEquals(venom.MutableList, carnage.MutableList)}"); // False. This is why Equals(venom, carnage) returns false.
WriteLine("");


record SomeRecord(List<string> List);

record RecordOnlyWithValueNonMutableProperty(int NonMutableProperty);

record RecordOnlyWithValueMutableProperty
{
    internal int MutableProperty { get; set; } = 1; // this property gets boxed
}

record MixedRecord(List<string> List, int NonMutableProperty)
{
    internal List<string> MutableList { get; set; } = new();
    internal int MutableProperty { get; set; } = 1; // this property gets boxed
}

The performance penalty is obvious here. A larger data to copy in a record instance you have, a larger performance penalty you get. Generally, you should create small, slim classes and this rule applies to records too.

If your application is using database or file system, I wouldn't worry about this penalty much. The database/file system operations are generally slower.

I made some synthetic test (full code below) where classes are wining but in real life application, the impact should be unnoticeable.

In addition, the performance is not always number one priority. These days, the maintainability and readability of your code is preferable than highly optimized spaghetti code. It is the code author choice which way (s)he would prefer.

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace SmazatRecord
{
    class Program
    {
        static void Main()
        {
            var summary = BenchmarkRunner.Run<Test>();
        }
    }

    public class Test
    {

        [Benchmark]
        public int TestRecord()
        {
            var foo = new Foo("a");
            for (int i = 0; i < 10000; i++)
            {
                var bar = foo with { Bar = "b" };
                bar.MutableProperty = i;
                foo.MutableProperty += bar.MutableProperty;
            }
            return foo.MutableProperty;
        }

        [Benchmark]
        public int TestClass()
        {
            var foo = new FooClass("a");
            for (int i = 0; i < 10000; i++)
            {
                var bar = new FooClass("b")
                {
                    MutableProperty = i
                };
                foo.MutableProperty += bar.MutableProperty;
            }
            return foo.MutableProperty;
        }
    }

    record Foo(string Bar)
    {
        internal int MutableProperty { get; set; } = 10;
    }

    class FooClass
    {
        internal FooClass(string bar)
        {
            Bar = bar;
        }
        internal int MutableProperty { get; set; }
        internal string Bar { get; }
    }
}

Result:

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.1379 (1909/November2018Update/19H2)
AMD FX(tm)-8350, 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=5.0.103
  [Host]     : .NET Core 5.0.3 (CoreCLR 5.0.321.7212, CoreFX 5.0.321.7212), X64 RyuJIT
  DefaultJob : .NET Core 5.0.3 (CoreCLR 5.0.321.7212, CoreFX 5.0.321.7212), X64 RyuJIT
Method Mean Error StdDev
TestRecord 120.19 μs 2.299 μs 2.150 μs
TestClass 98.91 μs 0.856 μs 0.800 μs
Loose answered 13/11, 2020 at 22:21 Comment(31)
there are also tuplesYoungling
While most of that is correct, records don't have to be immutable, there is no guarantee for that. But what they do is copy(clone) and check for equalitity everything including private readonly fields and mutable properties which have setters.Tutor
@Tutor You are right. The record can be mutable if you make it mutable. I've edited my answer to note this. Thx for bringing it up.Loose
What actually happens when you assign a record to a new variable? is it copied? or is it just copying the reference like a class would? What are the performance implications of using an immutable record model verse a mutable class model? Does the entire Module have to be recreated each time something is changed?Spillage
@Spillage I've updated my answer for you.Loose
Hmm, I still do not understand why there was a need for the new record type, why not use struct? I suspect some internal memory management reason. I like that with C# one gains access to a comprehensive set of libraries, but as a language it does not impress me. Better than VB, I guess.Turmoil
@RadimCernej If you have data structures where members points to other things, you need a reference type. Records solve this problem by providing by-default a value oriented semantic where a record can point to another record. Structures can't do than. In addition, structures don't support inheritance.Loose
I don't understand what you're saying when you claim that barAsCopy is a shallow clone of bar, and that the MutableProperty gets boxed. Records are reference types, so when you assign bar to barAsCopy nothing is happening other than barAsCopy and bar now share a single reference, which is why ReferenceEquals returns true. There isn't any copying happening. When you use with with operator, that's when a clone is taking place, but that will be a shallow clone, not a deep clone, meaning that the reference to any reference types will be copied, not the referenced objectsWadsworth
I see no "shallow clone" in my answer but what you are writing your comment are valid statements about records.Loose
@Wadsworth is correct; bar with { } is NOT a deep copy/clone - it is shallow. A "deep copy" is one that clones the entire hierarchy rather than just the top level; with only copies the top level. Additionally, barAsCopy = bar is not a copy at all, but a shared reference. In contrast, if these values were structs (i.e., value types), the data would truly be copied rather than merely referenced and mutations to one would not effect the other.Hyperploid
@Loose "Clone" and "copy" mean the same thing in this context. The problem with your answer is your second code example where you claim var barAsCopy = bar; is a "shallow copy" and then you have var barAsDeepCopy = bar with { }; // A deep copy. This is wrong. In the former, there is no copying whatsoever, you are simply assigning an existing reference to a new variable, which is how all reference types work. In the latter, you are creating a shallow copy/clone, not a deep copy. The with operator doesn't create copies of reference types contained within properties of the recordWadsworth
@Wadsworth The problem with your comment is that you are not looking on my record itself and misinterpreting my demo. The record is indeed a deep copy because it contains only a value type property. The generic behavior of records with reference type properties is as you describing it in your previous comments.Loose
@Loose You're making it quite hard for anyone to interpret it "correctly". The line right before the "deep copy" claims that an assignment is a shallow copy. That is flat out wrong. Then, even if the copy of the record is a deep copy, for any record that contains a reference type, the method you used results in a shallow copy. When trying to illustrate how to create shallow vs. deep copies of records in general, this example is at a minimum misleading and confusing, and at most just flat out wrong.Wadsworth
So much wrong things in your code comments. It's scary that people trust accepted answer and learn from it.Sunken
@Wadsworth Check out my updated answer. I made it more explanatory. Maybe that will satisfy you.Loose
What's a "value type" ?Yesterday
@Yesterday Check thisLoose
It's completely wrong to say that "an assignment of a record is a shallow copy of the record." Assigning a record is the exact same as assigning a class, it's just a copy of the reference.Frisky
@Frisky I follow this generally accepted shallow copy definition.Loose
@Loose I see, but in the context of high level code and when we're talking about value/reference semantics, this is a very ambiguous definition of "shallow copy" in my opinion. Because this doesn't leave space for the practical meaning of deep copy (copying the whole hierarchy of the object) vs shallow copy (just copying the reference/values of the first level of the hierarchy).Frisky
I didn't come up with these definitions but I tent to use them because they are well known. If a reference A is pointing to the same memory location as a reference B, the reference A is a shallow copy of reference B. Yeah, we can say that the reference A is a reference copy of the reference B, but it is kinda mouthful.Loose
When should I use a record instead of a readonly struct (with in propagation)? Here goes my guess: - When data size is too big to be stack allocated. - In order to enjoy inheritance and polymorphism.Lighting
to me this answer is quite opinionated and should state that clearlyEpiphenomenalism
this the most well answered question ive ever seen on SOUndoing
Immutability is quite relevant for reference types, because reference types can easily be referenced from multiple places; i.e. references can be aliased. However, for structs (value types) mutability isn't really that important, because outside of ref and related trickiness, they'll never be aliased. However, ideally struct methods would all be marked readonly,to avoid being dependant on where copies happen for semantics. While it's certainly easier to reason about non-mutated things, that's doubly so for ref types - mutation is thus not generally a great argument for ref types.Breastwork
The benchmark is misleading; you're comparing apples to oranges. Your record example is using with to copy an existing record, in a way the class example does not. However, the entire performance difference you observe is explained by the extra copy you're doing in the record case. Records are classes; and (without inheritance) have identical performance characteristics when doing apples-to-apples comparison. Code using records does however more easily copy and more easily do a value-based equality, both of which aren't free.Breastwork
KUTlime, can you add information about record struct in your answer?Irkutsk
@Irkutsk I will, but give me some time. ;)Loose
@KUTlime, could you give an example of what you mean by unidirectional data flow?Stavropol
@Stavropol Example 1: Passing & using arguments passed to API endpoints. Example 2: Any read-only data passed to UI, or outside your software boundary (DTOs). Example 3: Passing around read-only configurations.Loose
This answer uses the term "shallow copy" to refer to just assiging the reference to a different variable and "deep copy" to refer to actual copy. This makes the explanation so much harder to understand.Proffitt
L
46

I really like the answers above, they are very precise and complete, but I am missing an important type: readonly struct (C#9) and, coming soon, record struct (C#10).

As we find C# and .Net used in new domains, some problems become more prominent. As examples of environments that are more critical than average about computation overheads, I can list

  • cloud/datacenter scenarios where computation is billed for and responsiveness is a competitive advantage.
  • Games/VR/AR with soft-realtime requirements on latencies

So, correct me if I am wrong, but I would follow the usual rules:


class / record / ValueObject:

  • Reference type; ref and in keywords are not needed.
  • Heap allocated; more work for GC.
  • Allows non-public parameterless constructor.
  • Allows inheritance, polymorphism and interface implementation.
  • Does not have to be boxed.
  • Use record as DTOs and immutable/value objects.
  • Use ValueObject when you need both immutability and, either IComparable or precise control over equality checks.

(readonly / record) struct:

  • Value type; can be passed as readonly reference with in keyword.
  • Stack allocated; suitable for cloud/datacenter/Games/VR/AR.
  • Does not allow non-public parameterless constructor.
  • Does not allow inheritance, polymorphism, but interface implementation.
  • Might have to be boxed frequently.
Lighting answered 13/12, 2021 at 15:20 Comment(3)
For some performance comparisons between struct and record struct, see: nietras.com/2021/06/14/csharp-10-record-structYuji
Thank you! Stack vs heap is a big help. I know not many people today understand the difference between the stack and heap. I wish records could be optimized into either stack or heap, eliminating any new heap allocationsSuppositive
I do wonder if it needs to persist for a long amount of time, across several functions or as a singleton, a record is better. I would assume it would better thash into ROM and lazy load into RAM as neededSuppositive
S
23

You can use structure types to design data-centric types that provide value equality and little or no behavior. But for relatively large data models, structure types have some disadvantages:

  • They don't support inheritance.
  • They're less efficient at determining value equality. For value types, the ValueType.Equals method uses reflection to find all fields. For records, the compiler generates the Equals method. In practice, the implementation of value equality in records is measurably faster.
  • They use more memory in some scenarios since every instance has a complete copy of all of the data. Record types are reference types, so a record instance contains only a reference to the data.

While records can be mutable, they are primarily intended for supporting immutable data models. The record type offers the following features:

  • Concise syntax for creating a reference type with immutable properties

  • Value equality

  • The concise syntax for nondestructive mutation

  • Built-in formatting for display

  • Support for inheritance hierarchies

Record types have some disadvantages:

  • C# records don’t implement the IComparable interface

  • In terms of encapsulation, records are much better than structs because you can’t hide the parameterless constructor in a struct, But Record still has poor encapsulation and we can instantiate an object with an invalid state.

  • Don't have control over equality checks

C# Record use cases:

  • Records will replace the Fluent Interface pattern in C#. The Test Data Builder pattern is a great example here. Instead of writing your own boilerplate code, you can now use the new with feature and save yourself tons of time and effort.

  • Record is good for DTOs

  • You may also need interim data classes while loading data to or retrieving it from the database or while doing some preprocessing. This is similar to the above DTOs, but instead of serving as data contracts between your application and external systems, these data classes act as DTOs between different layers of your own system. C# records are great for that too.

  • Finally, not all applications require a rich, fully encapsulated domain model. In most simpler cases that don’t need much encapsulation, C# records would do just fine. otherwise use DDD value object

^ ^

Shoreless answered 19/5, 2021 at 14:7 Comment(2)
records are great for Value objects as well because of the concise syntax for immutability and other related featuresMylesmylitta
Yes, In simple cases, when you don't require a rich, fully encapsulated domain model, Record would be fine otherwise use Value object.Shoreless
D
12

Records provide concise syntax for types where the fundamental use is storing data. For object-oriented classes, the fundamental use is defining responsibilities.

From Microsoft :

Records add another way to define types. You use class definitions to create object-oriented hierarchies that focus on the responsibilities and behavior of objects. You create struct types for data structures that store data and are small enough to copy efficiently. You create record types when you want value-based equality and comparison, don't want to copy values, and want to use reference variables. You create record struct types when you want the features of records for a type that is small enough to copy efficiently.

https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/records

Disfigurement answered 29/8, 2022 at 19:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.