On C# 9.0 Features

On C# 9.0 Features
   I started writing articles listing every newly released C# feature in the 3.0 timeframe. At the time there was not one good place that would post a list of all the features for a new version with explanations how they worked. You could find a list of features and you could find every feature explained separately in some blog post but it was surprisingly hard to find one list with explanation and people who would sometimes do it for one version didn’t do it for the next. Since C# 7.0 this is no longer the case as the official docs have good, detailed lists by version and there was indeed a C# 6 post somewhere on Codeplex that is now lost. I just kept my habit of writing these posts. However, these days I am a little busier and what is more the amount of features and frequency of releases has increased so I can barely write a post about one version before the next one hits. I do still see value in these posts as I include my opinion about a feature. I sometimes go back and check if I use a feature as much as I expected. From now on in these posts I will just link to the official feature explanation and go on with my opinion. When reading this article you can only read the relevant (linked) section from the docs, the other sections will be linked before discussing them.

Init-only Setters


   Init-only setters are a feature that is supposed to help with immutability. C# programmers are obsessed with the object initializer syntax. This is

var person = new Person
{
   Name = "Stilgar",
   Age = 38
}

It is relatively clean and concise way of declaring an object and the declaration is also short (no need to write a constructor with arguments and assign properties inside it)

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

One problem with this is that it requires the setters to be mutable even if the programmer doesn’t really want them to. This is what a proper immutable type would look like

class Person
{
   public Person (string name, int age)
   {
        Name = name;
        Age = age;
   }
   public string Name { get; }
   public string Age { get; }
}

And we repeated these name and age identifiers 4 times each. It is easy to see why so many C# devs think "screw immutability" and just use the shorter version. Init-only setters allow C# devs to continue being lazy while using somewhat immutable types. I have a big problem with this feature – it does not require the property to be set and therefore doesn’t play well with nullable reference types and everyone should be using nullable reference types. You still need the ugly = null!; on your properties if you have them enabled and you won’t get a warning if you don’t set a property. This makes this feature borderline useless because and I can’t stress this enough you should always enable nullable reference types. In addition, it is my understanding that you can set the property elsewhere if you are using a .NET language not aware of init-only setters (haven’t tested this). Overall, I can say I hate this feature. It is used to implement one of the best C# 9.0 features – records but they could have been implemented in another way if it was decided that init-only setters should not be a separate feature.

Records


   Records are the biggest and most important C# 9.0 feature. They allow us to declare immutable types as succinctly. Let’s do the Person type from above as a record

record Person
(
   string Name,
   int Age
)

This does everything our properly immutable version above did and more. Unlike the lazy programmer version this one (which is also lazy because it is short) has an actual constructor which can in the future be expanded to real constructor with code in it and also produces proper errors when a required field is not assigned. I expect records to become the default DTO type. ASP.NET model binders can handle them and their immutability better reflects something received over HTTP. You can’t change what you received over HTTP so an immutable type is in order.

Of course you’ve read the docs and know that this is called positional record and you can also create records with the property syntax or with mutable properties. The latter two are collectively known as the wrong ways to create records. I can think of some reasons to use the wrong records but I really wish they didn’t exist so not only would C# make immutability easy but also make mutability hard. One thing I am very worried about is the addiction to the aforementioned object initializer syntax. I have already argued with a bunch of C# devs on Facebook and on Reddit that the object initializer syntax is just wrong in the presence of records. Most of them simply didn’t realize you could do this

var person = new Person
(
   Name: "Stilgar",
   Age: 38
);

They knew all the features involved in this - constructors, named parameters, default parameter values (not shown here) they just didn’t realize you could combine them like that. People are so sunk into the object initializer syntax that they never thought that there is a better way in 2021. I converted some of them to the proper way of initializing objects (the constructor), but in an internet argument some of them would refuse to accept their error and throw really stupid arguments like saying that it is confusing to have parenthesis formatted like that, so they’d keep using the object initializer syntax with all its flaws because apparently the very same syntax with braces is somehow better. Yeah, I really wish mutable records and init-only properties were not a thing. They certainly won’t be in any project where the decision is mine.

   Because you read the docs you know about value equality, built-in formatting and non-destructive mutation. I love all of these, they are great little additions to the important thing. Still the important thing is that records can be used to create immutable types with succinct declarations that respect nullability.

Top-level Statements


   The Top-level Statements feature is pretty straight-forward. There are some use cases like small utility programs and maybe Azure Functions but in reality, I don’t know any professional programmer who ever said “My life would be so much better if my program’s entry point didn’t need these additional 6 lines of code 4 of which contain a single brace”. One place where it would be helpful is teaching beginners. While in the time I have taught courses I’ve never ever seen anyone struggle with this, I always took some time to explain to the students to ignore the additional code and only write in the body of the Main method. I am glad people who teach C# won’t need to do that anymore.

Pattern Matching Enhancements


   As I’ve pointed out since the introduction of pattern matching – the more patterns the better. It is one of the rare parts of the language where additional features are always good. You can just express more things as patterns and whatever burden this introduces only affects this small corner of the language. If you use pattern matching you’d love more of it, if you don’t then how much features they throw of it won’t concern you.

   One of the new patterns namely the not pattern is of special interest. It allows us to switch to null checking with if (x is null) and if (x is not null). As you may know if you are a regular reader, I am a big fan of using words instead of symbols for operators that do not come directly from math. Obviously null checking does not come from math, so I’d really love to use this way to do null checking. I have not tested how it plays with expression trees and Entity Framework but if it does play well, I will try to introduce it at least in new projects. One might wonder if and and or patterns can replace && and || but I don’t believe this is wise. Their syntax is quite different and there are edge cases.

