30 Days Of .NET 6 - Day 19 - Record Structs
[.NET, C#, 30 Days Of .NET 6]
Today’s item, record structs
, is a bit more elaborate to explain, but I shall do my best.
If you want to combine multiple types into a more complex type there are several ways to do this in .NET:
Note: In my sample code I have created multiple namespaces to keep from having to declare multiple types with different names.
I differentiate them in the main program like this:
using AnimalClass = Animal.Class.Animal;
using AnimalStruct = Animal.Struct.Animal;
using AnimalRecordStruct = Animal.RecordStruct.Animal;
using AnimalRecord = Animal.Record.Animal;
using ReadOnlyAnimalRecordStruct = Animal.ReadOnlyRecordStruct.Animal;
Classes
This is the most common way - you declare and name a class
and go on to declare its constituent types.
public class Animal
{
public string Name { get; set; }
public byte Legs { get; set; }
}
Here we have declared an Animal
class with two public properties that can be changed. This is a mutable class. You can control the mutability using the appropriate modifiers: init
, private set
; or not have a setter at all
Structs
A struct
is syntactically almost identical to a class
.
public struct Animal
{
public string Name { get; set; }
public byte Legs { get; set; }
}
Which begs the question what is the difference between a class
and a struct
?
There are a whole bunch of differences but the main ones are these:
First: A struct
is a value type. This essentially means if you assign an instance of a struct
to a variable, all the properties of the struct
are copied to this new variable. This means if you change the original struct
, the new one is unaffected.
This is unlike a class
, which is a reference type. This means if you copy an instance of a class
to a variable, what is actually copied is a pointer/reference to the original class
, so if you modify either, changes will reflect on both.
This is best explained with some code:
Here we create one class, assign it to a variable, print some properties, and then modify the original. We then print the properties again.
var firstClass = new AnimalClass() { Name = "Dog", Legs = 4 };
var secondClass = firstClass;
Console.WriteLine($"First class name is {firstClass.Name} and legs is {firstClass.Legs}");
Console.WriteLine($"Second class name is {secondClass.Name} and legs is {secondClass.Legs}");
firstClass.Name = "Cat";
Console.WriteLine($"First class name is {firstClass.Name} and legs is {firstClass.Legs}");
Console.WriteLine($"Second class name is {secondClass.Name} and legs is {secondClass.Legs}");
The results look like this:
First class name is Dog and legs is 4
Second class name is Dog and legs is 4
First class name is Cat and legs is 4
Second class name is Cat and legs is 4
Of interest here is that we only changed the firstClass
name, but in the output the second class name has changed as well to Cat
.
This underscores the point - firstClass
and secondClass
, despite being different variables, refer to the same thing.
If we change to a struct
, the code looks like this:
var firstStruct = new AnimalStruct() { Name = "Dog", Legs = 4 };
var secondStruct = firstStruct;
Console.WriteLine($"First struct name is {firstStruct.Name} and legs is {firstStruct.Legs}");
Console.WriteLine($"Second struct name is {secondStruct.Name} and legs is {secondStruct.Legs}");
firstStruct.Name = "Cat";
Console.WriteLine($"First struct name is {firstStruct.Name} and legs is {firstStruct.Legs}");
Console.WriteLine($"Second struct name is {secondStruct.Name} and legs is {secondStruct.Legs}");
The output should look like this:
First struct name is Dog and legs is 4
Second struct name is Dog and legs is 4
First struct name is Cat and legs is 4
Second struct name is Dog and legs is 4
Note here that the second struct
name remains dog
, even after we change the name of the first.
Second: A class
is allocated on the stack, and is garbage collected, while a struct
is allocated on the heap.
You can read a good explanation of this here
There are some other differences, and you can read about them here:
- How to choose between using a struct and a class (Microsoft)
- Differences between a struct and a class (C-Sharp Corner)
The main gist is generally, use a class.
In addition to class
and struct
, there are some additional structures:
Tuple
In .NET Framework 4 a new type was introduced to allow developers to quickly couple a bunch of types together to address the very common use case of returning multiple types from a method without having to create a class
(or a struct
) to package them together - System.Tuple.
It works like this:
var animalTuple = Tuple.Create("Dog", 4);
Console.WriteLine($"Animal Tuple name is {animalTuple.Item1} and legs is {animalTuple.Item2}");
This should print the following:
Animal Tuple name is Dog and legs is 4
What the compiler does here is create an object with as many types properties as there are items passed into the static Create method.
You can also create a Tuple
directly like this:
var animalTuple2 = new Tuple<string, int>("Dog", 4);
The properties are named Item{n}
, where n
is from 1 to 8. Item8
, by the way, is ANOTHER tuple, so if if you wanted to pass for example 8 items, you must create Item8
as another Tuple
with 1 item.
In other words, the element in Item7
is accessed like this:
tuple.Item7
The element in Item8
, however, is accessed like this:
tuple.Item8.Item1
This is because Item8
is a Tuple
.
This allows you to return more than 8 items - just keep nesting the 8th Tuple
.
Now if you are returning more than 8 items you are probably better off reexamining your design and should consider creating an object directly.
The Item{n}
properties are strongly typed - in our example above Item1
is a string
and Item2
is an int
.
The problem with this construct is it is a nightmare to use and maintain, You literally have no clue what Item6
represents without having to go and look at the code that creates it.
This led to the creation of…
ValueTuple
The ValueTuple was introduced in .NET Framework 4.7 and .NET Core 1.
It is essentially and improvement of the System.Tuple
.
You use it like this:
var animalValueTuple = (Name: "Dog", Legs: 4);
Console.WriteLine($"Animal Value Tuple name is {animalValueTuple.Name} and legs is {animalValueTuple.Legs}");
This should print the following:
Animal Value Tuple name is Dog and legs is 4
A couple of things to note:
- In addition to providing the value, you also provide a name that the compiler will use to create the appropriate property name
- Unlike the
Tuple
that provides access to the types using properties, theValueTuple
provides them as fields - As a result of the above,
ValueTuple
values are mutable, unlikeTuple
values that cannot be changed as they are read-only properties.
You can convert a ValueTuple
to a Tuple
by using the ToTuple extension method:
var converted = animalValueTuple.ToTuple();
Console.WriteLine($"Converted Animal Value Tuple name is {converted.Item1} and legs is {converted.Item2}");
Of course doing this you lose the property names, which are converted to Item{n}
.
If you really want to modify a Tuple
properties, you can convert it to ValueTuple
using the ToValueTuple extension method:
var convertedValueTuple = animalTuple.ToValueTuple();
What happens here is the Item1
and Item2
are converted from readonly
properties to mutable public fields.
So far those are four techniques of packaging types.
.NET 5 introduced a new one:
Records
The rationale behind records is that there is a considerable use case requiring a lightweight type for packaging data, that had sensible default behaviour to for this purpose.
This is a very common case for distributed systems where you have data access objects (no behaviour, immutable) that are used to pass around data.
This is best illustrated using an example:
First, you declare the record type:
public record Animal(string Name, int Legs);
Note the keyword record
Next you have your logic:
var dog = new Animal("Dog", 4);
var otherDog = new Animal("Dog", 4);
Console.WriteLine($"The animal record name is {dog.Name} and legs are {dog.Legs}");
This looks so far pretty much like a class
.
The difference is the out of the box features that you get:
Immutability
Out of the box, the properties are read only, and therefore the record is immutable. You can achieve this using classes but it is a lot more work writing a class that is immutable.
Logical Comparisons
The following code will return true
:
var dog = new Animal("Dog", 4);
var otherDog = new Animal("Dog", 4);
Console.WriteLine(dog == otherDog);
This will not be the case with a class
or a struct
, which consider them to be different. If you want the comparison to work you must override the ==
operator as well as (potentially) the .Equals method.
This is not practical for a project with several classes.
The logic here is two records whose properties have the same values is logically the same thing.
ToString()
You get a built in, pretty good .ToString()
implementation that actually prints the object values.
If you run this:
Console.WriteLine(dog.ToString());
It will print this:
Animal { Name = Dog, Legs = 4 }
Nondestructive mutation
If you really want to mutate a record, you can’t do it directly but you can do it by quickly creating a copy and changing the value that you want.
For example:
var amputatedDog = dog with { Legs = 3 };
Console.WriteLine($"The amputated dog record name is {amputatedDog.Name} and legs are {amputatedDog.Legs}");
Here we copy the dog
object as is, but change the Legs
property to 3.
This is also useful to quickly create copies of records for other uses.
Outright Mutation
Although records are generally meant to be immutable, you can in fact create mutable records.
This is done by using an alternative syntax to declare your record
, which looks a lot like that of a class
.
public record Cat
{
public string Name { get; set; }
public int Legs { get; set; }
}
The cat record here is fully mutable.
Init-Only Initialization
The default record implementation means you create objects like this:
var dog = new Animal("Dog", 4);
This is a bit difficult to read. What does that 4 stand for?
You can of course make it easier by doing this:
var dog = new Animal(Name: "Dog", Legs: 4);
This is better. The trouble with this is there is no way to enforce this in your code.
You can work around this by declaring your record like this:
public record Cat
{
public string Name { get; init; }
public int Legs { get; init; }
}
Once we have done this we create a cat
like this:
var cat = new Cat() { Name = "Lion", Legs = 4 };
Here it is very explicit what the property values being set are.
The init
keyword means that the property can only be set once.
Which brings us to the sixth technique of packaging types:
Record Structs
Unbeknownst to many, a record
is actually a class
under the hood.
We can demonstrate this using SharpLab, a very handy tool that allows you to see what the C# compiler is doing behind the scene to generate code.
Among the considerable code the compiler generates to support the implementation of records
, of note is the fact that ultimately a record
is a class
.
In .NET 6 (C# 10) you can now use record structs
.
The syntax of a record struct
is pretty much that of a normal record
. Just add the keyword struct
.
namespace Animal.RecordStruct
{
public record struct Animal(string Name, int Age);
}
If we consult SharpLab.io to look at the generate code, we see the following:
Here we see that the generated type is actually a struct.
The code is also less (121 vs 187 lines).
But what is the difference between a record struct
and a record class
?
The most important distinction is the name - a record struct
is a struct
, and a record class
is a class
.
This means the constraints/benefits of classes vs structs apply
More nuanced is the following:
Record classes Are Immutable. Record structs are not
Using our declaration above of a record struct, we can do the following:
var mouse = new AnimalRecordStruct("Mouse", 4);
mouse.Name = "Mongoose";
The equivalent code for a record class
will not compile:
var rat = new AnimalRecord("Rat", 4);
rat.Name = "Mongoose";
You will get the following error:
This is because for record classes
, the properties are immutable.
Aside from this, most of the other features behave the same:
- Equality comparisons
ToString()
with
copy semantics- Initializers
The question arises - what if you want an immutable record struct
?
To achieve this you add the readonly
keyword to the deceleration:
namespace Animal.ReadOnlyRecordStruct
{
public readonly record struct Animal(string Name, int Age);
}
This code will not compile, as the properties now are readonly
var rabbit = new ReadOnlyAnimalRecordStruct("Rabbit", 4);
rabbit.Name = "Mongoose";
Record structs should offer better performance
Record structs
by virtue of being structs
should offer better performance in most use cases.
Thoughts
The multiplicity of options is likely going to cause a lot of confusion for developers:
Classes
with properties that haveinit
modifiers, which will be immutable, likerecord classes
.Structs
with properties that haveinit
modifiers, which will be immutable, likerecord structs
.- Normal mutable
classes
- Normal mutable
structs
Record structs
Record classes
Readonly record structs
However, with a sufficient understanding of record classes
and record structs
as well as knowledge of your use cases in the problem domain, it should be clear when to use which.
The benefits of record structs
and readonly record structs
in terms of communicating intent should make it easier to write and understand high performance, distributed systems.
The code is in my Github
TLDR
.NET 6 had introduced a record struct
type, that brings record
semantics to structs
similar to record
classes.
This is Day 19 of the 30 Days Of .NET 6 where every day I will attempt to explain one new / improved thing in the upcoming release of .NET 6.
Happy hacking!