When to use record or class on C#?

Asked

Viewed 289 times

10

One of the novelties of C# 9 are the record types, I saw that one of the biggest differences between them and the classes are the methods .ToString() and .Equals() who has a different behavior. But I want to know if I should take into account other points besides these methods when choosing between using one or the other, or making this comparison between them is not correct?

In short: When should I choose to use class or record?

2 answers

9


TL; DR

The main reason to adopt this mechanism is to have a simple data structure that does not adopt the common style in object orientation, that is, there is a preference for functional style.

The record used in the way he was thinking is a type with semantics of value but that is stored as reference and has time of life detached from those who created it. That’s why he’s a substitute for a struct and not one exactly of the class.

Speaking as mechanism, record, for all intents and purposes, it is a class, so I believe there is this confusion. There is a small change and with that a different motivation. This mechanism has no limitations in relation to what the class can do, it has, optionally, something else. The concept is quite different.

Myth of immutability

In the end I quote the official documentation as a source to show what the mechanism is and how it works. But in the description of what is new in language she is clearly wrong at least one point, which undermines the credibility of the rest.

C# 9.0 introduces record types, which are a Reference type that provides synthesized methods to provide value Semantics for Equality. Records are immutable by default.

Source.

Records are not immutable by default, at least not different from how class is. A field is mutable by default in a record, if you don’t want him to change, you have to say that he is readonly. All examples use properties, and here we can even consider that they are immutable by default because only if you put a method set in it is that it happens to be changeable indeed. Ok, but this is equal to the class.

It is a fact that record was created to facilitate the use of immutable structures, but it does not need to be so. Nothing different has in it that requires being immutable. And class can do exactly the same.

Now there is a new way to manipulate the property that is the init. But this is not a resource of record, is something new that works with the class as well. This is a mechanism that encourages immutability, at least public, since it lets put an initial value, but no longer mess with the value after that.

Nothing prevents the field from being changed internally differently. To be totally unchangeable you still have to make the field be readonly. And the init works well with this, in class or in the register.

There is a situation that record is immutable by default:

public record Pessoa(string Nome, int Idade);

In the very simple form of syntax record this behavior was adopted, so it can only touch the value during the initialization of the object, and as we see there are no other parts that move internally. In practice it creates a property for you with the init.

In this form is not that immutability is the standard, it is the only possible way, at least in the members described in this syntax called Primary constructor, nothing prevents having other fields or properties and they are changeable, which will make the structure as a changeable whole.

Note that immutability is shallow, that is, only in the basic object of each property, the objects that make up this object do not become immutable automatically, it is their problem to take care of it if it is the case. It has not changed in relation to what already happened with the class.

Builder

To be clear, support for immutable objects already occurred with classes. There has been a simplification of the way to do this and one of those simplifications is to no longer need to create a constructor to do the initialization, but this is a Feature of init and not of record.

I’m afraid people start ignoring constructor, which may be ignored in some cases when you have the init, but not always should. See more in What good is a builder?.

When what you want is the object to be immutable, the record may be a preference, but it’s not just that and this issue is collateral.

Real reason to use a record

Simplification is a slightly stronger reason, but still not the strongest of all to use the record.

Guys with value semantics tend to be immutable, it is true, but it is not obligatory that they are.

When you wanted semantics by value you could use classes, the biggest example is string. Then the record gave nothing new to the language.

Types with semantics by value and mainly immutable could already be adopted with struct and more clearly and guaranteed with readonly struct.

The record It should be compared to a simple structure, because that’s what it is. You shouldn’t compare it to a class. When you need a class, use a class.

The biggest reason to adopt a struct, and the same goes for the record is to have a simple data structure. These types composition forms have as their center the use of dice and not of a object complex.

Read In programming, what is an object?. And forget that a little bit. Here I am using the term object by the definition of object orientation. An object is a complex component where you have something cohesive and you access behaviors as if it were a single thing. Data needed for all this to work is not important to the programmer, it’s all implementation detail, the object is what matters. For all this to function mutability is important and it is customary to have several methods that manipulate this object. Behaviors tend to be very different between objects.

In a struct the focus is the die, so it is often said that the use of primitive types or simple types defined by the user (programmer) is a violation of object orientation. How nice.

One struct is a simple datum, not a complex composition. The datum is what interests you, not a cohesive behavior on top of something complex. It always has semantics by value and in fact it is usually by value, except if you use a ref struct.

A record serves for the same thing, to be a structure focused on the data, with semantics by value, probably in an immutable way. No wonder initially he would call himself data class, that is, it would be a data class, and not a class of an object, which is the normal.

So why use one record in place of a struct if both serve the same purpose?

When you need all this already described but need unlink life time with the creator of this data object, then the record is recommended. A record is always a type by reference, although the default semantics is by value, and it is always stored in the heap, unless it changes one day. Being in the heap life time is controlled by having a reference to it, but no matter where it comes from. It does not depend on the method that created this object to be running, or on an object that initially adopted it in a field of still existing, it only depends on some reference to it to exist.

So the mechanism is a class. In fact everything you can do with a class can do with it. And everything you do with it can be done with a class, always could.

Okay, I get why you adopted him, but then why did you invent it if you could do it before?

