About new items in .NET 5 and C # 9.0

Good afternoon.



We have used .NET since its inception. We have solutions written in all versions of the framework in production: from the very first to the latest .NET Core 3.1.



The history of .NET, which we have been closely following all this time, is happening before our eyes: the version of .NET 5, which is planned to be released in November, has just been released in the form of Release Candidate 2. We have long been warned that the fifth version will be epoch-making: it will end .NET schizophrenia, when there were two branches of the framework: classic and core. Now they will merge in ecstasy, and there will be one continuous .NET.



Released RC2you can already start to fully use it - no new changes are expected before the release, there will only be a fix of found bugs. Moreover: RC2 already has an official website dedicated to .NET.



And we present you with an overview of the innovations in .NET 5 and C # 9. All information with code examples was taken from the official blog of the developers of the .NET platform (as well as from many other sources) and verified personally.



New native and just new types



C # and .NET simultaneously added native types:



  • nint and nuint for C #
  • their corresponding System.IntPtr and System.UIntPtr in BCL


The point for adding these types is operations with low-level APIs. And the trick is that the real size of these types is determined already at runtime and depends on the bitness of the system: for 32-bit ones, their size will be 4 bytes, and for 64-bit ones, respectively, 8 bytes.



Most likely you will not come across these types in real work. As, however, with another new type: Half. This type exists only in BCL, there is no analogue for it in C # yet. It is a 16-bit type for floating point values. It can come in handy for those cases when hellish precision is not required, and you can win some memory for storing values, because the types float and double occupy 4 and 8 bytes. The most interesting thing is that for this type in general so fararithmetic operations are not defined, and you cannot even add two variables of type Half without explicitly casting them to float or double. That is, the purpose of this type is now purely utilitarian - to save space. However, they plan to add arithmetic to it in the next release of .NET and C #. In a year.



Attributes for local functions



Previously, they were prohibited, and this created some inconvenience. In particular, it was impossible to hang attributes of the parameters of local functions. Now you can set attributes for them, both for the function itself and for its parameters. For example, like this:



#nullable enable
private static void Process(string?[] lines, string mark)
{
    foreach (var line in lines)
    {
        if (IsValid(line))
        {
            // Processing logic...
        }
    }

    bool IsValid([NotNullWhen(true)] string? line)
    {
        return !string.IsNullOrEmpty(line) && line.Length >= mark.Length;
    }
}


Static lambda expressions



The point of the feature is to make sure that lambda expressions cannot capture any context and local variables that exist outside the expression itself. In general, the fact that they can capture the local context is often useful in development. But sometimes this can be the cause of hard-to-catch errors.



To avoid such errors, lambda expressions can now be marked with the static keyword. And in this case, they lose access to any local context: from local variables to this and base.



Here's a pretty comprehensive usage example:



static void SomeFunc(Func<int, int> f)
{
    Console.WriteLine(f(5));
}

static void Main(string[] args)
{
    int y1 = 10;
    const int y2 = 10;
    SomeFunc(i => i + y1);          //  15
    SomeFunc(static i => i + y1);   //  : y1    
    SomeFunc(static i => i + y2);   //  15
}


Note that constants capture static lambdas just fine.



GetEnumerator as Extension Method



Now the GetEnumerator method can be an extension method, which will allow you to iterate through the foreach even that could not be iterated over before. For example - tuples.



Here is an example when it becomes possible to iterate over ValueTuple via foreach using the extension method written for it:



static class Program
{
    public static IEnumerator<T> GetEnumerator<T>(this ValueTuple<T, T, T, T, T> source)
    {
        yield return source.Item1;
        yield return source.Item2;
        yield return source.Item3;
        yield return source.Item4;
        yield return source.Item5;
    }

    static void Main(string[] args)
    {
        foreach(var item in (1,2,3,4,5))
        {
            System.Console.WriteLine(item);
        }
    }
}


This code prints numbers from 1 to 5 to the console.



Discard pattern in parameters of lambda expressions and anonymous functions



Micro-improvement. In case you don't need parameters in a lambda expression or in an anonymous function, you can replace them with an underscore, thereby ignoring:



Func<int, int, int> someFunc1 = (_, _) => {return 5;};
Func<int, int, int> someFunc2 = (int _, int _) => {return 5;};
Func<int, int, int> someFunc3 = delegate (int _, int _) {return 5;};


Top-level statements in C #



This is a simplified C # code structure. Now, writing the simplest code really looks simple:



using System;

Console.WriteLine("Hello World!");


And it will all compile just fine. That is, now you do not need to create a method in which the console output statement should be placed, you do not need to describe any class in which the method should be placed, and there is no need to define a namespace in which the class should be created.



By the way, in the future, C # developers are thinking of developing a topic with simplified syntax and trying to get rid of the using System; in obvious cases. In the meantime, you can get rid of it by simply writing like this:



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


And it really will be a single line working program.



More complex options can be used:



using System;
using System.Runtime.InteropServices;

Console.WriteLine("Hello World!");
FromWhom();
Show.Excitement("Top-level programs can be brief, and can grow as slowly or quickly in complexity as you'd like", 8);

void FromWhom()
{
    Console.WriteLine($"From {RuntimeInformation.FrameworkDescription}");
}

internal class Show
{
    internal static void Excitement(string message, int levelOf)
    {
        Console.Write(message);

        for (int i = 0; i < levelOf; i++)
        {
            Console.Write("!");
        }

        Console.WriteLine();
    }
}


In reality, the compiler itself will wrap all this code in the necessary namespaces and classes, you just won't know about it.



Of course, this feature has limitations. The main one is that this can only be done in one project file. As a rule, it makes sense to do this in the file where you previously created the entry point to the program in the form of the Main (string [] args) function. At the same time, the Main function itself cannot be defined there - this is the second limitation. In fact, such a file itself with a simplified syntax is the Main function, and it even contains the args variable implicitly, which is an array with parameters. That is, this code will also compile and display the length of the array:



System.Console.WriteLine(args.Length);


In general, the feature is not the most important, but for demonstration and training purposes it is quite suitable for itself. Details here .



Pattern matching in an if statement



Imagine that you need to check an object variable that it is not of a certain type. Until now, it was necessary to write like this:



if (!(vehicle is Car)) { ... }


But with C # 9.0, you can write humanly:



if (vehicle is not Car) { ... }


It also became possible to compactly record some checks:



if (context is {IsReachable: true, Length: > 1 })
{
    Console.WriteLine(context.Name);
}


This new notation is equivalent to the good old one like this:



if (context is object && context.IsReachable && context.Length > 1 )
{
    Console.WriteLine(context.Name);
}


Or you can also write the same thing in a relatively new way (but this is already yesterday):



if (context?.IsReachable && context?.Length > 1 )
{
    Console.WriteLine(context.Name);
}


In the new syntax, you can also use the boolean operators and, or and not, plus, parentheses to prioritize:



if (context is {Length: > 0 and (< 10 or 25) })
{
    Console.WriteLine(context.Name);
}


And these are just improvements to pattern matching in a regular if. What we added to the pattern matching for the switch expression - read on.



Improved pattern matching in switch expression



The switch expression (not to be confused with the switch statement) has been hugely improved in terms of pattern matching. Let's look at examples from the official documentation . Examples are devoted to calculating the fare of a certain transport at a certain time. Here's the first example:



public decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car c           => 2.00m,
    Taxi t          => 3.50m,
    Bus b           => 5.00m,
    DeliveryTruck t => 10.00m,
    { }             => throw new ArgumentException("Unknown vehicle type", nameof(vehicle)),
    null            => throw new ArgumentNullException(nameof(vehicle))
};


The last two lines in the switch statement are new. Curly braces represent any object that is not null. And you can now use the matching keyword to match against null.



This is not all. Note that for each mapping to an object, you have to create a variable: c for Car, t for Taxi, and so on. But these variables are not used. In such cases, you can already use the discard pattern in C # 8.0 now:



public decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car _           => 2.00m,
    Taxi _          => 3.50m,
    Bus _           => 5.00m,
    DeliveryTruck _ => 10.00m,
    // ...
};


But starting with the ninth version of C #, you can write nothing at all in such cases:



public decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car             => 2.00m,
    Taxi            => 3.50m,
    Bus             => 5.00m,
    DeliveryTruck   => 10.00m,
    // ...
};


The improvements to the switch expression don't end there. It is now easier to write more complex expressions. For example, often the result returned must depend on the property values ​​of the passed object. Now this can be written shorter and more convenient than a combination of if's:



public static decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car { Passengers: 0 } => 2.00m + 0.50m,
    Car { Passengers: 1 } => 2.0m,
    Car { Passengers: 2 } => 2.0m - 0.50m,
    Car => 2.00m - 1.0m,
    // ...
};


Pay attention to the first three lines in the switch: in fact, the value of the Passengers property is checked, and in case of equality, the corresponding result is returned. If there is no match, then the value for the general variant will be returned (the fourth line inside the switch). By the way, property values ​​are checked only if the passed vehicle object is not null and is an instance of the Car class. That is, you should not be afraid of Null Reference Exception when checking.



But that's not all. Now, in the switch expression, you can even write expressions for more convenient matching:



public static decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
    Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
    Bus => 5.00m,

    DeliveryTruck t when (t.GrossWeightClass >= 5000) => 10.00m + 5.00m,
    DeliveryTruck t when (t.GrossWeightClass >= 3000 && t.GrossWeightClass < 5000) => 10.00m,
    DeliveryTruck => 8.00m,
    // ...
};


And that's not all. The switch expression syntax has been extended to nested switch expressions to make it even easier for us to describe complex conditions:



public static decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car c => c.Passengers switch
    {
        0 => 2.00m + 0.5m,
        1 => 2.0m,
        2 => 2.0m - 0.5m,
        _ => 2.00m - 1.0m
    },
    // ...
};


As a result, if you completely glue all the code examples already given, you get the following picture with all the described innovations at once:



public static decimal CalculateToll(object vehicle) =>
    vehicle switch
{
    Car c => c.Passengers switch
    {
        0 => 2.00m + 0.5m,
        1 => 2.0m,
        2 => 2.0m - 0.5m,
        _ => 2.00m - 1.0m
    },

    Taxi t => t.Fares switch
    {
        0 => 3.50m + 1.00m,
        1 => 3.50m,
        2 => 3.50m - 0.50m,
        _ => 3.50m - 1.00m
    },

    Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
    Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
    Bus => 5.00m,

    DeliveryTruck t when (t.GrossWeightClass >= 5000) => 10.00m + 5.00m,
    DeliveryTruck t when (t.GrossWeightClass >= 3000 && t.GrossWeightClass < 5000) => 10.00m,
    DeliveryTruck => 8.00m,

    null => throw new ArgumentNullException(nameof(vehicle)),
    _ => throw new ArgumentException(nameof(vehicle))
};


But that's not all either. Here's another example: an ordinary function that uses the switch expression mechanism to determine the load based on the passed time: morning / evening rush hour, day and night periods:



private enum TimeBand
{
    MorningRush,
    Daytime,
    EveningRush,
    Overnight
}

private static TimeBand GetTimeBand(DateTime timeOfToll) =>
    timeOfToll.Hour switch
    {
        < 6 or > 19 => TimeBand.Overnight,
        < 10 => TimeBand.MorningRush,
        < 16 => TimeBand.Daytime,
        _ => TimeBand.EveningRush,
    };


As you can see, in C # 9.0 it is also possible to use the comparison operators <,>, <=,> =, as well as the logical operators and, or and not, when matching.



But this, damn it, is not the end. You can now use ... tuples in the switch expression. Here is a complete example of code that calculates a certain coefficient to the fare, depending on the day of the week, time of day and direction of travel (to / from the city):



private enum TimeBand
{
    MorningRush,
    Daytime,
    EveningRush,
    Overnight
}

private static bool IsWeekDay(DateTime timeOfToll) =>
    timeOfToll.DayOfWeek switch
{
    DayOfWeek.Saturday => false,
    DayOfWeek.Sunday => false,
    _ => true
};

private static TimeBand GetTimeBand(DateTime timeOfToll) =>
    timeOfToll.Hour switch
{
    < 6 or > 19 => TimeBand.Overnight,
    < 10 => TimeBand.MorningRush,
    < 16 => TimeBand.Daytime,
    _ => TimeBand.EveningRush,
};

public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
    (true, TimeBand.MorningRush, true) => 2.00m,
    (true, TimeBand.MorningRush, false) => 1.00m,
    (true, TimeBand.Daytime, true) => 1.50m,
    (true, TimeBand.Daytime, false) => 1.50m,
    (true, TimeBand.EveningRush, true) => 1.00m,
    (true, TimeBand.EveningRush, false) => 2.00m,
    (true, TimeBand.Overnight, true) => 0.75m,
    (true, TimeBand.Overnight, false) => 0.75m,
    (false, TimeBand.MorningRush, true) => 1.00m,
    (false, TimeBand.MorningRush, false) => 1.00m,
    (false, TimeBand.Daytime, true) => 1.00m,
    (false, TimeBand.Daytime, false) => 1.00m,
    (false, TimeBand.EveningRush, true) => 1.00m,
    (false, TimeBand.EveningRush, false) => 1.00m,
    (false, TimeBand.Overnight, true) => 1.00m,
    (false, TimeBand.Overnight, false) => 1.00m,
};


The PeakTimePremiumFull method uses tuples for matching, and this became possible in the new version of C # 9.0. By the way, if you look closely at the code, then two optimizations suggest themselves:



  • the last eight lines return the same value;
  • day and night traffic have the same coefficient.


As a result, the method code can be greatly reduced using the discard pattern:



public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
    (true, TimeBand.MorningRush, true)  => 2.00m,
    (true, TimeBand.MorningRush, false) => 1.00m,
    (true, TimeBand.Daytime,     _)     => 1.50m,
    (true, TimeBand.EveningRush, true)  => 1.00m,
    (true, TimeBand.EveningRush, false) => 2.00m,
    (true, TimeBand.Overnight,   _)     => 0.75m,
    (false, _,                   _)     => 1.00m,
};


Well, if you look even more closely, then you can reduce this option, taking out the coefficient 1.0 in the general case:



public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
    (true, TimeBand.Overnight, _) => 0.75m,
    (true, TimeBand.Daytime, _) => 1.5m,
    (true, TimeBand.MorningRush, true) => 2.0m,
    (true, TimeBand.EveningRush, false) => 2.0m,
    _ => 1.0m,
};


Just in case, let me clarify: comparisons are made in the order in which they are listed. On the first match, the corresponding value is returned and no further comparisons are made.

Update



Tuples in switch expression can be used in C # 8.0 as well. The worthless developer who wrote this article just got a little smarter.





And finally, here's another crazy example that demonstrates the new syntax for matching with both tuples and object properties:



public static bool IsAccessOkOfficial(Person user, Content content, int season) => 
    (user, content, season) switch 
{
    ({Type: Child}, {Type: ChildsPlay}, _)          => true,
    ({Type: Child}, _, _)                           => false,
    (_ , {Type: Public}, _)                         => true,
    ({Type: Monarch}, {Type: ForHerEyesOnly}, _)    => true,
    (OpenCaseFile f, {Type: ChildsPlay}, 4) when f.Name == "Sherlock Holmes"  => true,
    {Item1: OpenCaseFile {Type: var type}, Item2: {Name: var name}} 
        when type == PoorlyDefined && name.Contains("Sherrinford") && season >= 3 => true,
    (OpenCaseFile, var c, 4) when c.Name.Contains("Sherrinford")              => true,
    (OpenCaseFile {RiskLevel: >50 and <100 }, {Type: StateSecret}, 3) => true,
    _                                               => false,
};


This all looks rather unusual. For a complete understanding, I recommend that you look at the source , there is a complete example of the code.



New new as well as basically improved target typing



A long time ago in C # it became possible to write var instead of a type name, because the type itself could be determined from the context (in fact, this is called target typing). That is, instead of the following entry:



SomeLongNamedType variable = new SomeLongNamedType();


it became possible to write more compactly:



var variable = new SomeLongNamedType()


And the compiler will guess the type of variable itself. Over the years, the reverse syntax was implemented:



SomeLongNamedType variable = new ();


Special thanks for the fact that this syntax works not only when declaring a variable, but also in many other cases where the compiler can immediately guess the type. For example, when passing parameters to a method and returning a value from the method:



var result = SomeMethod(new (2020,10,01));

//...

public Car SomeMethod(DateTime p)
{
    //...

    return new() { Passengers = 2 };
}


In this example, when calling SomeMethod, the parameter of the DateTime type is created using the shorthand syntax. The value returned from the method is created in the same way.



Where there will really be a benefit from this syntax is when defining collections:



List<DateTime> datesList = new()
{
    new(2020, 10, 01),
    new(2020, 10, 02),
    new(2020, 10, 03),
    new(2020, 10, 04),
    new(2020, 10, 05)
};

Car[] cars = 
{
    new() {Passengers = 2},
    new() {Passengers = 3},
    new() {Passengers = 4}
};


The absence of the need to write the full type name when listing the elements of the collection makes the code a little cleaner.



Target typed operators ?? and?:



The ternary operator?: Was improved in C # 9.0. Previously, it required full compliance of return types, but now it is more smart. Here is an example of an expression that was invalid in earlier versions of the language, but quite legal in the ninth:



int? result = b ? 0 : null; // nullable value type


Previously, it was required to explicitly cast from zero to int? .. Now it is not necessary.



Also, in the new version of the language, it is permissible to use the following construction:



Person person = student ?? customer; // Shared base type


The customer and student types, although derived from Person, are technically different. The previous version of the language did not allow you to use such a construct without explicit type casting. Now the compiler understands perfectly well what is meant.



Overriding the return type of methods



In C # 9.0, it was allowed to override the return type of overridden methods. There is only one requirement: the new type must be inherited from the original (covariant). Here's an example:



abstract class Animal
{
    public abstract Food GetFood();
    ...
}
class Tiger : Animal
{
    public override Meat GetFood() => ...;
}


In the Tiger class, the return value of the GetFood method has been redefined from Food to Meat. It is now okay if Meat is derived from Food.



init properties are not really readonly members



An interesting feature has appeared in the new version of the language: init-properties. These are properties that can only be set during initial initialization of the object. It would seem that readonly class members exist for this, but in fact they are different things that allow you to solve different problems. To understand the difference and the beauty of init properties, here's an example:



Person employee = new () {
    Name = "Paul McCartney",
    Company = "High Technologies Center",
    CompanyAddress = new () {
        Country = "Russia",
        City = "Izhevsk",
        Line1 = "246, Karl Marx St."
    }
}


This syntax for declaring a class instance is very convenient, especially when there are more objects among the class properties. But this syntax has limitations: the corresponding class properties must be mutable . This is because the initialization of these properties occurs after the call to the constructor. That is, the Person class from the example should be declared like this:



class Person {
    //...
    public string Name {get; set;}
    public string Company {get; set;}
    public Address CompanyAddress {get; set;}
    //...
}


However, in fact, the Name property is immutable. Currently, the only way to make this read-only property is to declare a private setter:



class Person {
    //...
    public string Name {get; private set;}
    //...
}


But in this case, we immediately lose the ability to use the convenient syntax for declaring a class instance by assigning values ​​to properties inside curly braces. And we can set the value of the Name property only by passing it in parameters to the class constructor. Now imagine that the CompanyAddress property is, in fact, immutable as well. In general, I found myself in such a situation many times, and I always had to choose between two evils:



  • fancy constructors with a bunch of parameters, but all the properties of the read-only class;
  • convenient syntax for creating an object, but all the properties of the read-write class, and I must remember this and not accidentally change them somewhere.


At this point, someone might recall the readonly members of the class and suggest styling the Person class like this:



class Person {
    //...
    public readonly string Name;
    public readonly string Company;
    public readonly string CompanyAddress;
    //...
}


To which I will answer that this method is not only not in Feng Shui, but it also does not solve the problem of convenient initialization: readonly members can also be set only in the constructor, like properties with a private setter.



But in C # 9.0 this problem is solved: if you define a property as an init property, you get both a convenient syntax for creating an object, and a property that is actually immutable in the future:



class Person {
    public string Name { get; init; }
    public string Company { get; init; }
    public Address CompanyAddress { get; init; }
}


By the way, in init-properties, as in the constructor, you can initialize readonly class members, and you can write like this:



public class Person
{
    private readonly string name;
       
    public string Name
    { 
        get => name; 
        init => name = (value ?? throw new ArgumentNullException(nameof(Name)));
    }
}


Record is a legalized DTO's



Continuing the topic of immutable properties, we come to the main, in my opinion, innovation of the language: the record type. This type is designed to conveniently create entire immutable structures, not just properties. The reason for the emergence of a separate type is simple: working according to all canons, we constantly create DTO's to isolate different layers of the application. DTOs are usually just a collection of fields, without any business logic. And, as a rule, the values ​​of these fields do not change during the lifetime of this DTO's.



.



DTO – Data Transfer Object. (DAL, BL, PL) - . «». -DTO' DAL BL, , DTO-, , DTO-, - ( HTML- JSON-).



β€” DTO-, - -, .



DTO- - . DTO-, AutoMapper - .



, DTO- .



So, after many, many years, the C # developers finally got to the really needed improvement: they legalized DTO models as a separate record type.



Until now, all DTO models that we created (and we created them in large quantities) were ordinary classes. For the compiler and for runtime, they were no different from all other classes, although they were not so in the classical sense. Few people have used structures for DTO models - this was not always acceptable for various reasons.



Now we can define record (hereinafter referred to as a record) - a special structure that is designed to create immutable DTO models. Recording takes an intermediate place between structures and classes in their usual sense. It is both subclass and superstructure. A record is still a reference type with all the ensuing consequences. Records almost always behave like a regular class, they can contain methods, they allow inheritance (but only from other records, not from objects, although if the record does not explicitly inherit from anything, then it inherits from object as implicitly as everything in C # ) can implement interfaces. Moreover, you don't have to make records completely immutable at all. And where, then, is the meaning and what is the difference?



Let's just create an entry:



public record Person 
{
    public string LastName { get; }
    public string FirstName { get; }

    public Person(string first, string last) => (FirstName, LastName) = (first, last);
}


And now here's an example of how to use it:



Person p1 = new ("Paul", "McCartney");
Person p2 = new ("Paul", "McCartney");

System.Console.WriteLine(p1 == p2);


This example will print true to the console. If Person were a class, then false would be printed to the console because objects are compared by reference: two reference variables are equal only if they refer to the same object. But that's not the case with recordings. Records are compared by the value of all their fields, including private ones.



Continuing with the previous example, let's look at this code:



System.Console.WriteLine(p1);


In the case of a class, we would receive the full name of the class in the console. But in the case of records, we will see this in the console:



Person { LastName = McCartney, FirstName = Paul}


The fact is that for records the ToString () method is implicitly overridden and displays not the type name, but a complete list of public fields with values. Similarly, for records, the == and! = Operators are implicitly redefined, which makes it possible to change the comparison logic.



Let's play with record inheritance:



public record Teacher : Person
{
    public string Subject { get; }

    public Teacher(string first, string last, string sub)
        : base(first, last) => Subject = sub;
}


Now let's create two posts of different types and compare them:



Person p = new("Paul", "McCartney");
Teacher t = new("Paul", "McCartney", "Programming");

System.Console.WriteLine(p == t);


Although the Teacher record is inherited from Person, the p and t variables will not be equal, false will be printed to the console. This is because the comparison is made not only for all fields of records, but also for types, and the types here are clearly different.



And although comparing inherited record types is allowed (but pointless), comparing different record types in general is not allowed in principle:



public record Person
{
    public string LastName { get; }
    public string FirstName { get; }

    public Person(string first, string last) => (FirstName, LastName) = (first, last);
}

public record Person2
{
    public string LastName { get; }
    public string FirstName { get; }

    public Person2(string first, string last) => (FirstName, LastName) = (first, last);
}

// ...

Person p = new("Paul", "McCartney");
Person2 p2 = new("Paul", "McCartney");
System.Console.WriteLine(p == p2);    //  


The entries seem to be the same, but there will be a compilation error on the last line. You can only compare records that are of the same type or inherited types.



Another nice feature of records is the with keyword, which makes it easy to create modifications to your DTO models. Take a look at an example:



Person me = new("Steve", "Brown");
Person brother = me with { FirstName = "Paul" };


In this example, for the brother record, the values ​​of all fields will be filled from the me record, except for the FirstName field - it will be changed to Paul.



So far, you've seen the classic way to create records β€” with full definitions of constructors, properties, and so on. But now there is also a laconic way:



public record Person(string FirstName, string LastName);

public record Teacher(string FirstName, string LastName,
    string Subject)
    : Person(FirstName, LastName);

public sealed record Student(string FirstName,
    string LastName, int Level)
    : Person(FirstName, LastName);


You can define records in shorthand, and the compiler will create the properties and constructor for you. However, this feature has an additional feature - you can not only use a shorthand notation to define properties and a constructor, but at the same time you can add your own method to the entry:



public record Pet(string Name)
{
    public void ShredTheFurniture() =>
        Console.WriteLine("Shredding furniture");
}

public record Dog(string Name) : Pet(Name)
{
    public void WagTail() =>
        Console.WriteLine("It's tail wagging time");

    public override string ToString()
    {
        StringBuilder s = new();
        base.PrintMembers(s);
        return $"{s.ToString()} is a dog";
    }
}


In this case, the properties and constructor of records will also be created automatically. Less and less boilerplate code, but only applicable to posts. This doesn't work for classes and structs.



In addition to everything already said, the compiler can also automatically create a deconstructor for records:



var person = new Person("Bill", "Wagner");

var (first, last) = person; //    
Console.WriteLine(first);
Console.WriteLine(last);


However, at the IL level, records are still a class. However, there is one suspicion for which no confirmation has yet been found: for sure, at the runtime level, records will be wildly optimized somewhere. Most likely, due to the fact that it will be known beforehand that a particular record is immutable. This opens up opportunities for optimization, at least in a multi-threaded environment, and the developer does not even need to put special efforts for this.



In the meantime, we are rewriting all DTO models from classes to records.



.NET Source Generators



Source Generator (hereinafter referred to as simply a generator) is a pretty interesting feature. A generator is a piece of code that is executed at the compilation stage, has the ability to analyze the already compiled code, and can generate additional code that will also be compiled. If it is not entirely clear, then here is one rather relevant example when a generator may be in demand.



Imagine a C # /. NET web application that you write in ASP.NET Core. When you launch this application, there is a huge amount of initialization background work on analyzing what this application is made of and what it should do at all. Reflection is used frantically. As a result, the time from launching the application to the start of processing the first request can be obscenely long, which is unacceptable in highly loaded services. The generator can help reduce this time: even at the compilation stage, it can analyze your already compiled application and additionally generate the necessary code that will initialize it much faster at startup.



There are also a fairly large number of libraries that use reflection to determine at runtime the types of objects used (among them there are many top Nuget packages). This opens up a huge scope for optimization using generators, and the authors of this feature expect the corresponding improvements from the library developers.



Code generators are a new topic and too unusual to fit within the scope of this post. Additionally, you can see an example of the simplest "Hello, world!" generator in this review .



There are two new features associated with code generators, which are described below.



Partial methods



Partial classes in C # have been around for a long time, their original purpose is to separate the code generated by a certain designer from the code written by the programmer. Partial methods have been tailored in C # 9.0. They look something like this:



public partial class MyClass
{
    public partial int DoSomeWork(out string p);
}
public partial class MyClass
{
    public partial int DoSomeWork(out string p)
    {
        p = "test";
        System.Console.WriteLine("Partial method");
        return 5;
    }
}


This surrogate example demonstrates that partial methods are essentially no different from ordinary ones: they can return values, they can accept out-variables, and they can have access modifiers.



From the information available, partial methods will be closely related to code generators, where they are intended to be used.



Module initializers



There are three reasons for introducing this functionality:



  • Allow libraries to have some kind of one-time initialization at boot with minimal overhead and no explicit need for the user to call anything;
  • the existing functionality of static constructors is not very suitable for this role, because the raintime must first figure out whether a class with a static constructor is used at all (these are the rules), and this gives measurable delays;
  • code generators must have some kind of initialization logic that doesn't need to be called explicitly.


Actually, the last point seems to have become decisive for the feature to be included in the release. As a result, we came up with a new attribute that we need to coat the method that is initialization:



using System.Runtime.CompilerServices;
class C
{
    [ModuleInitializer]
    internal static void M1()
    {
        // ...
    }
}


There are some restrictions on the method:



  • it must be static;
  • it must have no parameters;
  • it shouldn't return anything;
  • it shouldn't work with generics;
  • it must be accessible from the containing module, that is:

    • it must be internal or public
    • it doesn't have to be a local method


And it works like this: as soon as the compiler finds all the methods marked with the ModuleInitializer attribute, it generates special code that calls them all. The order of invocation of the initializer methods cannot be specified, but it will be the same at each compilation.



Conclusion



Having already published the post, we noticed that it is more devoted to the news in the C # 9.0 language than to the news of .NET itself. But it turned out well.



All Articles