Performance and Interop Features


   The Performance and Interop Features are fine I guess. I can see a reason for each of them to exist although I have never written code that requires any of them but this is normal since I don’t write any low-level libraries.

Fit and Finish Features


   The Fit and Finish Features contain some very pleasant improvements. The target-typed new() is very appealing to me because I am a big proponent of the style of var usage referred to “var when type is apparent”. There is an issue with this because people can’t agree on what apparent type means. Most notably

var customers = customersQuery.ToList();

Some say the type is obviously List but you need to believe the method name and more importantly you don’t know what type the items in the list are which is crucial information. With the addition of target-typed new() I can simply ban any var usage (yeah, with the exception of anonymous types). The code will not get more bloated because there will still be only one mention of the type per line. Even better target-typed new() works with field and property initializers which will reduce bloat and make the practice consistent between variables and fields/properties. Sadly, this is not a change you can just introduce in an existing project because it will affect most variable declarations. I am looking forward to experimenting with this on some greenfield project.

   The conditional expressions type resolution improvements are great. I’ve needed to resolve this int? vs int thing with an explicit cast so many times. Now a lot of these corner cases will just work.

   The static modifier on lambdas (as well as on local functions) is useful to prevent accidental performance regressions but I am not sure I’d bloat the code with it. It would be cool if it was the default behavior and there was a keyword to indicate that you allow capture but it is too late for that now. Discards on lambdas are obviously useful addition too.

   I don’t really care about foreach with extension GetEnumerator but applying attributes to local functions is useful. As the docs note nullability attribute are an obvious use case.

Source Generators


   Source Generators are a feature which allows adding code that runs as part of the compilation process that can expect existing code and generate additional code to be added to the project. It is not allowed to change existing code but it enables some pretty cool things. The main use case is improving performance by removing runtime generation and reflection. This is very useful for reducing startup time. To get a better feeling about the possibilities this feature opens you can read how the new JSON serializers utilize it. Once source generators ship we will see more and more libraries and frameworks improve performance using it. The future is bright.
Tags:   english programming 
Posted by:   Stilgar
19:42 25.09.2021

Comments:

First Previous 1 Next Last 

Posted by   JOKe (Unregistered)   on   13:17 28.09.2021

is record instance of Type ? I mean if I do Person is Type ? is it true ? How do you cehck if something is a record or not ? for exampel for struts before you had IsValueType on the Type itself, but is there isRecordType or ?  

Also about records :

Can you define a constructor if you want for a record ?
For example Java example :
public record Person(String name, String address) {
   public Person {
       Objects.requireNonNull(name);
       Objects.requireNonNull(address);
   }
}

or

public record Person(String name, String address) {
   public Person(String name) {
       this(name, "Unknown");
   }
}

both have use cases.

Zero fucks given about the other features :) however I have one feature request to you:
When you created this noblog we were young and I find it super hard to read the text this days :) can you increase the font size with at least 10% ? :D

Posted by   Stilgar   on   13:58 28.09.2021

I don't think there is an easy way though people have found a way to check (looking for specially named <Clone>$ method via reflection). Why would you want to do that. Records are reference types. It seems that unless you are building some disassembly tool there is no reason to check if a type is a record.

Yes, you can add constructor body and alternative constructors to records but you will have to type in the properties. They are thinking of adding some shortcuts for null checks which I think are not important since you can just turn on nullable reference types in C# but of course for library authors who need to support usage where nullable reference types are not turned on and therefore do need the proper exception it would save a few lines of code. This proposal will not make it in C# 10 if at all but it is being discussed actively.

You might want to use reading mode. Sadly I have no idea why new Edge can't detect whatever things makes the button appear. Old Edge and Firefox handle it but not new Edge (probably other Chromium browsers that have reading mode too)

Posted by   JOKe (Unregistered)   on   14:05 28.09.2021

well not disassembly only but lets say I am making a library that is deserializing content from some proprietary format I need to check is it a record or a struct in order to know how to create it and set stuff, in Java we have isRecord and getRecordComponents[] and on the RecordComponent you have a lot more https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/reflect/RecordComponent.html

Ok so in short C# 9 records again a nice language feature and half baked API, but what can you expect from a language where the main type Type was implementing an interface called _Type .. :) I hope they fixed 20 years later :)

Posted by   Stilgar   on   14:19 28.09.2021

There is no difference between classes and records for this. You'd do for records the same thing you'd do for classes.

Posted by   JOKe (Unregistered)   on   09:24 30.09.2021

yes ok for this you are right indeed, but if I have to write a runtime and I have to persist the record in memory I would need this, but I guess Microsoft knows that no one writes runtimes for dotnot except them.

Posted by   Stilgar   on   18:28 01.10.2021

Why? For the runtime the record is just a class. The only thing that knows about records is the compiler.

Posted by   Guest (Unregistered)   on   12:11 12.10.2021

First Previous 1 Next Last 


Post as:



Post a comment: