Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are the disadvantages of using records instead of classes?

Tags:

c#-9.0

C# 9 introduces record reference types. A record provides some synthesized methods like copy constructor, clone operation, hash codes calculation and comparison/equality operations. It seems to me convenient to use records instead of classes in general. Are there reasons no to do so?

It seems to me that currently Visual Studio as an editor does not support records as well as classes but this will probably change in the future.

like image 962
MarkusParker Avatar asked Jan 23 '26 17:01

MarkusParker


2 Answers

Firstly, be aware that if it's possible for a class to contain circular references (which is true for most mutable classes) then many of the auto generated record members can StackOverflow. So that's a pretty good reason to not use records for everything.

So when should you use a record?

Use a record when an instance of a class is entirely defined by the public data it contains, and has no unique identity of it's own.

This means that the record is basically just an immutable bag of data. I don't really care about that particular instance of the record at all, other than that it provides a convenient way of grouping related bits of data together.

Why?

Consider the members a record generates:

  1. Value Equality

Two instances of a record are considered equal if they have the same data (by default: if all fields are the same).

This is appropriate for classes with no behavior, which are just used as immutable bags of data. However this is rarely the case for classes which are mutable, or have behavior.

For example if a class is mutable, then two instances which happen to contain the same data shouldn't be considered equal, as that would imply that updating one would update the other, which is obviously false. Instead you should use reference equality for such objects.

Meanwhile if a class is an abstraction providing a service you have to think more carefully about what equality means, or if it's even relevant to your class. For example imagine a Crawler class which can crawl websites and return a list of pages. What would equality mean for such a class? You'd rarely have two instances of a Crawler, and if you did, why would you compare them?

  1. with blocks

with blocks provides a convenient way to copy an object and update specific fields. However this is always safe if the object has no identity, as copying it doesn't lose any information. Copying a mutable class loses the identity of the original object, as updating the copy won't update the original. As such you have to consider whether this really makes sense for your class.

  1. ToString

The generated ToString prints out the values of all public properties. If your class is entirely defined by the properties it contains, then this makes a lot of sense. However if your class is not, then that's not necessarily the information you are interested in. A Crawler for example may have no public fields at all, but the private fields are likely to be highly relevant to its behavior. You'll probably want to define ToString yourself for such classes.

like image 68
Yair Halberstadt Avatar answered Jan 25 '26 20:01

Yair Halberstadt


As others have pointed out, it's great for data types that contain pure data and have no logic. Records do reduce boilerplate. The thing I dislike about records is the constructor, which makes it a bit too easy to introduce accidental bugs.

Positional Parameters

Let's assume, you have this record Person

public record Person(string Firstname, string Lastname);

// Then using it like this:
var someone = new Person("John", "Doe");

Looks good, right? But what happens, if someone mistakenly uses the parameters in the incorrect order? The last name is now in the first name.

var wrong = new Person("Doe", "John");

The compiler will not be able to catch this mistake. Now, it's possible that you have even multiple parameters with the same datatype in the constructor. This will increate the area for mistakes even more. Imagine another developer refactors this object and switches the parameter's ordering? The lastname now comes first, but your IDE will not change the parameters position automatically and the compiler will not warn you either for initializations that were not updated. This now would introduce multiple "silent bugs" and if you don't have a solid test or code review process that catches these kinds of errors, the chance is you will not discover what happened until everything is in production and that maybe for some time already. (And do you unit test if the first name is really the first name anyways?)

Of course, you could avoid this by using value types, but that is often just overkill (and there's a better way, keep reading).

Optional Parameters

A second disadvantage is that you can not easily introduce optional parameters without having to change the code. Take this example:

public record Person(string Firstname,  string? Middlename, string Lastname);

Introducing the optional Middlename property will break your code everywhere this type is used. At least the compiler will warn you now, but since it's an optional parameter, you will have to refactor to named parameters or add a null everywhere.

// Named parameters
var person = new Person(Firstname: "Peter", Lastname: "Parker");

Recommendation

What I personally recommend nowadays is to use required properties. Note that the required modifier (C# 11) was introduced after record (C# 9).

By refactoring our previous model, we can reduce the chance of mistakes and improve the developer experience (DX) for users using your type. There is some more code to write, it pays off in my opinion.

public record Person 
{
   public required string Firstname { get; init; }
   public required string Lastname { get; init; }
   public string? Middlename { get; init; }
}

var someone = new Person 
{
   Firstname = "John",
   Lastname = "Doe"
}

Now as you can see, this approach requires a few more lines of code, because it's a bit more explicit. In my opinion, it pays off. It's a nice balance between as little boilerplate as possible and the best developer experience and safety of code. Sometimes being explicit is worth it, even if it leads to more code. To note here is that the record will still provide you with the overrides (e.g., HashCode and ToString). The record is still immutable and you can use the with keyword.

PS: Use your IDE's code snippet to create the Properties automagically! For example prop in the Rider IDE will create the property for you and you only have to fill out the blanks.

like image 41
El Mac Avatar answered Jan 25 '26 20:01

El Mac



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!