To facilitate use with value semantics. And also to make it clearer that you have a data object and not an object in the direction of object orientation. For some time I’ve been saying that C# wants to move away from the idea of OOP (without abandoning). This form encourages a more functional use of language (without adopting altogether).

And how it makes it easier?

It automatically provides methods that are usually needed on an object with semantics by value, which is what happens when you create a struct, but it doesn’t happen when you create a class. Even most of the classes that people create are wrong because they are incomplete, even if they work because you never end up using what is not done, or "tidy up" the situation to get around the lack of the mechanism done correctly. So always put:

Fiat 147 todo detonado andando pelas ruas

Then understand the record as a ref struct which is in the heap and is managed by Garbage Collector. If you don’t know, the ref struct can only be in the stack, it cannot be part of an object in the heap (to struct can).

That’s the decision you have to make.

So when I think about creating a class I don’t even have to think about whether I should be a record?

Not exactly. The problem is that people are addicted to creating classes when, in some cases, they should actually create struct, or ref struct. And I even had a reason to adopt a class when it’s right to create a form of struct. Without a mechanism that allows life time to be disconnected with the person who created the object, especially ref struct only by being able to use within methods, the only solution that existed was the class. But it was wrong, although it worked well. Now it has the proper mechanism. That is the real reason to exist and adopt a record.

Although it is possible it is inappropriate to use reference semantics in a record of the C#.

More information on the record

Wikipedia gives a very reasonable definition of what a record. Universally a record does not differ from what it already knows, it is only a Plain Old Data with value semantics. There are no more details about how they should be implemented.

To get an idea, what we call struct in C# is called record in Pascal/Delphi. Some people call struct of record. Conceptually don’t compare it to a class, although it is one in C#, but it is implementation detail.

The idea of being a reference is something that served the implementation of C# well since the language already had a form of record totally by value. But semantics by value is maintained to still be a record.

The C# compiler provides suitable implementations of some methods that all objects have, among them the equality method and comparison operators, the GetHashCode(), ToString() and forms copy and clone the object. In some circumstances may provide a Deconstruct(), the getters and initializers or other methods (such as PrintMembers() and the implementation can evolve and have others). The constructor is also created allowing the initialization of the data. So he does even a little bit more than the compiler does for a [ref] struct. And he knows how to do it the right way, the program doesn’t always know. This is an important difference for the class.

The record does not cease to be a class, so it has inheritance, for example, and that would be a good reason to adopt it when you need this language resource that is not available for a [ref] struct. Just be careful because inheritance in types with semantics by value can be a little complicated in some cases.

Important to say that these compiler-created methods can be replaced by a version of you if it suits you better and knows what you’re doing. And it can have other additional methods, like a class, just seems a little abuse. The record exists mainly to meet demands of anemic domain.

I still don’t have fully formed opinion about using a record as just an immutable class, to facilitate competition for example, because the class serves that as well. But I think people will use it because it’s easier than class and because they can’t create the right methods in class. Many people will believe that it is only an immutable class because it already has a lot of second-class material spread over the internet saying this, first-class people don’t make that mistake.

Completion.

Use records to:

  • Dtos and anemic domains
  • Simple guys, like a struct, that need to have indefinite life time
  • Types focusing on data (not behavior), especially when there is a relationship between them but it is not a single object, in a way as if it were a named tuple
  • Immutable classes that will be used in competitive contexts and you can handle it.

One problem I see is that there is incentive in the examples to adopt classes that are clearly changeable as immutable. This is the case with person which, by definition, changes its state throughout life, in addition to having various behaviors in most scenarios, and so a class has always been very suitable.

In a situation that needs to remain unchanged to facilitate competition a record can be adopted, but when you need to change one data on that object, another is created, but it’s the same person, you have to deal with it. It’s the same problem of microservices, you get inconsistencies if you don’t synchronize objects that are different with altered states but represent the same thing, and it’s a more difficult problem to solve. Note that not always this is complicated, then it is ok to use.

Immutable objects tend to be less efficient in various situations where they would have to make a mutation requiring copying, but may be better in others where the domain really is immutable. Do not force something to be immutable if it is not or will not be used in an environment that needs to be concurrent.

Use where it helps you and does not create complication.

See the documentation. And note that F# already had a similar mechanism because it has always favored functional style over object-oriented. And see a good use tutorial.

6

The idea of the guy record is to support immutable entities, that is, a type of reference that will maintain its value throughout the context that is inserted.

To use an immutable class you have to meet a series of points, as all properties only have getters. In the case of record the idea is to have it all without verification implementations.

Just not. :(

There’s a Open on. NET Github exactly speaking objects of the type record do not present compile errors when its properties are assigned after construction. The contour solution is the use of the also new properties feature Init-Only.

Let’s assume the following record:

public record Pessoa
{
    public string Nome { get; set; }
    public int Idade { get; set; }
}

In essence the type, the following code would not compile:

var pessoa = new Pessoa();
pessoa.Nome = "Rogerson Nazario"; //Porém compila!

In order for there to be an error in the previous comment line, the record should be as follows:

public record Pessoa
{
    public string Nome { get; init; }
    public int Idade { get; init; }
}

In the official website of Microsoft there is a technical reference on more functionalities of record.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.