>
>
>
What's new in C# 9: overview

Konstantin Volohovsky
Articles: 14

What's new in C# 9: overview

Although C# 9 came out over half a year ago, the community is still processing its immense list of changes and has yet to discover best practices for the new features. It's a great excuse to go over C# 9 features one more time.

Properties for initialization only

C# 9 got a new keyword - init. After an object is initialized, its properties that have the init keyword cannot be changed. Was something like this possible before? Yes, you could employ a constructor and do something similar - but using an initializer to do this wouldn't have worked.

public class PersonClass
{
    public string Name { get;}
    public string Surname { get; set; }
}

public static void Main()
{
    var person = new PersonClass() { Name = "Silver", Surname = "Chariot" };
    //Error CS0200
    //Property or indexer 'PersonClass.Name' cannot be assigned
    //to --it is read only
 }

Let's change the code and use init:

public class PersonClass
{
    public string Name { get; init; }
    public string Surname { get; init; }
}

public static void Main()
{
    var person = new PersonClass() { Name = "Silver", Surname = "Chariot" };
    //No error
    person.Name = "Hermit";
    //Error CS8852
    //Init-only property or indexer 'PersonClass.Name' can only be assigned
    //in an object initializer, or on 'this' or 'base'
    //in an instance constructor or an 'init' accessor.
}

Records

What are they?

One of the main new features in C# 9 is a new type - record. A record is a new reference type that you can create instead of classes or structures. To see how it's different from the other two, let's see what the new type can do.

Positional syntax

We can see new features from the very beginning. Of course, you can employ the old approach and define a record similarly to classes and structures. There is, however, a short way to do this:

public record PersonRecord(string Name, string Surname);

The construct expands as follows:

public record PersonRecord
{
    public string Name { get; init; }
    public string Surname { get; init; }

    public PersonRecord(string name, string surname)
    {
        Name = name;
        Surname = surname;
    }
    public void Deconstruct(out string name, out string surname)
    {
        name = Name;
        surname = Surname;
    }
}

Okay, the deconstructor is new, what else? Correct, instead of set, there's the init keyword I've mentioned earlier. Consequently, by default, records are immutable - and that's exactly the cases for which they are intended.

When you initialize variables, the deconstructor allows you to get the values of all parameters of the declared record:

var person = new PersonRecord("Silver", "Chariot");
var (name, surname) = person;

You won't be able to change this record:

person.Name = "Hermit";
//Error CS8852
//Init - only property or indexer 'PersonRecord.Name' can only be assigned
//in an object initializer, or on 'this' or 'base'
//in an instance constructor or an 'init'

In the previous examples, I did not define a body for the shortened form - but this can be done. Moreover, you can change property values, if you don't like the ones that have been created automatically:

public record PersonRecord(string Name, string Surname)
{
    public string Name { get; set; } = Name;
    public string Surname { get; set; } = Surname;
    public string GetFullName()
        => Name + " " + Surname;
}
public static void Main()
{
    var person = new PersonRecord("Hermit", "Purple");
    person.Name = "Silver";
    Console.WriteLine(person.GetFullName());
    //Silver Purple
}

Value equality

As we know, structures do not have overridden comparison operators. When comparing class instances, we do not compare data inside objects, but references to them. Now let's take a look at how this happens for records:

public record Person(string Name, string Surname);

public static void Main()
{
    var first = new Person("Hermit", "Purple");
    var second = new Person("Hermit", "Purple");
    Console.WriteLine(first == second);
    //true
}

Yes, that's right - the comparison is based on record field values. The "==" and "!=" operators and the Object.Equals(Object) method are overridden, so we do not need to worry about them.

The ToString method

Talking about overridden methods. ToString is also overridden. While for structures and classes this method returns their names, for records it also returns the contents:

var personRecord = new PersonRecord("Moody", "Blues");
var personStruct = new PersonStruct("Moody", "Blues");
var personClass = new PersonClass("Moody", "Blues");

Console.WriteLine(personRecord.ToString());
Console.WriteLine(personStruct.ToString());
Console.WriteLine(personClass.ToString());

//PersonRecord { Name = Moody, Surname = Blues }
//PersonStruct
//PersonClass

Inheritance

I haven't had a chance to mention that in IL code records are classes. Although this is true, it would be incorrect to say that they are the same. While records do support inheritance, you cannot inherit records from classes. However, records can implement interfaces.

There are a few interesting points about inheritance as related to records. Take a look at this example:

public record Person(string Name, string Surname);
public record PersonEnglish(string Name, string MiddleName, string Surname)
    : Person(Name, Surname);

public static void Main()
{
    var person = new Person("Tom", "Twain");
    var englishPerson = new PersonEnglish("Tom", "Finn", "Twain");

    Console.WriteLine(englishPerson);
    //PersonEnglish { Name = Tom, Surname = Twain, MiddleName = Finn }

    var (one, two, three) = englishPerson;
    Console.WriteLine(one + " " + two + " " + three);
    //Tom Finn Twain
}

Child records have the same overridden methods as their parents. However, unexpectedly, the order of property values, that the ToString method and the deconstructor return, differs. Do keep this in mind.

You can see another interesting thing when comparing records. Inspect the following code:

public record Person(string Name, string Surname);
public record Teacher(string Name, string Surname, int Grade)
    : Person(Name, Surname);
public record Student(string Name, string Surname, int Grade)
    : Person(Name, Surname);
public static void Main()
{
    Person teacher = new Teacher("Tom", "Twain", 3);
    Person student = new Student("Tom", "Twain", 3);
    Console.WriteLine(teacher == student);
    //false
    Student student2 = new Student("Tom", "Twain", 3);
    Console.WriteLine(student2 == student);
    ///true
}

In this example, all instances have the same set of properties and property values. Nonetheless, variables declared as Person produce false when compared, while comparing Person to Student yields true. This happens, because the comparison method takes the runtime type into account during comparison.

Reversible changes

You can use the with keyword to create record instances based on existing record instances. This feature allows you to change indicated property values by way of object initialization syntax:

var person = new Person("Tom", "Twain");
var another = person with { Name = "Finn" };

Console.WriteLine(another);
//Person { Name = Finn, Surname = Twain } 

var another2 = another with { };
Console.WriteLine(another == another2);
//true

In order for a property to be able to use the with keyword, this property needs to have the set or init access method, because, as we've already discussed, the initializer does not work without them.

Use cases

Why did developers of C# add the record type? As I've already said, records are assumed immutable. Consequently, they are best suited in scenarios that require an immutable data set (without methods). This includes functional programming where programs are sequences of pure functions and where immutability is very important.

Another obvious use case is the DTO pattern used in data exchange between an application's subsystems. This applies mostly to web programmers that need to pass data between an application's layers, for example, registration models, login, and others.

Top-level instructions

Good news: coding in C# is now even more enjoyable! At least for newbies and those who want to check something quickly. For example, to write an article about the new C#.

Thanks to top-level instructions, we no longer need to be tied to long namespace and class constructs and drag them around. This means, "Hello World" many not look like this anymore:

using System;

namespace TestApp
{
    class Program 
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

But rather like this:

using System;
Console.WriteLine("Hello World!");

Of course, you cannot write a real-world multifunctional application like this - only one file can have top-level instructions. But that was not the goal anyway. Another thing to keep in mind is, our Main method did not go anywhere. Moreover, this is where our code is executed. Because of this we cannot override the method - and can get access to args:

using System;
Console.WriteLine(args[0]);

static void Main(string[] args)
    //Warning CS7022: The entry point of the program is global code;
    //ignoring 'Main(string[])'{
    Console.WriteLine(args[1]);
}

Let's run the program in the console:

TestApp.exe Hello World!
Hello

Target typing

new()

Target typing refers to getting a variable's type from context. That's exactly what developers of C# 9 decided to improve.

The first thing we see is the new... new. Basically, the new syntax of new is var in reverse. If the variable's type is already known (for example, from the declaration), you can skip new when writing expressions:

ObservableCollection<string> collection = new();
Person person = new("Hermit", "Purple");

Unfortunately, C# cannot read thoughts so far, so it won't understand this expression:

var collection = new();
//Error CS8754 There is no target type for 'new()'

So a fair question remails - where do we use it? We already have the generally accepted var, and now we have two identical forms of short notation:

var collection = new ObservableCollection<string>();
ObservableCollection<string> collection = new();

For a program's code this new feature may seem excessive. However, there is one place where we have to indicate a type in declaration - class members. That's right, now you can decrease the amount of code inside a class's body. You don't need these anymore:

public Dictionary<int,List<string>> field = new Dictionary<int,List<string>>();

Here's what your code would look like in C# 9:

public class School
{
    ObservableCollection<Student> studentList = new();
    ObservableCollection<Teacher> teacherList = new();
}

The ?? and ?: operators

The ternary operator can now understand arguments of different types better, which is why we can do without explicit casts:

Person person = expr ? student : teacher;

In C# 9 early preview stages, developers of C# announced that the null-coalescing operator will be able to process different types that have the same base class. However, looks like this feature did not make it to the release:

Person person = student ?? teacher;
//Error CS0019
//Operator '??' cannot be applied to operands of type 'Student' and 'Teacher'

Covariant return type

In child classes, you can now override the return type. Of course, there is no dynamic typing - I am talking only about types linked by inheritance. This change is intended to make life easier when the "Factory method pattern" is involved. Here's an example: suppose, a video game has various types of merchants who sell goods of different types (i.e. goods that have different characteristics):

public abstract class Item
{
    ....
}
public class MagicItem : Item
{
    ....
}
public class WeaponItem : Item
{
    ....
}
public abstract class Merchant
{
    ....
    public abstract Item BuyItem();
}

public class MagicianMerchant : Merchant
{
    ....
    public override MagicItem BuyItem() { return new MagicItem(); }
}

public class WeaponMerchant : Merchant
{
    ....
    public override WeaponItem BuyItem() { return new WeaponItem(); }
}

public static void Main()
{
    var magician = new MagicianMerchant();
    var blacksmith = new WeaponMerchant();

    MagicItem boughtItem1 = magician.BuyItem();
    WeaponItem boughtItem2 = blacksmith.BuyItem();

}

In the code above, you can see that in C# 9 you can obtain the compile-time types straight from the corresponding methods, and get immediate access to their own fields and methods. Previously, it would have been necessary to do an explicit type cast:

MagicItem boughtItem1 = (MagicItem)magician.BuyItem();
WeaponItem boughtItem2 = (WeaponItem)blacksmith.BuyItem();

By the way, if Item were an interface, this feature would have worked. And it would not have worked if Merchant were an interface.

Static lambda expressions and anonymous functions

In C#, when anonymous functions refer to local variables, the compiler allocates memory for a temporary object. This is necessary, because an anonymous function can exist longer than the function that created it. Now you can define lambda expressions and anonymous functions as static so that they do not overtake surrounding context, thus preventing memory allocation:

double RequiredScore = 4.5;
var students = new List<Student>() 
{ 
    new Student("Hermit", "Purple", average: 4.8),
    new Student("Hierophant", "Green", average: 4.1),
    new Student("Silver", "Chariot", average: 4.6)
};

var highScoreStudents =
    students.Where(static x => x.AverageScore > RequiredScore);
//Error CS8820
//A static anonymous function cannot contain a reference to 'RequiredScore'

In this scenario, passing references to constants is possible:

const double RequiredScore = 4.5;
var students = new List<Student>() 
{ 
    new Student("Hermit", "Purple", average: 4.8),
    new Student("Hierophant", "Green", average: 4.1),
    new Student("Silver", "Chariot", average: 4.6)
};

var highScoreStudents =
    students.Where(static x => x.AverageScore > RequiredScore);
//No error

Discard for anonymous and lambda function parameters

Here I'll mention one more small enhancement. If we do not need parameters in an expression, you can leave an underscore in their place. For example, if we do not need sender and EventArgs, you can avoid the compiler's warning:

button1.Click += (_, _) => ShowNextWindow();

You can indicate type if you need to:

button1.Click += (object _, EventArgs _) => ShowNextWindow();

GetEnumerator extension support

Now foreach can recognize GetEnumerator as an extension method, which means you can iterate through what was previously impossible. The developers' motivation to introduce this feature were use cases like iterating through a tuple:

public static class TupleExtensions
{
    public static IEnumerator<T>
        GetEnumerator<T>(this ValueTuple<T, T, T, T> tuple)
    {
        yield return tuple.Item1;
        yield return tuple.Item2;
        yield return tuple.Item3;
        yield return tuple.Item4;
    }
}
foreach(var item in (1, 2, 3, 4))
{
   //1 2 3 4
}

Now you can even iterate through Range:

public static IEnumerator<Index> GetEnumerator(this Range number)
{
    for (Index i = number.Start; i.Value < number.End.Value; i = i.Value + 1)
    {
        yield return i;
    }
}

public static void Main()
{
    foreach (var i in 1..5)
    {
        Console.WriteLine(i);
        //1 2 3 4
    }
}

Instead of the range, you could do this for other types, for example, int. What is wrong with this? Microsoft documentation explicitly states that the ranges are not intended for this. The most common recommendation is to use the GetEnumerator extension method only when the application design justifies this. This makes sense - many code reviewers may be surprised when they see an enumerated Object.

Pattern matching enhancements

In C# 9 we got more keywords: and, not, or. They are used in template syntax, and with them, it is now possible to use comparison operators (<, <=, >, >=) and parentheses. Here is a comprehensive example that demonstrates such syntax in a logical expression:

public static bool IsPasses(Student student)
{
    return student is ({ AverageScore: >= 4.4, } or { Privilege: true }) 
                   and not {Department: "Central" };
}

static void Main()
{
    Student nullCandidate = null;
    var candidate = new Student(name: "Tom", surname: "Twain",
        department: "South", privilege: false, score: 4.6);

    Console.WriteLine(IsPasses(nullCandidate));
    //false

    Console.WriteLine(IsPasses(candidate));
    //true
}

Of course, all of this is syntactic sugar. You can use more classical syntax to implement the same method:

public static bool IsPasses2(Student student)
{
    return    student != null
           && (student.AverageScore >= 4.4 || student.Privilege == true) 
           &&  student.Department != "Central";
}

By the way, note that in the new version of code the check for null is automatic. When comparing methods, you can also notice that the new syntax is significantly more readable.

What's more, the enhanced pattern matching in C# 9 also affected switch statements. Let's create a similar method:

public static bool IsPassesCommon(Student student)
    => student switch
    {
        { Privilege: true} => true,
        { AverageScore: >= 3.5 } and {AverageScore: <= 4.5 } => true,
        _ => false
    };

By the way, before C# 9, switch expressions required the variable name (or an underscore instead of it) after the class name, even if this name was not used anywhere. Now you can omit it:

public static bool IsStudies(Person person)
    => person switch
    {
        Student => true,
        Teacher => false,
        _ => false
    };

Local function attributes

Here everything is pretty clear. You can now apply attributes to local functions. For example, the Conditional attribute:

static void Main()
{
    [Conditional("DEBUG")]
    static void PrintDebug()
    {
        Console.WriteLine("This is debug mode");
    }

    PrintDebug();
    Console.WriteLine("Hello World!");
    //Debug:
    //This is debug mode
    //Hello World!

    //Release:
    //Hello World!
    }
}

New data types and performance

To improve support of low-level libraries that require high performance, C# 9 introduced a few new data types and extension features for unsafe code. Most developers probably do not need this, but it's a good idea to familiarize yourself with this new addition.

New data types: nint, nuint and half. It is easy to guess that the first two are integers whose size depends on the operating system's digit capacity: 4 bytes in 32-bit systems, and 8 bytes in 64-bit systems. half is a 16-bit real number that is mainly intended to store information when there is no requirement for high precision. Yes, I meant only storage, I did not include arithmetic operations.

Two more enhancements that work only in the unsafe mode are the SkipLocalsInit attribute to suppress localsinit flags, as well as pointers to functions. From the documentation:

public static T UnsafeCombine<T>(delegate*<T, T, T> comb, T left, T right) => 
    comb(left, right);
....
static int localMultiply(int x, int y) => x * y;
int product = UnsafeCombine(&localMultiply, 3, 4);

Code Generators

What are they?

Another big new feature is code generators. It is so big it won't fit into this article's scope - fortunately, there are already enough articles dedicated to this topic on the Internet. Shortly speaking - they allow you to check programs and supplement them with new code files during compilation. However, I'd like to focus on syntax changes that came with the generators.

Partial methods

Partial methods were upgraded to work better with code generators. Partial methods existed before, but now they can have a return value, out parameters, and access modifiers. This means, they are now not that different from usual methods:

public partial class Person
{
    public string Name { get; set; }
    public string Surname { get; set; }
    public Person(string name, string surname)
    {
        Name = name;
        Surname = surname;
    }
    public partial bool Speak(string line, out string text)
}
public partial class Person
{
    public partial bool Speak(string line, out string text)
    {
        if (string.IsNullOrEmpty(line))
            return false;

        text = Name + ": " + line; 
        Console.WriteLine(text);
        return true;
    }
}

It seems that now in C#, it is possible to separate header files and implementation, as well as do a forward declaration. C++ crept up from where you did not expect it.

I'll point out that if a partial method got an access modifier, the project will not compile without the implementation.

ModuleInitializerAttribute

The last addition is the ModuleInitializer attribute. It was introduced because libraries - including code generators - need initialization logic. The compiler calls methods that have this attribute before accessing a field or calling a method inside the module. Documentation describes the following requirements for initialization methods:

  • the method must be static;
  • the method must be a regular member method (i.e. not an access method, constructor, local function etc.);
  • the method must not have parameters;
  • the method must return void;
  • the method must not be generic or be a part of a generic class;
  • the method must be available from the containing module (possess the internal or public access modifier).

Here's an example:

public class Program
{
    static string StartMessage;

    [ModuleInitializer]
    public static void Init()
    {
        StartMessage = "Hello World!";
    }

    static void Main()
    {
        Console.WriteLine(StartMessage);
        //Hello World!
    }
}

I've also mentioned that an application can have several initialization methods:

public class Program
{
    static string StartMessage;

    [ModuleInitializer]
    internal static void Init1()
    {
        StartMessage = "Hello World!";
    }

    [ModuleInitializer]
    internal static void Init2()
    {
        StartMessage = "foo bar";
    }

    static void Main()
    {
        Console.WriteLine(StartMessage);
        //foo bar
    }
}

We cannot affect the order in which initialization methods will be called (at least, there's no feature for it, per se), but the order is always the same. From this example, you might assume that methods are called one after the other and the result is obvious, but this is not so. When initialization methods are in different parts of an application (or just in different classes), the result may be unexpected. This is why it's better to logically separate them in order to avoid a state similar to the one above. Finally, let's take a look at a peculiar example that someone may have been thinking about:

[ModuleInitializer]
public static void Main()
{
    Console.WriteLine("Hello World!");
    //Hello World!
    //Hello World!
}

Yes, the method was shamelessly called twice. I think it's clear why you shouldn't apply the ModuleInitializer attribute to the Main method.

Conclusion

The list of new features in C# 9 turned out to be pretty impressive. A significant part of the enhancements is nothing more than syntactic sugar that does not fundamentally change anything, but does make developers' lives easier. As for the rest, we excited to see how C# keeps gradually developing to meet modern needs.

If you want to study C# 9 new features in more detail, take a look at Microsoft's documentation that will link you straight to technical articles whose links I used in this text.