Wednesday, October 14, 2020

C# 9.0: Records

C# 9.0 is almost around the corner, we are just less than a month away from .NET Conf 2020, where C# 9.0 along with .NET 5.0 and Visual Studio 16.8.0 will be officially released. 

One of the biggest features coming with C# 9.0 is Records. I think it's high time we have a look at what Records are.

You can try out this feature even now with the latest Preview of Visual Studio 2019 (Version 16.8.0 Preview 4 as of today) and .NET 5.0 RC (SDK 5.0.100-rc.2 as of today).

In this post, let's have an overview of what Records are. But before diving in, let's consider the following example using C# 8.

Consider, you have a Person class with just 2 properties, FirstName and LastName.

var person = new Person("John""Doe");

So to be able to do this, I need to create a Person class with 2 properties and then set values to them through the constructor, something like below.

public class Person
{
    public string FirstName { getset; }
    public string LastName { getset; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

Next, say, I am creating another Person with the same FirstName, LastName.

var otherPerson = new Person("John""Joe");

There are often times, I need to compare whether it's the same person. I don't care about the references, I am only concerned about its values. Basically something like this,

bool isTheSamePerson = person == otherPerson;

And for us to be able to something like this, we need to modify Person class to implement IEquatable<T> and override operators.

public class Person : IEquatable<Person>
{
    public string FirstName { get; }
    public string LastName { get; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public bool Equals(Person other) => other is object && FirstName == other.FirstName && LastName == other.LastName;

    public static bool operator ==(Person left, Person right) => left is object ? left.Equals(right) : left is null;

    public static bool operator !=(Person left, Person right) => !(left == right);

    public override bool Equals(object obj) => Equals(obj as Person);

    public override int GetHashCode() => HashCode.Combine(FirstName, LastName);
}

Now say, you want to deconstruct the Person (part of C# 7.0), that's something like this,

(string firstName, string lastName) = person;

To be able to do this, you need to add a Deconstruct method to Person class.

public void Deconstruct(out string firstName, out string lastName)
{
    firstName = FirstName;
    lastName = LastName;
}

And finally, say we want to override Person.ToString() to return $"{FirstName} {LastName}". Yes, that means another method.

public override string ToString() => $"{FirstName} {LastName}";

Suddenly you can see our small class has grown and these functionalities are pretty much we are going need in most of the cases. So our C# 8.0 class is going to look like below with all the above functionality.

public class Person : IEquatable<Person>
{
    public string FirstName { get; }
    public string LastName { get; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = FirstName;
        lastName = LastName;
    }

    public bool Equals(Person other) => other is object && FirstName == other.FirstName && LastName == other.LastName;

    public static bool operator ==(Person left, Person right) => left is object ? left.Equals(right) : left is null;

    public static bool operator !=(Person left, Person right) => !(left == right);

    public override bool Equals(object obj) => Equals(obj as Person);

    public override int GetHashCode() => HashCode.Combine(FirstName, LastName);

    public override string ToString() => $"{FirstName} {LastName}";
}

What if we can simplify all this code. Enter Records.

Records

With C# 9.0, you can have all the above functionality available by default by creating a Record of Person instead of a Class of Person.

public record Person(string FirstName, string LastName);

You should see something strange here, we can specify parameters at the record definition level. This form is called positional records. You can still use the traditional syntax, which is defining separate properties and a constructor, etc.

So basically, I can do something like this and I will get a below output.

using System;

var person = new Person("John""Doe");
var otherPerson = new Person("John""Doe");

// Value Comparision
Console.WriteLine($"Value Comparison: {person == otherPerson}");

// Reference Comparision
Console.WriteLine($"Reference Comparison: {ReferenceEquals(personotherPerson)}");

// Deconstruction
(string firstNamestring lastName) = person;
Console.WriteLine($"Deconstuct: {firstName} {lastName}");

// ToString()
Console.WriteLine($"ToString(): {person}");

public record Person(string FirstNamestring LastName);

Output:

Value Comparison: True
Reference Comparison: False
Deconstuct: John Doe
ToString(): Person { FirstName = John, LastName = Doe }

So what is this new type, Record? 

Records are reference types and are immutable by default. There is a nice read here What's new in C# 9.0: Record types, I highly recommend you to read that for a more detailed explanation of records and its internal implementation.

Another nice thing with Records is, records support with-expressions. It's something like this,

var someOtherPerson = person with { FirstName = "Jane" };

So this will create a new object someOtherPerson, and all the properties of person will be shallow copied, but the specified properties will be changed.  I can verify that by printing someOtherPerson,

Console.WriteLine(someOtherPerson);
// Person { FirstName = Jane, LastName = Doe }

If you try to set values to FirstName/LastName properties without using the object initializer, we can see something interesting here.

init accessor
Here we can see a new accessor called init. It's another feature coming with C# 9.0. Properties that have init accessor can be set only in the object initializer or in the instance constructor. So when a record is defined using positional record syntax, all its arguments will have init accessor.

Let's modify the person record a bit by adding another property.

public record Person(string FirstNamestring LastName)
{
    public int Age { getset; }
}

So if we try to set property values,

someOtherPerson.LastName = "Smith"// this is NOT ALLOWED, because it has init accessor
someOtherPerson.Age = 20; // this is allowed

Hope that's enough to get you started on exploring more on records.

If you haven't saved the date of .NET Conf 2020, just spend like a minute and do save the date right now.

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment