In the previous C# tutorial I mentioned the new Init-Only Properties in C# 9. I'll be using the new init accessor in this tutorial as well with the new record type in C# 9.

Record Types are Not Immutable by Default

Before we dive into the cool new record type features, however, I want to mention that most of the C# tutorials are focusing on the record type in terms of immutability. Although it's true that record types are intended to be immutable with init-only properties, record types are not immutable by default. You can easily create a mutable record type.

record Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var person = new Person
        {
            Name = "John Doe",
            Age = 18
        };

        person.Name = "Jane Doe";

        Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
    }
}

// Output
Name: Jane Doe, Age: 18

As I discussed in the previous tutorial on init-only properties in C# 9, we can make the record type immutable by swapping out the set accessor on the properties with the new init accessor. This allows us to still use the object initializer to create a new instance of the record type while making it immutable. Using similar code as above, we will now get a compiler error telling us that we can no longer assign a value of "Jane Doe" to the Name property.

record Person
{
    public string Name { get; init; }
    public int Age { get; init; }
}

class Program
{
    static void Main(string[] args)
    {
        var person = new Person
        {
            Name = "John Doe",
            Age = 18
        };

        person.Name = "Jane Doe";  // Error CS8852

        Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
    }
}

The main point to walk away with here is that record types are not immutable by default. Record types can be made immutable using the same conventions we have always used in addition to the new C# 9 init-only properties.

Positional Records

I wanted to mention positional records in C# 9 first, because I think it's incredibly handy when it comes to creating immutable record types with a few values. Rather than writing the immutable record type as above with two init-only properties, we can simplify it greatly by adding the properties as arguments to the constructor and the C# compiler will automatically generate two init-only properties for us.

record Person(string Name, int Age) { }

class Program
{
    static void Main(string[] args)
    {
        var person = new Person("John Doe", 18);

        Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
    }
}

// Output
Name: John Doe, Age: 18

Notice that we're still using the Name and Age properties in the WriteLine statement, but they're not declared in the record type. Magic!

Record Type Overrides ToString Method

Let's reduce the C# code even more, because the record type overrides the ToString method and prints the properties by default.

record Person(string Name, int Age) { }

class Program
{
    static void Main(string[] args)
    {
        var person = new Person("John Doe", 18);

        Console.WriteLine(person.ToString());
    }
}

// Output
Person { Name = John Doe, Age = 18 }

Record uses Value-based Equality

The new record type doesn't just override the ToString method, it also overrides the Equals method. Although a record and class are still both reference types, the new record type uses it's property values to determine if records are equal and not if they are pointing to the same memory location / instance. This is similar to a struct, which is a value type in C#.

Therefore, two record types with the same property values are equivalent, regardless if they point to the same instance or not.

record Person(string Name, int Age) { }

class Program
{
    static void Main(string[] args)
    {
        var person = new Person("John Doe", 18);

        var person2 = new Person("John Doe", 18);

        if (person == person2)
           Console.WriteLine("They are equivalent.");
    }
}

// Output
They are equivalent.

If you want to know if the two records are the same instance, you can use ReferenceEquals.

Record Type and With-expression

As mentioned earlier, the new record type is really intended to be immutable. There will be times when you want to create a new record based on an existing record with some minor changes. This can be done using a with-expression in C# 9.

record Person(string Name, int Age) { }

class Program
{
    static void Main(string[] args)
    {
        var person = new Person("John Doe", 18);

        var person2 = person with { Name = "Jane Doe" };

        Console.WriteLine(person2.ToString());
    }
}

// Output
Person { Name = Jane Doe, Age = 18 }

Conclusion

The new record type and init-only properties in C# 9 are very handy for creating immutable types. Init-only properties allow us to create immutable record types using object initialization. Using positional records we can completely eliminate init-only properties from the record declaration, because the C# compiler will automatically create the properties for us.

The new record type in C# 9 overrides both the virtual ToString and Equals methods on the Object base class. Calling ToString on a record type will display all its properties. Since record types use value-based equality like a struct, record types are equal when their properties are equal.

And finally, a with-expression allows one to create a new instances of a record based on an existing record.