One of the highlighted language features that were introduced in C# version 9.0 (.NET 5.0) were records, or record classes. Records are the third way of declaring custom data types, the other two being classes and structures. But, before I explain them to you, let's analyze what led to their creation.
As we will learn later, there are numerous times when we will need to create a custom data type that will do nothing more than hold information. Example:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
using
System;
namespace
HelloWorld
{
public class
Student
{
public string Name { get; set; }
public string Surname { get; set; }
public int Age { get; set; }
}
public class
Program
{
static void Main(string[] args)
{
Student student = new Student();
student.Name = "Doe";
student.Surname = "John";
student.Age = 18;
Console.ReadLine();
}
}
}
|
If you look closely at the Student class, you notice that it is a "dumb" object, it contains no methods or functions (in other words, functionality), but just some properties that can store data, such as the name or age of a student. This kind of type is tipically referred to as a DTO (Data Transfer Object). Its only purpose is to hold data and transfer it between various parts of a program - instead of calling a function with three distinct parameters such as age, name and surname, we can "group" them together into a single student type, and pass that type as parameter. These kind of objects are often used with databases - the columns from a table should match the properties types and names in a DTO, such that each "row" from that database table can be stored in an instance of the DTO.
This approach works fine, but there are a few downsides to it:
- We need to create a new class for each of these types, and much of the code could be duplicate. This is even worse if we follow the guideline of placing each class into its own separate file
- The syntax is very verbose, with all the getters and setters of each property. Creating such objects is time consuming and non-essential
- The verbosity increases if we want to make the properties immutable (they can be initially set, but not modified afterwards), because we have to create a constructor with mandatory parameters and manually assign the parameters values to the read-only properties, initiallyzing them
Now, let's do the same thing, using records:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
using
System;
namespace
HelloWorld
{
public record
Student(string Name, string Surname, int Age);
class
Program
{
static void Main(string[] args)
{
Student student = new Student("Doe", "John", 18);
Console.ReadLine();
}
}
}
|
You have to agree that the syntax is much more concise and the boilerplate code is minimal. However, you should know that there are few slight differences between using a class and a record:
- Records are immutable by default. This will not compile:
1234567891011121314151617using System;namespace HelloWorld{public record Student(string Name, string Surname, int Age);class Program{static void Main(string[] args){Student student = new Student("Doe", "John", 18);student.Age = 20; // will cause an error, init-only properties cannot be changedConsole.ReadLine();}}}
- They implement equality out of the box. If I create two instances of Student, with the same values, they are considered equal, but they are not the same instance.
Records can be updated or cloned using the with keyword:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
using
System;
namespace
HelloWorld
{
public record
Student(string Name, string Surname, int Age);
class
Program
{
static void Main(string[] args)
{
Student student = new Student("Doe", "John", 18);
student = student with { Age = 20 }; // updates the value of Age on same record instance
var otherStudent = student with { }; // clones the record instance into a new one
Console.ReadLine();
}
}
}
|
By default, a record is treated like a class, being a reference type stored on the Heap memory. Since C# version 10.0, you can declare records as structures, which are value types and are stored in the Stack memory:
|
1
2
|
public record Student(string Name, string Surname, int Age); // record as class
public record struct Point(int X, int Y); // record as struct
|
Records can also have their properties explicitly defined:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
using
System;
namespace
HelloWorld
{
public record
Student
{
public string Name { get; set; }
public string Surname { get; set; }
public int Age { get; set; }
public Student(string Name, string Surname, int Age)
{
this.Name = Name;
this.Surname = Surname;
this.Age = Age;
}
}
public class
Program
{
static void Main(string[] args)
{
Student student = new Student("Doe", "John", 18);
student.Age = 20; // this is now permitted
Console.ReadLine();
}
}
}
|
In which case they look exactly like a class, and they are no longer immutable, because I manually specified a setter on the properties. One difference compared to a class is that we can still use the auto implemented equality.
It should be noted that immutability, even on basic records, is only applied on primitive, simple data types. If one of the properties of the record would be an instance of another class, the members of that class would still be modifiable:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
using
System;
namespace
HelloWorld
{
public class
Battery
{
public int Charge { get; set; }
}
public record
Phone(string Model, Battery Battery);
public class
Program
{
static void Main(string[] args)
{
Phone phone = new Phone("Nokia", new Battery() { Charge = 50 });
phone.Model = "Blackberry"; // this is NOT permitted, property Model is immutable
phone.Battery = new Battery(); // this is NOT permitted, property Battery is immutable
phone.Battery.Charge = 20; // this IS permitted, members of Battery are not immutable
Console.ReadLine();
}
}
}
|
Records support inheritance, of which we will learn in the future. The boxing and unboxing rules still apply, just in the case of classes:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
using
System;
namespace
HelloWorld
{
public record
Person(string Name, int Age);
public record
Employee(string Name, int Age, decimal Sallary) : Person(Name, Age);
public class
Program
{
static void Main(string[] args)
{
Person person = new Employee("John", 50, 2000);
bool isPerson = person is Person; // true
bool isEmployee = person is Employee; // still true
Console.ReadLine();
}
}
}
|
Opposed to inheritance, records can be marked as sealed, which means they cannot declare subtypes (cannot be inherited):
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
using
System;
namespace
HelloWorld
{
public sealed record
Person(string Name, int Age);
public record
Employee(string Name, int Age, decimal Sallary) : Person(Name, Age); // will not compile
public class
Program
{
static void Main(string[] args)
{
Console.ReadLine();
}
}
}
|
Although I haven't yet explained this concept either, records can also be marked as abstract, in which case they cannot be directly instantiated. They need to be implemented by concrete types, which can then be later instantiated:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
using
System;
namespace
HelloWorld
{
public abstract record
Person(string Name, int Age);
public record
Employee(int Age, decimal Sallary) : Person("John", Age);
public class
Program
{
static void Main(string[] args)
{
Person somePerson = new Person("John", 50); // not allowed, its abstract
Person otherPerson = new Employee(50, 2000); // allowed
bool isPerson = otherPerson is Person; // true
bool isEmployee = otherPerson is Employee; // still true
Console.ReadLine();
}
}
}
|
In the end, when should you use records? According to Microsoft itself, "For records, the fundamental use is storing data. For classes, the intended use should be defining responsabilities." In other words, when you have dumb objects, that just hold data, and especially when the flow of that data is unidirectional (as in getting data from a database, putting it in recods, and never changing it afterwards, but only deliver it to the parts of the program that need it) and the data should be immutable, use records, as much as possible. When you need logic and functionality, use classes.