Records have been added in C# 9 and 10.
They have already become a mainstay, that much is clear.
So, are you using records in your designs, then? Do you need any clarifications?
Alright, stay with me, and I will clarify the use of records in C#.
I promise, before this video ends, you will know your records inside and out.
You will know how they work and how to utilize them in your object-oriented
and functional designs. Let's get straight to code.
What does this line of code say? Quite formally, it defines an immutable class with
two read-only properties, FirstName and LastName, and with value-typed semantics.
You can use this class whenever a value object is needed.
And that is the whole story. If you know what I just said, then you can go
and watch other videos on my channel because that is all there is in records, seriously.
But if that still puzzles you, and you want to see what are the internals of
this record, then stay with me. I will now implement this same record by hand.
It is a class like any other. The compiler will synthesize two
properties from the primary constructor. That is what it does when it
sees the record declaration. The properties will be read-only,
but with init-only setters on them. You can also use primary constructors on
your custom classes starting with C# 12. There is no magic in them.
It is a constructor like any other, with a few twists you might want to be aware of.
Watch my other video explaining how you can use primary constructors correctly, while avoiding
the possibility of making bugs in that way. There's a link in the description as well.
So, the record has a constructor that initializes the two mandatory read-only public properties.
It also supports the so-called non-destructive mutation.
To change an instance, you will create a new instance and
list the properties you want to change. Records support non-destructive
mutation via the with expressions and those require a copy constructor.
So, you can do the same thing. You can also support
non-destructive mutation in your custom classes by supplying a copy constructor.
Then, create a new instance from this one, set the desired properties to new values, and that's it.
That is non-destructive mutation in a custom class.
Records also support deconstruction. What you see in the primary constructor
are the so-called positional properties. You can assign a record to a tuple literal,
consisting of variables in the same positional order as in the primary constructor.
And you can use discard if you want to ignore some components.
A custom class can define a special Deconstruct method to achieve the same effect.
Its work is opposite to the constructor's work. It's filling out the output variables this time.
With this, we have covered immutability, construction, and deconstruction of records.
Records also implement equivalence. They implement the strongly typed IEquatable
interface for performance reasons. The general Equals just forwards the call.
Everything is so straightforward at this point. But the story does not end here.
If you forgot equality and inequality operator overloads, you
have just added a bug to your class. Fortunately, the compiler will never forget
to synthesize these two operators as well. Last but not least, the compiler will also
synthesize a convenient ToString overload. Look what I have got - about
20 lines of code and it is all condensed into a single-line declaration.
I have coded hundreds of value objects in my past projects and now that we have got records
in C# I see no point in doing that ever again. But this is far from all.
Oh, records are much more than this one line. A record is a class.
Therefore, it can derive from another record. Can you imagine the complexity of changes
this modification has made under the hood? From the outside, instantiating one
or the other record looks the same. The difference is in the code the compiler
has synthesized under the declaration. If I gave you the task to implement these
two classes, one deriving from the other, still maintaining value-typed semantics, would
you be able to do that without introducing bugs? Watch and learn.
The derived class will delegate part of the construction to its base.
The compiler will synthesize a special virtual property that returns the type
within which the objects can be equal. The two objects are equal only if their types
are equal and their components are equal. Did you know that?
This is the only valid implementation of Equals. The rest of the base class
will be straightforward. One last complication will come
when converting objects to strings. The compiler will synthesize a protected
virtual method that appends the object's components at the back of a StringBuilder.
There's a lot of fine logic here, as you can witness.
Have you ever developed this kind of logic in your value objects?
Well, compilers are not picky. They do it all, so every record you declare
in C# will have all this logic implemented. I didn't count the lines, but it looks like there
are some 50 of them in my custom implementation. The variant based on records
consists of two lines of code. I will remove my custom implementations
and not look back on them anymore. If you wish to read the source code of
this demo, you can visit my Patreon page. Join the growing community on Patreon and
get access to source code of this video and all other videos on my channel.
You can also join the Discord server associated with this channel and participate in
discussions, ask questions about C# and .NET. You're welcome.
You can find the links in the description. From now on it, will be
records all the way through. It is not a good practice to
derive classes from one another. A much better design is to define
an abstract empty record and then derive sealed records for all the variants.
Did I just complete a major refactoring in just a few keystrokes?
Oh, yes I did! That is as close to functional discriminated
unions as you can do in C# today. You can watch another video I have
made about discriminated unions in C#. There's also the link in the description.
Besides positional properties, you can add a custom property of your choice.
This property will obviously be optional when constructing a record and it has a default value.
You can use object initializer syntax to set it during construction.
You can change that property in the with expression, but you
cannot access it in deconstruction. It is allowed to redefine the positional property.
That lets you change its visibility, make it mutable, or add validation.
Use the class parameter to initialize the custom property.
You can even override the printout if you must. Not that I see much value
in this, but you can do it. You can see that the printout
is now more condensed than it would have been without my intervention.
There are two more topics I want to cover before ending this video.
One is record structs. It is usual to wrap a low-level
type, such as a GUID, or a string, to give it a strong type and the domain meaning.
This is the strongly-typed PersonId, the one you would much favor in a DDD project.
A record struct is a struct. It has no object header, it supports no virtual
functions, it supports no inheritance, and it is always copied by value.
Keep them small. The record structs are also
much faster than traditional structs because they don't use reflection.
Their equality members are are synthesized. There's no faster way to compare
structs than what record structs do. Therefore, don't use traditional structs anymore.
Use record structs from now on. And you can do some exciting
things with record structs. What if I wanted to ensure that a string I
used is non-empty, non-whitespace, always? First of all, properties in
record structs are mutable. Remove the setter from positional
properties to make this record immutable. Use validation to restrict the contained value.
One last issue is that structs always have a parameterless constructor that zeroes-out their
content, it sets all components to defaults. So, you must make sure to reimplement the default
constructor if you wish to enforce validation. A touch of genius - making a non-empty
string assignable to an ordinary string. I like this.
I can use this non-empty string definition in all places I want to ensure that the proper string
has been assigned without further validation. That is the design principle often
quoted in functional programming: make invalid states unrepresentable.
You cannot even try to construct a Book object if you don't already have
a non-empty string as the title. Since C# 10, you're allowed to
write record class, not just record. Use this form if you find it more
intention-revealing, more readable, anything. One last note and this demo is over.
It is about the immutability of records. C# only supports so-called shallow immutability.
It will never modify a property on a record, but if that property is referencing
a mutable class, a mutable object, then it might happen that you mutate that object,
hence ruining the immutability of this record. It is therefore your job to ensure that any
record you plan to make immutable only consists of other immutable types and immutable records.
I was very careful to do that precisely in my design, so all the records I made
in this demo are deeply immutable. This completes the deep dive on record
classes and record structs in C#. Stay around, subscribe to my
channel, and watch other videos I have made about object-oriented
and functional programming in C#.